mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Integrate package-based supervision with explicit recovery and visible workers
Retain planning guidance and upkeep; use stock edxeth, Intercom and scheduled prompts. Verify with 162 tests and isolated Fireworks trials. Document unresolved upstream reload and scheduler limitations. Co-Authored-By: Pi/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -279,3 +279,91 @@ export const completeGoalDescription =
|
||||
"## Log and ticks the goal [x]. If any check differs, it fails closed and requires a fresh supervisor review.";
|
||||
|
||||
export const completeGoalParamDescription = "The goal's text: the line after 'goal:' in the plan file.";
|
||||
|
||||
/* Package-based prototype, in flow order. Legacy exports above still serve src/index.ts. */
|
||||
|
||||
// Planning and interview. Keep the full drafting guide one-shot rather than repeating it each turn.
|
||||
export function prototypePlanning(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; give each distinct goal a failure mode, discriminator and evidence expectation above ## Log. Record the requested worker model in preferences. Use /goals review or /goals ready; /goals exit preserves the draft.`;
|
||||
}
|
||||
export function prototypePlanningSeed(objective: string, planPath: string): string {
|
||||
return `Draft or revise ${planPath} for this objective: ${objective}. Read any existing plan first. ${prototypePlanning(planPath)}\n\n${planDrafting}`;
|
||||
}
|
||||
export const prototypePlanDocument = (objective: string) => `# Goal plan\n\n## Objective\n${objective}\n\n## Goals\n\n## Log\n`;
|
||||
export const prototypeDiscuss = "Discuss the current draft in ordinary chat. Do not launch a worker or reopen the review menu until requested.";
|
||||
|
||||
// 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 childPlanRole = "You are the delegated implementation worker. Keep the supplied plan and evidence current; do not approve your own goals or launch a second writer. Call AttachGoalPlan with the explicit plan path in your task before implementation (also after reconnect if unbound). Send completion and blocker reports via Intercom to the supervisor ID supplied in the task, 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. Record the returned worker session path and confirmed Intercom ID in plan preferences for reconnection. Do not start a second writer. Inspect actual outputs when the child reports.\n\n${plan}`;
|
||||
}
|
||||
|
||||
// Supervision and turn-event upkeep (not a scheduled wake-up).
|
||||
export function prototypeSupervisor(workerName: string, planPath: string, supervisorId: string): string {
|
||||
return `You are the goal supervisor in the main chat for ${planPath}. Inspect actual artifacts, saved verification, applicable AGENTS.md and skills yourself; delegate implementation to '${workerName}'. Keep authorized work moving to the requested outcome, not merely approval paperwork. Investigate blocked/waiting/done claims and change ineffective instructions. Give brief visible assessments with judgment. You may maintain the plan but must not weaken the goal to accept worker output.
|
||||
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 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.`;
|
||||
}
|
||||
export function prototypeUpkeep(planPath: string, foldedPlan: string): string {
|
||||
return `Plan upkeep (turn event, not a timer): ${planPath} has had eight turns without working-set changes. Keep subtasks ([/] active), evidence and one useful ## Log/Learnings entry current. Move settled detail below ## Log, preserving the agreed outcome and user voice. Continue authorized unfinished work in your current role; a reminder is not permission to resume paused work.\n\n${foldedPlan}`;
|
||||
}
|
||||
export function prototypePlanContext(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}`;
|
||||
}
|
||||
export function planChangedReview(planPath: string): string {
|
||||
return `Plan 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.`;
|
||||
}
|
||||
export function manualReview(planPath: string): string {
|
||||
return `Review the current plan ${planPath}, worker progress and actual evidence. Do not launch a duplicate writer.`;
|
||||
}
|
||||
|
||||
// Check-ins. The installed scheduler owns storage/timing/UI. Removal guidance must never add jobs.
|
||||
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: 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.
|
||||
export const prototypeCompleteGoalDescription = "Parent supervisor or solo self-verification only. Inspect the actual artifact and saved verification first; cite nonempty evidence files and describe what you observed. Exact goal subject required. Manual ticks and worker reports are claims; ignored/uncommitted evidence is allowed. This records judgment, not an independent judge.";
|
||||
export const prototypeMessages = {
|
||||
noPlan: "no plan attached",
|
||||
emptyPlan: "empty plan (save may be in progress)",
|
||||
completionUnavailable: "Completion is available only to the active parent supervisor or solo worker.",
|
||||
cancelled: "Cancelled; no sign-off recorded.",
|
||||
uniqueGoal: "Use one unique exact goal subject from the plan; no sign-off recorded.",
|
||||
childAttachOnly: "AttachGoalPlan is available only to the delegated goals-worker.",
|
||||
invalidAttachment: "Supply the explicit absolute path from the parent task to a readable, nonempty goal plan; no attachment changed.",
|
||||
};
|
||||
export const goalToolBlocked = (mode: string) => `Goals are ${mode}; no worker launch/resume authorized.`;
|
||||
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 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)}`;
|
||||
}
|
||||
export function completionResult(goal: string, sessionId: string, remaining: boolean, solo: boolean): string {
|
||||
return `Recorded ${solo ? "solo self-verification" : "parent judgment"} for ${goal}; not independent verification. ${remaining ? "Continue only remaining open or unsigned goals in your current role." : `All non-cancelled goals are reviewed. ${removeGoalSchedule(sessionId)}`}`;
|
||||
}
|
||||
|
||||
// Pause/resume and solo recovery. Stored stop confirmation is invalidated on every worker launch.
|
||||
export const pausedRole = "Goal work is paused. Do not launch, resume or authorize work. Incoming reports are observations, not permission. Help inspect or stop existing workers if requested.";
|
||||
export function pauseExitNotice(worker: { id?: string; sessionFile: string } | undefined, exited: boolean): string {
|
||||
return `Goals ${exited ? "exited to ordinary chat" : "paused locally"}; plan and evidence retained. ${worker ? worker.id ? `Inspect and stop runtime id ${worker.id} through subagent_kill or its pane; confirm the actual result.` : `Only saved session ${worker.sessionFile} is recorded, not a kill id. Locate its live pane/session and confirm termination; never pass the file path to subagent_kill.` : "No worker recorded: inspect /subagents if a launch was interrupted; absence is not proof of stop."} Remote stop is NOT yet confirmed. Restore failed compaction/model/credits in the existing session and continue only after explicit authorization; never restart long work.`;
|
||||
}
|
||||
export function resumeNotice(workerName: string, planPath: string, worker: { sessionFile: string } | undefined): string {
|
||||
return `User authorized continuation of ${planPath}. Inspect worker state before any launch/resume. ${worker ? `Use the existing session ${worker.sessionFile}; if live, inspect/message it; only if confirmed stopped use subagent_resume.` : `Use '${workerName}' only after confirming no prior writer exists.`} Continue only unfinished goals; retain saved progress and scheduler edits.`;
|
||||
}
|
||||
export const soloRole = "Solo mode: implement the approved plan directly; do not delegate a concurrent writer. Verify artifacts before CompleteGoal; completion is self-verification, not independent supervisor review. Continue only unfinished goals and keep plan/evidence current.";
|
||||
export function soloNotice(planPath: string): string {
|
||||
return `User authorized solo work on ${planPath} after confirming no other writer remains. ${soloRole}`;
|
||||
}
|
||||
export function attachNotice(planPath: string, solo: boolean, notedWorker: string | undefined): string {
|
||||
return `Attached to the existing plan ${planPath}; read it and its evidence without restarting completed work or re-deriving settled decisions. ${notedWorker ? `Recorded worker session: ${notedWorker}; inspect liveness before resume.` : ""} ${solo ? soloRole : "Present /goals review or /goals ready; no implementation before approval."}`;
|
||||
}
|
||||
|
||||
+312
-70
@@ -1,54 +1,176 @@
|
||||
/** Opt-in prototype: the existing chat plans and supervises an edxeth interactive worker. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { type FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { foldPlan, GOAL_LINE } from "./plan.js";
|
||||
import { planViews } from "./plan-view.js";
|
||||
import {
|
||||
attachGoalPlanDescription,
|
||||
attachNotice,
|
||||
childPlanAttached,
|
||||
childPlanRole,
|
||||
completionLog,
|
||||
completionResult,
|
||||
emptyEvidence,
|
||||
evidenceUnavailable,
|
||||
goalToolBlocked,
|
||||
manualReview,
|
||||
pausedRole,
|
||||
pauseExitNotice,
|
||||
planChangedReview,
|
||||
planUnavailable,
|
||||
prototypeCompleteGoalDescription,
|
||||
prototypeDiscuss,
|
||||
prototypeMessages,
|
||||
prototypePlanContext,
|
||||
prototypePlanDocument,
|
||||
prototypePlanning,
|
||||
prototypePlanningSeed,
|
||||
prototypeSupervisor,
|
||||
prototypeUpkeep,
|
||||
readyApproved,
|
||||
removeGoalSchedule,
|
||||
resumeNotice,
|
||||
scheduleCheckIn,
|
||||
soloNotice,
|
||||
soloRole,
|
||||
} from "./prompts.js";
|
||||
|
||||
const STATE = "pi-goals-main-supervisor-v1";
|
||||
const WORKER = "goals-worker";
|
||||
type Mode = "chat" | "planning" | "supervising" | "paused" | "solo";
|
||||
type GoalStatus = "open" | "active" | "done" | "cancelled";
|
||||
interface State {
|
||||
mode: Mode;
|
||||
plan?: string;
|
||||
worker?: { id: string; sessionFile: string };
|
||||
worker?: { id?: string; sessionFile: string };
|
||||
workerStopped?: boolean;
|
||||
signoffs: Record<string, { evidence: string[]; observation: string }>;
|
||||
child?: boolean;
|
||||
}
|
||||
const initial = (): State => ({ mode: "chat", signoffs: {} });
|
||||
const digest = (text: string) => createHash("sha256").update(text).digest("hex");
|
||||
const key = (text: string) => text.trim().toLowerCase();
|
||||
const SUBTASK_LINE = /^\s+(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*(.*)$/;
|
||||
function goals(text: string) {
|
||||
return foldPlan(text).split("\n").flatMap((line, index) => {
|
||||
const match = GOAL_LINE.exec(line);
|
||||
return match ? [{ subject: match[2].trim(), checked: /x/i.test(match[1]), index }] : [];
|
||||
if (!match) return [];
|
||||
const box = match[1].toLowerCase();
|
||||
return [{ subject: match[2].trim(), status: (box === "x" ? "done" : box === "/" ? "active" : box === "-" ? "cancelled" : "open") as GoalStatus, index }];
|
||||
});
|
||||
}
|
||||
/** Open subtasks under the goal at goalLine, up to the next goal line; the widget shows these. */
|
||||
function openSubtasks(plan: string, goalLine: number): string[] {
|
||||
const lines = foldPlan(plan).split("\n");
|
||||
const out: string[] = [];
|
||||
for (let i = goalLine + 1; i < lines.length; i++) {
|
||||
if (GOAL_LINE.test(lines[i])) break;
|
||||
const m = SUBTASK_LINE.exec(lines[i]);
|
||||
if (m && (m[1] === " " || m[1] === "/")) out.push(m[2].trim());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} });
|
||||
|
||||
export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
let state = initial();
|
||||
let generation = 0;
|
||||
let workerRevision = 0;
|
||||
let launchPending = false;
|
||||
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);
|
||||
// Missing, empty and failed reads are unavailable snapshots, never an empty authoritative plan.
|
||||
const readPlan = () => {
|
||||
try {
|
||||
if (!state.plan) throw new Error(prototypeMessages.noPlan);
|
||||
const text = readFileSync(state.plan, "utf8");
|
||||
if (!text.trim()) throw new Error(prototypeMessages.emptyPlan);
|
||||
return { text };
|
||||
} catch (error) { return { error: planUnavailable(state.plan, error) }; }
|
||||
};
|
||||
const planText = () => {
|
||||
try { return state.plan ? readFileSync(state.plan, "utf8") : ""; } catch { return ""; }
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) throw new Error(snapshot.error);
|
||||
return snapshot.text;
|
||||
};
|
||||
let turnsStale = 0;
|
||||
let lastWorkingSet = "";
|
||||
const checkIn = (ctx: ExtensionContext) => scheduleCheckIn(ctx.sessionManager.getSessionId(), state.plan ?? "");
|
||||
const hasScheduleTool = () => pi.getAllTools().some((tool) => tool.name === "schedule_prompt");
|
||||
const notedPlanValue = (prefix: string) => {
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) return null;
|
||||
const m = new RegExp(`^\\-\\s*${prefix}:\\s*(.+)$`, "im").exec(foldPlan(snapshot.text));
|
||||
return m?.[1]?.trim() ?? null;
|
||||
};
|
||||
function refresh(ctx: ExtensionContext) {
|
||||
const items = goals(planText());
|
||||
if (state.mode === "chat") { ctx.ui.setStatus("goals", undefined); ctx.ui.setWidget("goals", undefined); return; }
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) {
|
||||
ctx.ui.setStatus("goals", snapshot.error);
|
||||
ctx.ui.setWidget("goals", [snapshot.error]);
|
||||
return;
|
||||
}
|
||||
const items = goals(snapshot.text);
|
||||
// Reopened/deleted/ambiguous goal identities lose their sign-off. Manual ticks remain claims.
|
||||
for (const subject of Object.keys(state.signoffs)) {
|
||||
const matches = items.filter((g) => key(g.subject) === subject);
|
||||
if (matches.length !== 1 || !matches[0].checked) { delete state.signoffs[subject]; save(); }
|
||||
if (matches.length !== 1 || matches[0].status !== "done") { delete state.signoffs[subject]; save(); }
|
||||
}
|
||||
const accepted = items.filter((g) => g.checked && state.signoffs[key(g.subject)]).length;
|
||||
ctx.ui.setStatus("goals", state.mode === "chat" ? undefined : `goals: ${state.child ? "worker" : state.mode} | ${accepted}/${items.length} reviewed`);
|
||||
ctx.ui.setWidget("goals", state.mode === "chat" ? undefined : [
|
||||
...items.map((g) => `${state.signoffs[key(g.subject)] && g.checked ? "✓" : g.checked ? "?" : "○"} ${g.subject}`),
|
||||
...(items.some((g) => g.checked && !state.signoffs[key(g.subject)]) ? ["? = completion claim; parent review still required"] : []),
|
||||
]);
|
||||
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`);
|
||||
// Progress and subtask visibility: the widget is the task list, so the active goal shows its
|
||||
// next open subtasks without reading archived checkboxes below the fold.
|
||||
const plan = snapshot.text;
|
||||
const focus = items.find((g) => g.status === "active") ?? items.find((g) => g.status === "open" && openSubtasks(plan, g.index).length > 0);
|
||||
const mark = (status: GoalStatus, signed: boolean) => status === "done" ? (signed ? "✓" : "?") : status === "active" ? "▸" : status === "cancelled" ? "✗" : "○";
|
||||
const lines: string[] = items.map((g) => `${mark(g.status, Boolean(state.signoffs[key(g.subject)]))} ${g.subject}`);
|
||||
if (focus) {
|
||||
const muted = (s: string) => ctx.ui.theme.fg("muted", ` ◦ ${s}`);
|
||||
lines.push(...openSubtasks(plan, focus.index).slice(0, 3).map(muted));
|
||||
}
|
||||
if (items.some((g) => g.status === "done" && !state.signoffs[key(g.subject)])) lines.push("? = completion claim; parent review still required");
|
||||
ctx.ui.setWidget("goals", lines);
|
||||
}
|
||||
function watchPlan(ctx: ExtensionContext) {
|
||||
planWatcher?.close();
|
||||
planWatcher = undefined;
|
||||
clearTimeout(planEditTimer);
|
||||
planEditTimer = undefined;
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text !== undefined) planHash = digest(planViews(snapshot.text).short);
|
||||
if (state.child || state.mode !== "supervising" || !state.plan) return;
|
||||
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.
|
||||
try {
|
||||
planWatcher = watch(dirname(state.plan), { persistent: false }, () => {
|
||||
if (stamp !== generation) return;
|
||||
if (planEditTimer) clearTimeout(planEditTimer);
|
||||
planEditTimer = setTimeout(() => {
|
||||
planEditTimer = undefined;
|
||||
if (stamp !== generation || state.mode !== "supervising") return;
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) { ctx.ui.notify(snapshot.error!, "warning"); return; }
|
||||
refresh(ctx);
|
||||
const hash = digest(planViews(snapshot.text).short);
|
||||
if (hash === planHash) return;
|
||||
planHash = hash;
|
||||
notice = true;
|
||||
send(planChangedReview(state.plan!));
|
||||
}, 150);
|
||||
});
|
||||
planWatcher.on("error", (error) => { planWatcher?.close(); planWatcher = undefined; ctx.ui.notify(`Plan monitoring failed: ${error.message}`, "error"); });
|
||||
} catch (error) { ctx.ui.notify(`Plan monitoring unavailable: ${String(error)}`, "error"); }
|
||||
}
|
||||
function restore(ctx: ExtensionContext) {
|
||||
generation++;
|
||||
@@ -59,11 +181,14 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (childEnvironment) {
|
||||
state.child = true;
|
||||
state.mode = "solo";
|
||||
state.plan = process.env.PI_GOALS_SHARED_PLAN || state.plan;
|
||||
// Lineage-only workers attach the explicit task path using AttachGoalPlan.
|
||||
save();
|
||||
}
|
||||
notice = true;
|
||||
turnsStale = 0;
|
||||
lastWorkingSet = "";
|
||||
refresh(ctx);
|
||||
watchPlan(ctx);
|
||||
}
|
||||
function compatible() {
|
||||
const tools = pi.getAllTools();
|
||||
@@ -76,7 +201,23 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (triggerTurn) pi.sendUserMessage(`[pi-goals]\n${content}`, { deliverAs: "followUp" });
|
||||
else pi.sendMessage({ customType: "pi-goals-supervision", content, display: true }, { deliverAs: "followUp", triggerTurn: false });
|
||||
}
|
||||
const help = "/goals <objective> | review | ready | status | stop | resume | solo | exit\n/subagents opens edxeth's worker UI. Stop/exit pause this plan locally; worker termination must be confirmed through subagent_kill or its pane. No forced compaction or model switch.";
|
||||
async function confirmOwnership(ctx: ExtensionContext, target: string, text: string, solo = true): Promise<boolean> {
|
||||
if (launchPending) { ctx.ui.notify("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";
|
||||
const choice = await ctx.ui.select(solo ? "Confirm all other writers for the current and target plans are stopped (inspect /subagents and their panes). A missing handle is not proof. Take over in this session?" : "Confirm no other supervisor owns this plan. Preserve any existing worker session and reconnect rather than starting another writer.", [confirmation, "Cancel"]);
|
||||
if (stamp !== generation || revision !== workerRevision) return false;
|
||||
if (choice !== confirmation) return false;
|
||||
if (readFileSync(target, "utf8") !== text) { ctx.ui.notify("Plan changed during takeover; confirm again.", "warning"); return false; }
|
||||
return true;
|
||||
}
|
||||
function enterSolo(ctx: ExtensionContext) {
|
||||
state.mode = "solo"; state.workerStopped = true;
|
||||
generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
send(`${removeGoalSchedule(ctx.sessionManager.getSessionId())}\n\n${soloNotice(state.plan!)}`);
|
||||
}
|
||||
const help = "/goals <objective> | review | ready | status | stop | resume | solo | exit | attach <plan.md> [solo] | model <model>\n/subagents opens edxeth's worker UI. Stop/exit pause this plan locally; worker termination must be confirmed through subagent_kill or its pane. 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) {
|
||||
if (state.mode !== "planning") { ctx.ui.notify("Ready applies to a draft; use status or resume.", "warning"); return; }
|
||||
const text = planText();
|
||||
@@ -88,7 +229,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (menu) {
|
||||
const choice = 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 the current draft in ordinary chat. Do not launch a worker or reopen the review menu until requested."); return; }
|
||||
if (choice === "Discuss") { send(prototypeDiscuss); 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); }
|
||||
@@ -97,90 +238,191 @@ 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);
|
||||
send(`Ready approved this plan: ${state.plan}. Stay in this chat as supervisor. Delegate the first open goal to agent '${WORKER}' with subagent; provide name, title and task. Include the absolute plan path in the task. Do not start a second writer. Inspect actual outputs when the child reports.\n\n${text}`);
|
||||
state.mode = "supervising"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
send(`${checkIn(ctx)}\n\n${readyApproved(WORKER, state.plan!, state.worker?.sessionFile, text, ctx.sessionManager.getSessionId())}`);
|
||||
}
|
||||
|
||||
pi.on("session_start", (_e, ctx) => restore(ctx));
|
||||
pi.on("session_tree", (_e, ctx) => restore(ctx));
|
||||
pi.on("session_shutdown", () => { generation++; planWatcher?.close(); planWatcher = undefined; clearTimeout(planEditTimer); planEditTimer = undefined; });
|
||||
pi.on("session_compact", () => { notice = true; });
|
||||
pi.on("agent_end", (_e, ctx) => refresh(ctx));
|
||||
pi.on("turn_end", (_event, ctx) => {
|
||||
if (!["supervising", "solo"].includes(state.mode)) return;
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) { notice = true; return; }
|
||||
const workingSet = foldPlan(snapshot.text);
|
||||
turnsStale = workingSet === lastWorkingSet ? turnsStale + 1 : 0;
|
||||
lastWorkingSet = workingSet;
|
||||
refresh(ctx);
|
||||
if (turnsStale >= 8 && goals(snapshot.text).some(g => g.status === "open" || g.status === "active")) {
|
||||
// Pi queues context-only messages until tool results are appended at turn_end.
|
||||
// This reaches the next model call in a long run without triggering another run.
|
||||
pi.sendMessage({ customType: "pi-goals-upkeep", content: prototypeUpkeep(state.plan!, workingSet), display: false }, { triggerTurn: false });
|
||||
turnsStale = 0;
|
||||
}
|
||||
});
|
||||
pi.on("agent_end", (_e, ctx) => { refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); });
|
||||
// No context hook. Historical message arrays, native checkpoints and model selection are untouched.
|
||||
pi.on("before_agent_start", (event) => {
|
||||
if (state.child || state.mode === "chat") return;
|
||||
const role = state.mode === "supervising"
|
||||
? `You are the goal supervisor in the main chat. Inspect files, evidence and applicable AGENTS/skills yourself; delegate implementation to '${WORKER}'. Investigate blocked/waiting/done claims rather than accepting them. Change ineffective instructions. Keep authorized work moving until the requested result is inspected, not merely until approval paperwork exists. Use edxeth subagent for first launch and subagent_resume with the returned sessionFile for follow-up; never duplicate a live writer. Worker reports arrive automatically. A recap alone sends no instruction. When a worker finishes a goal, inspect the actual artifact and saved verification, call CompleteGoal, then resume it for the next open goal. Retain brief visible assessments. Do not enable edxeth's tool-restricted orchestrator mode.`
|
||||
: state.mode === "planning"
|
||||
? `Plan only. Ask material unresolved questions, not a quota. Draft ${state.plan} with objective, preferences, distinct '- [ ] goal: ...' subjects, failure modes and evidence/verification for each. Keep goal lines and evidence references above ## Log. Do not implement, launch workers or treat your own draft as approved. Present it for /goals review or /goals ready.`
|
||||
: state.mode === "paused"
|
||||
? "Goal work is paused. Do not launch, resume or authorize work. Incoming worker reports are observations, not permission to continue. Help inspect or stop existing workers if requested."
|
||||
: "Solo mode: this chat may implement the approved plan directly. Do not delegate a concurrent writer. Verify artifacts before CompleteGoal.";
|
||||
const message = notice ? { customType: "pi-goals-plan", content: `Current goal mode: ${state.mode}. Earlier role messages are historical; this current role governs.\nPlan: ${state.plan}\n${planText()}`, display: false } : undefined;
|
||||
pi.on("before_agent_start", (event, ctx) => {
|
||||
if (state.mode === "chat") return;
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) {
|
||||
notice = true; // Retry resync on the next turn; do not consume a failed snapshot.
|
||||
return { systemPrompt: `${event.systemPrompt}\n\n${state.child ? childPlanRole : ""}\n${snapshot.error}` };
|
||||
}
|
||||
const role = state.child ? childPlanRole : state.mode === "supervising"
|
||||
? prototypeSupervisor(WORKER, state.plan!, ctx.sessionManager.getSessionId())
|
||||
: state.mode === "planning" ? prototypePlanning(state.plan!) : state.mode === "paused" ? pausedRole : soloRole;
|
||||
const content = notice ? prototypePlanContext(state.child ? "worker" : state.mode, state.plan, snapshot.text)
|
||||
: undefined;
|
||||
if (content) turnsStale = 0;
|
||||
notice = false;
|
||||
return { systemPrompt: `${event.systemPrompt}\n\n${role}`, ...(message ? { message } : {}) };
|
||||
return { systemPrompt: `${event.systemPrompt}\n\n${role}`, ...(content ? { message: { customType: "pi-goals-plan", content, display: false } } : {}) };
|
||||
});
|
||||
pi.on("tool_call", (event) => {
|
||||
if (state.child || !["subagent", "subagent_resume"].includes(event.toolName)) return;
|
||||
if (state.mode === "planning" || state.mode === "paused") return { block: true, reason: `Goals are ${state.mode}; no worker launch/resume authorized.` };
|
||||
// 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) { launchPending = true; state.workerStopped = false; workerRevision++; save(); }
|
||||
});
|
||||
pi.on("tool_result", (event) => {
|
||||
if (state.child || state.mode !== "supervising" || !["subagent", "subagent_resume"].includes(event.toolName) || event.isError) return;
|
||||
if (state.child || !state.plan || !["subagent", "subagent_resume"].includes(event.toolName)) return;
|
||||
launchPending = false;
|
||||
if (event.isError) return;
|
||||
const details = event.details as { id?: string; sessionFile?: string } | undefined;
|
||||
if (details?.id && details.sessionFile) { state.worker = { id: details.id, sessionFile: details.sessionFile }; save(); }
|
||||
if (details?.id && details.sessionFile) { state.worker = { id: details.id, sessionFile: details.sessionFile }; state.workerStopped = false; workerRevision++; save(); }
|
||||
});
|
||||
|
||||
pi.registerCommand("goals", {
|
||||
description: "Prototype: plan here, supervise a visible edxeth worker",
|
||||
handler: async (args, ctx) => {
|
||||
if (state.child) { ctx.ui.notify("This is the delegated worker. Goal approval belongs to its parent.", "info"); return; }
|
||||
const command = args.trim();
|
||||
if (!command || command === "help") { ctx.ui.notify(help, "info"); return; }
|
||||
if (command === "status") { refresh(ctx); ctx.ui.notify(`${state.mode}\nPlan: ${state.plan ?? "none"}\nWorker session: ${state.worker?.sessionFile ?? "not recorded"}\nLiveness is owned by edxeth; inspect /subagents.`, "info"); return; }
|
||||
if (command === "review" || command === "ready") { await ready(ctx, command === "review"); return; }
|
||||
if (command === "stop" || command === "exit") {
|
||||
state.mode = command === "stop" ? "paused" : "chat"; generation++; notice = true; save(); refresh(ctx);
|
||||
send(`Goals ${command === "stop" ? "paused" : "exited to ordinary chat"} locally; plan and evidence retained. ${state.worker ? `Use subagent_kill with id ${JSON.stringify(state.worker.id)} to stop the tracked worker, and confirm the result. Do not resume it.` : "No worker recorded. Inspect /subagents if a launch was interrupted."} Remote stop is NOT yet confirmed.`, Boolean(state.worker)); return;
|
||||
}
|
||||
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 (!compatible()) { ctx.ui.notify("edxeth tools unavailable; plan remains paused.", "error"); return; }
|
||||
state.mode = "supervising"; generation++; notice = true; save(); refresh(ctx);
|
||||
send(`User authorized continuation. Inspect worker state in /subagents before any launch. ${state.worker ? `Continue the existing session with subagent_resume: ${state.worker.sessionFile}.` : `Use subagent with '${WORKER}' only if no prior worker exists.`} Continue only unfinished goals in ${state.plan}.`); return;
|
||||
}
|
||||
if (command === "solo") {
|
||||
if (!state.plan || !goals(planText()).length) { ctx.ui.notify("Register a goal plan first.", "warning"); return; }
|
||||
if (state.worker) { ctx.ui.notify("Stop and inspect the existing worker first. Solo takeover is deliberately not automatic; use /goals exit and ordinary chat after confirming it stopped.", "warning"); return; }
|
||||
state.mode = "solo"; generation++; notice = true; save(); refresh(ctx); send(`User explicitly authorized solo work on ${state.plan}. No worker has been recorded; inspect /subagents if a launch previously failed.`); return;
|
||||
}
|
||||
if (state.worker || state.mode === "supervising") { ctx.ui.notify("Exit and resolve the existing worker before replacing the plan. The current plan is preserved.", "warning"); return; }
|
||||
const path = join(ctx.cwd, ".pi", "plan", `${ctx.sessionManager.getSessionId()}-main.md`);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
// Never overwrite an earlier plan at this session path; the model can revise it after inspection.
|
||||
try { readFileSync(path); } catch { writeFileSync(path, `# Goal plan\n\n## Objective\n${command}\n\n## Goals\n\n## Log\n`); }
|
||||
state = { mode: "planning", plan: path, signoffs: {} }; generation++; notice = true; save(); refresh(ctx);
|
||||
send(`Draft or revise ${path} for this objective: ${command}. Read any existing plan first. Ask only material questions; no implementation before Ready.`);
|
||||
try {
|
||||
if (state.child) { ctx.ui.notify("This is the delegated worker. Goal approval belongs to its parent.", "info"); return; }
|
||||
const command = args.trim();
|
||||
if (!command || command === "help") { ctx.ui.notify(help, "info"); return; }
|
||||
if (command === "status") {
|
||||
refresh(ctx);
|
||||
ctx.ui.notify([
|
||||
`Mode: ${state.mode}`,
|
||||
`Plan: ${state.plan ?? "none"}`,
|
||||
`Preferred worker model (plan): ${notedPlanValue("preferred worker model") ?? "not stated; use /goals model <model>"}`,
|
||||
`Recorded worker session: ${state.worker?.sessionFile ?? "not recorded"}`,
|
||||
notedPlanValue("worker session") ? `Worker session noted in plan: ${notedPlanValue("worker session")}` : "",
|
||||
`Hourly check-in: schedule_prompt job ${JSON.stringify(`goals-${ctx.sessionManager.getSessionId()}`)} (list/remove via schedule_prompt; plan-change reviews are the plan-watcher event hook)`,
|
||||
"Liveness is owned by edxeth; inspect /subagents.",
|
||||
].filter(Boolean).join("\n"), "info");
|
||||
return;
|
||||
}
|
||||
if (command === "review" && state.mode === "supervising") { notice = true; send(manualReview(state.plan ?? "")); return; }
|
||||
if (command === "review" || command === "ready") { await ready(ctx, command === "review"); return; }
|
||||
if (command === "model" || command.startsWith("model ")) {
|
||||
if (!state.plan || !goals(planText()).length) { ctx.ui.notify("Register a goal plan first.", "warning"); return; }
|
||||
const ref = command.slice("model".length).trim();
|
||||
const lines = planText().split("\n");
|
||||
const pref = `- preferred worker model: ${ref || "(none specified)"}`;
|
||||
const found = lines.findIndex((line) => /^-\s*preferred worker model:/i.test(line));
|
||||
if (found >= 0) lines[found] = pref;
|
||||
else { const title = lines.findIndex((line) => /^#\s/.test(line)); lines.splice(title >= 0 ? title + 1 : 0, 0, pref); }
|
||||
writeFileSync(state.plan, lines.join("\n"));
|
||||
planHash = digest(planViews(planText()).short);
|
||||
refresh(ctx);
|
||||
ctx.ui.notify(ref ? `Preferred worker model set to ${ref} in plan preferences. The supervisor selects it at launch and verifies the resolved model; the worker pane's own model is chosen with /model in that pane.` : "Preferred worker model cleared.", "info");
|
||||
return;
|
||||
}
|
||||
if (command === "attach" || command.startsWith("attach ")) {
|
||||
const rest = command.slice("attach".length).trim();
|
||||
const [raw, kind] = rest.split(/\s+/);
|
||||
const solo = kind === "solo";
|
||||
if (kind && !solo) { ctx.ui.notify("Use /goals attach <path-to-plan.md> [solo].", "warning"); return; }
|
||||
if (!raw) { ctx.ui.notify("Use /goals attach <path-to-plan.md> [solo].", "info"); return; }
|
||||
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 (!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;
|
||||
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, workerStopped: solo || (!noted && state.workerStopped) };
|
||||
generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
if (solo) enterSolo(ctx);
|
||||
else send(attachNotice(target, false, noted));
|
||||
return;
|
||||
}
|
||||
if (command === "stop" || command === "exit") {
|
||||
if (state.mode === "planning") {
|
||||
if (command === "stop") { ctx.ui.notify("A draft cannot pause; use /goals exit to leave planning with the draft preserved.", "warning"); return; }
|
||||
// Planning exit must not get the model trapped re-planning or lose the draft.
|
||||
state.mode = "chat"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
ctx.ui.notify(`Planning exited; draft preserved at ${state.plan}. No implementation was approved or started. Reconnect with /goals attach ${state.plan}.`, "info");
|
||||
return;
|
||||
}
|
||||
state.mode = command === "stop" ? "paused" : "chat"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
send(`${removeGoalSchedule(ctx.sessionManager.getSessionId())}\n\n${pauseExitNotice(state.worker, command === "exit")}`, Boolean(state.worker) || hasScheduleTool());
|
||||
return;
|
||||
}
|
||||
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 (!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 (!(await confirmOwnership(ctx, state.plan, planText()))) return;
|
||||
enterSolo(ctx);
|
||||
return;
|
||||
}
|
||||
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 path = join(ctx.cwd, ".pi", "plan", `${ctx.sessionManager.getSessionId()}-main.md`);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
// Never overwrite an earlier plan at this session path; the model can revise it after inspection.
|
||||
try { writeFileSync(path, prototypePlanDocument(command), { flag: "wx" }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
|
||||
state = { mode: "planning", plan: path, signoffs: {}, worker: state.worker, workerStopped: state.workerStopped }; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
send(prototypePlanningSeed(command, path));
|
||||
} catch (error) { ctx.ui.notify(String(error), "error"); }
|
||||
},
|
||||
});
|
||||
pi.registerTool({
|
||||
name: "AttachGoalPlan", label: "Attach delegated plan", description: attachGoalPlanDescription,
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
async execute(_id, params, _signal, _update, ctx) {
|
||||
if (!state.child) return result(prototypeMessages.childAttachOnly);
|
||||
try {
|
||||
if (!isAbsolute(params.path) || !goals(readFileSync(params.path, "utf8")).length) return result(prototypeMessages.invalidAttachment);
|
||||
} catch { return result(prototypeMessages.invalidAttachment); }
|
||||
state.plan = params.path; generation++; notice = true; save(); refresh(ctx);
|
||||
return result(childPlanAttached(params.path));
|
||||
},
|
||||
});
|
||||
pi.registerTool({
|
||||
name: "CompleteGoal", label: "Review goal evidence",
|
||||
description: "Parent-only recorded judgment, not an independent judge. Inspect the actual artifact and saved verification first; cite nonempty evidence files and describe what you observed. Exact goal subject required. Ignored/uncommitted evidence is allowed. Manual ticks are claims. Do not use this merely because the worker reports success.",
|
||||
description: prototypeCompleteGoalDescription,
|
||||
parameters: Type.Object({ goal: Type.String(), 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("Completion is available only to the active parent supervisor or solo worker.");
|
||||
if (signal?.aborted) return result("Cancelled; no sign-off recorded.");
|
||||
const text = planText();
|
||||
const matches = goals(text).filter((g) => key(g.subject) === key(params.goal));
|
||||
if (matches.length !== 1 || !state.plan) return result("Use one unique exact goal subject from the plan; no sign-off recorded.");
|
||||
if (state.child || !["supervising", "solo"].includes(state.mode)) return result(prototypeMessages.completionUnavailable);
|
||||
if (signal?.aborted) return result(prototypeMessages.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(prototypeMessages.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(`Empty evidence: ${file}`); }
|
||||
catch (error) { return result(`Evidence unavailable: ${String(error)}. No sign-off recorded.`); }
|
||||
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]");
|
||||
if (!/^##\s+Log\s*$/im.test(text)) lines.push("\n## Log");
|
||||
writeFileSync(state.plan, `${lines.join("\n").trimEnd()}\n\n- Parent review: ${JSON.stringify(params.goal)}; ${JSON.stringify(params.observation)}; evidence ${JSON.stringify(evidence)}\n`);
|
||||
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()).short);
|
||||
save(); refresh(ctx);
|
||||
return result(`Recorded parent judgment for ${matches[0].subject}. This is not independent verification. Continue supervising any remaining open or unsigned goals.`);
|
||||
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"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user