mirror of
https://github.com/wassname/pi-plan.git
synced 2026-10-02 12:30:49 +08:00
Restore automatic plan proposals and make goal commands explicit
Reduce unchanged upkeep and identity-only review noise; retain user README structure and screenshot with abridged terminal example. 174 tests pass; actual Herdr automatic proposal captured. Co-Authored-By: Pi/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
1 parent
cabb4446aa
commit
c175096ddb
13 files changed
+358
-131
No files matched your search
@@ -5,6 +5,8 @@ export function planViews(plan: string): { short: string; long: 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;
|
||||
|
||||
+6
-6
@@ -284,22 +284,22 @@ export const completeGoalParamDescription = "The goal's text: the line after 'go
|
||||
|
||||
// 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.`;
|
||||
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. 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 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}`;
|
||||
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. ${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 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. 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}`;
|
||||
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}`;
|
||||
}
|
||||
|
||||
// Supervision and turn-event upkeep (not a scheduled wake-up).
|
||||
@@ -308,8 +308,8 @@ export function prototypeSupervisor(workerName: string, planPath: string, superv
|
||||
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 prototypeUpkeep(planPath: string): string {
|
||||
return `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 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}`;
|
||||
|
||||
+40
-11
@@ -217,7 +217,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
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.";
|
||||
const help = "/goals new [initial idea] | review | ready | status | stop | resume | solo | exit | attach <plan.md> [solo] | model <model>\n/subagents opens the worker controls. 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();
|
||||
@@ -254,14 +254,27 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
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 && 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.sendMessage({ customType: "pi-goals-upkeep", content: prototypeUpkeep(state.plan!), display: false }, { triggerTurn: false });
|
||||
}
|
||||
});
|
||||
pi.on("agent_end", (_e, ctx) => { refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); });
|
||||
let proposedDraft = "";
|
||||
let proposing = false;
|
||||
pi.on("agent_settled", async (_e, ctx) => {
|
||||
if (state.child || state.mode !== "planning" || !ctx.hasUI || proposing) return;
|
||||
const text = planText();
|
||||
const version = `${state.plan}:${digest(text)}`;
|
||||
if (!goals(text).length || version === proposedDraft) return;
|
||||
proposedDraft = version;
|
||||
proposing = true;
|
||||
try {
|
||||
pi.sendMessage({ customType: "goal-plan-proposal", content: text, display: true }, { triggerTurn: false });
|
||||
await ready(ctx, true);
|
||||
} finally { proposing = false; }
|
||||
});
|
||||
// No context hook. Historical message arrays, native checkpoints and model selection are untouched.
|
||||
pi.on("before_agent_start", (event, ctx) => {
|
||||
if (state.mode === "chat") return;
|
||||
@@ -294,12 +307,25 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
});
|
||||
|
||||
pi.registerCommand("goals", {
|
||||
description: "Prototype: plan here, supervise a visible edxeth worker",
|
||||
description: "Goal plan actions: new, review, ready, status, stop, resume, solo, attach, model, exit",
|
||||
getArgumentCompletions: (prefix) => ["new", "review", "ready", "status", "stop", "resume", "solo", "attach", "model", "exit", "help"].filter((verb) => verb.startsWith(prefix)).map((verb) => ({ value: verb, label: verb })),
|
||||
handler: async (args, ctx) => {
|
||||
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; }
|
||||
let command = args.trim();
|
||||
if (!command) {
|
||||
const actions = ["status — Show current plan", "new — New plan", "attach — Open an existing plan", "review — Review current plan", "ready — Approve draft", "stop — Pause work", "resume — Continue paused work", "solo — Work in this session", "model — Set worker model", "exit — Leave goal mode", "help — Show commands"];
|
||||
const before = generation;
|
||||
const choice = await ctx.ui.select("Goal plan actions", actions);
|
||||
if (!choice || before !== generation) return;
|
||||
command = choice.split(" — ")[0];
|
||||
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;
|
||||
command += ` ${value.trim()}`;
|
||||
}
|
||||
}
|
||||
if (command === "help") { ctx.ui.notify(help, "info"); return; }
|
||||
if (command === "status") {
|
||||
refresh(ctx);
|
||||
ctx.ui.notify([
|
||||
@@ -318,6 +344,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
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();
|
||||
if (!ref) { ctx.ui.notify("Use /goals model <provider/model>; no preference changed.", "info"); return; }
|
||||
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));
|
||||
@@ -331,9 +358,9 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
}
|
||||
if (command === "attach" || command.startsWith("attach ")) {
|
||||
const rest = command.slice("attach".length).trim();
|
||||
const [raw, kind] = rest.split(/\s+/);
|
||||
const [raw, kind, extra] = rest.split(/\s+/);
|
||||
const solo = kind === "solo";
|
||||
if (kind && !solo) { ctx.ui.notify("Use /goals attach <path-to-plan.md> [solo].", "warning"); return; }
|
||||
if (extra || (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;
|
||||
@@ -375,13 +402,15 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
enterSolo(ctx);
|
||||
return;
|
||||
}
|
||||
if (command !== "new" && !command.startsWith("new ")) { ctx.ui.notify(`Unknown or incomplete command. ${help}`, "warning"); return; }
|
||||
const objective = command.slice(4).trim();
|
||||
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; }
|
||||
try { writeFileSync(path, prototypePlanDocument(objective), { 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));
|
||||
send(prototypePlanningSeed(objective, path));
|
||||
} catch (error) { ctx.ui.notify(String(error), "error"); }
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user