From 55507d31701344fc978495fb1941c718568183cf Mon Sep 17 00:00:00 2001 From: wassname2 Date: Tue, 15 Sep 2026 16:14:43 +0800 Subject: [PATCH 1/6] Keep restored worker pane identity in status and pause guidance --- src/index.ts | 6 ++++-- src/prompts.ts | 4 ++-- test/goals.test.ts | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index e7b45d5..b4d9c7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -421,8 +421,10 @@ export default function mainSupervisor(pi: ExtensionAPI) { sendAttachment(state.plan, event.fromSessionId, nativeMessages.attached(data.sessionFile)); } if (data.type === "stopped" && event.fromSessionId === worker.intercomId && typeof data.text === "string") { + const id = `${event.fromSessionId}:${data.entryId || digest(data.text)}`; + if (records(ctx, REPORT).some(report => report.id === id)) return; if (data.identity) { worker.identity = data.identity; worker.sessionFile = data.identity.sessionFile; save(); } - const report: Report = { id: `${event.fromSessionId}:${data.entryId || digest(data.text)}`, plan: state.plan, session: event.fromSessionId, sessionFile: worker.sessionFile!, requestId: data.requestId, text: data.text }; + const report: Report = { id, plan: state.plan, session: event.fromSessionId, sessionFile: worker.sessionFile!, requestId: data.requestId, text: data.text }; recordReport(ctx, report); } }, @@ -649,7 +651,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { `Last observed worker model: ${state.worker?.identity?.model ?? "unconfirmed"}; verify current choice before claiming configuration.`, `Pending report reviews: ${pendingReports(ctx).map(report => report.id).join(", ") || "none"}`, `Recorded worker session: ${state.worker?.sessionFile ?? "not recorded"}`, - `Worker Intercom: ${state.worker?.intercomId ?? "unconfirmed"}; native pane: ${state.worker?.paneId ?? "unconfirmed"}`, + `Worker Intercom: ${state.worker?.intercomId ?? "unconfirmed"}; native pane: ${state.worker?.identity?.paneId || state.worker?.paneId || "unconfirmed"}`, notedPlanValue("worker session") ? `Worker session noted in plan: ${notedPlanValue("worker session")}` : "", `Check-in: session-scoped pi-scheduler task ${JSON.stringify(`goals-${ctx.sessionManager.getSessionId()}`)} (default 1h; /schedules all shows current recurrence; manage_scheduled_task updates it)`, "Inspect the exact Intercom session and native pane; a binding or idle status is not completion.", diff --git a/src/prompts.ts b/src/prompts.ts index 68c3c8a..f8fbda0 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -253,8 +253,8 @@ export function completionResult(goal: string, sessionId: string, remaining: boo // 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: { intercomId?: string; sessionFile?: string; paneId?: string } | undefined, exited: boolean): string { - return `Goals ${exited ? "exited to ordinary chat" : "paused locally"}; plan and evidence retained. ${worker ? `Locate the recorded native pane ${worker.paneId ?? "unknown"}, Intercom session ${worker.intercomId ?? "unknown"}, saved session ${worker.sessionFile ?? "unknown"}. Send an explicit pause there; inspect and confirm actual stop without closing the review conversation.` : "No worker recorded: inspect Intercom and native panes; absence is not proof of stop."} Remote stop is NOT yet confirmed. Resume only after explicit authorization.`; +export function pauseExitNotice(worker: { intercomId?: string; sessionFile?: string; paneId?: string; identity?: { paneId?: string } } | undefined, exited: boolean): string { + return `Goals ${exited ? "exited to ordinary chat" : "paused locally"}; plan and evidence retained. ${worker ? `Locate the recorded native pane ${worker.identity?.paneId || worker.paneId || "unknown"}, Intercom session ${worker.intercomId ?? "unknown"}, saved session ${worker.sessionFile ?? "unknown"}. Send an explicit pause there; inspect and confirm actual stop without closing the review conversation.` : "No worker recorded: inspect Intercom and native panes; absence is not proof of stop."} Remote stop is NOT yet confirmed. Resume only after explicit authorization.`; } export function resumeNotice(workerName: string, planPath: string, worker: { sessionFile?: string; intercomId?: string } | undefined): string { return `User authorized continuation of ${planPath}. Inspect worker state before any launch. ${worker ? `Use the existing session ${worker.sessionFile ?? "unknown"} and exact Intercom UUID ${worker.intercomId ?? "unknown"}; if live, inspect/message it. Do not open a replacement. If stopped, inspect saved history and partial work, preserve drafts and use stock pane controls only after confirming safe stop. Continue only remaining work in a newly authorized context; never replay the completed assignment. Preserve later human model choices.` : `Use OpenGoalWorker for '${workerName}' only after confirming no prior writer exists.`} Continue only unfinished goals; retain saved progress and scheduler edits.`; diff --git a/test/goals.test.ts b/test/goals.test.ts index b5b52ea..976bcf5 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -1237,7 +1237,14 @@ it("reviews a saved worker revision through inspection, silent delivery, retry a await parent.command("status"); expect(parent.ctx.ui.notify.mock.lastCall?.[0]).toContain("Pending report reviews: none"); worker.hooks.get("session_start")({}, worker.ctx); + const staleStop = worker.channel.publish.mock.lastCall?.[0]; + vi.stubEnv("HERDR_PANE_ID", "restored-pane"); + await worker.tools.get("AttachGoalPlan").execute("restore", { path: parent.path }, undefined, undefined, worker.ctx); + parent.event({ type: "message", fromSessionId: workerId, payload: staleStop }); + await parent.command("status"); + expect(parent.ctx.ui.notify.mock.lastCall?.[0]).toContain("native pane: restored-pane"); await parent.command("stop"); + expect(parent.messages.at(-1)?.message.content).toContain("native pane restored-pane"); form.report = report("Revision failed", "error"); const paused = parent.messages.length; await parent.hooks.get("agent_settled")({}, parent.ctx); From 08e8af604fda7922d02b22d7d32e7f438729fc2c Mon Sep 17 00:00:00 2001 From: wassname2 Date: Tue, 15 Sep 2026 16:25:36 +0800 Subject: [PATCH 2/6] Reduce repeated worker notices and shrinking-backlog reminders --- src/index.ts | 13 +++++++------ src/notice-display.ts | 2 +- src/prompts.ts | 6 +++--- test/goals.test.ts | 13 ++++++++++++- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index b4d9c7d..7a1030c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -357,18 +357,19 @@ export default function mainSupervisor(pi: ExtensionAPI) { } const records = (ctx: ExtensionContext, type: string): T[] => ctx.sessionManager.getBranch().flatMap(entry => entry.type === "custom" && entry.customType === type ? [entry.data as T] : []); const pendingReports = (ctx: ExtensionContext) => records(ctx, REPORT).filter(report => !records(ctx, REVIEW).some(review => review.report === report.id) && !records(ctx, REPORT).some(newer => newer.session === report.session && newer.supersedes === report.id)); - function recordReport(ctx: ExtensionContext, report: Report, wake = true) { + function recordReport(ctx: ExtensionContext, report: Report, wake = true, show = true) { if (records(ctx, REPORT).some(saved => saved.id === report.id)) return; pi.appendEntry(REPORT, report); - send(workerReview(report.plan, report.session, `${report.id}\n${report.text}`), false); + if (show) send(workerReview(report.id, report.text), false); if (wake && ctx.isIdle()) remindReports(ctx); } function remindReports(ctx: ExtensionContext) { if (state.child || state.mode !== "supervising") return; const ids = pendingReports(ctx).map(report => report.id); - const fingerprint = digest(JSON.stringify(ids)); - if (!ids.length || records(ctx, REVIEW_REMINDER).at(-1) === fingerprint) return; - pi.appendEntry(REVIEW_REMINDER, fingerprint); + const branch = ctx.sessionManager.getBranch(); + const sinceReminder = branch.slice(branch.map(entry => entry.type === "custom" ? entry.customType : "").lastIndexOf(REVIEW_REMINDER) + 1); + if (!sinceReminder.some(entry => entry.type === "custom" && entry.customType === REPORT && ids.includes((entry.data as Report).id))) return; + pi.appendEntry(REVIEW_REMINDER); send(pendingReportReviews(ids)); } pi.registerEntryRenderer(REVIEW, entry => new Text((entry.data as ReportReview).content, 0, 0)); @@ -458,7 +459,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { if (retry && records(ctx, REPORT).some(report => report.id === retry && report.text === message.content!.text)) { aliases.set(id, retry); continue; } aliases.set(id, id); const supersedes = message.supersedes ? aliases.get(`${sender}:${message.supersedes}`) || `${sender}:${message.supersedes}` : undefined; - recordReport(ctx, { id, session: sender, sessionFile: owner.worker.sessionFile, plan: owner.plan, requestId: owner.worker.requestId, text: message.content.text, supersedes }, false); + recordReport(ctx, { id, session: sender, sessionFile: owner.worker.sessionFile, plan: owner.plan, requestId: owner.worker.requestId, text: message.content.text, supersedes }, false, false); // Intercom already displays and saves this report. } if (ctx.isIdle()) remindReports(ctx); } diff --git a/src/notice-display.ts b/src/notice-display.ts index 408faaa..13397cb 100644 --- a/src/notice-display.ts +++ b/src/notice-display.ts @@ -10,7 +10,7 @@ export function noticeDisplay(pi: ExtensionAPI) { context.messageType === "user" && mirrored.has(markdown) ? "" : markdown); pi.registerEntryRenderer(NOTICE, (entry, { expanded }, theme) => { const { content } = entry.data as { content: string }; - const label = content.includes("\nPlan changed.") ? "Plan changed · review requested" : "Goal instructions"; + const label = content.includes("\nPlan changed") ? "Plan changed · review requested" : content.includes("\nPending worker reviews:") ? "Worker reviews pending" : "Goal instructions"; if (expanded) return new Markdown(content, 0, 0, getMarkdownTheme()); return { render: (width) => [truncateToWidth(theme.fg("muted", `[pi-goals] ${label} · ${keyHint("app.tools.expand", "expand")}`), width)], diff --git a/src/prompts.ts b/src/prompts.ts index f8fbda0..ca4cf76 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -193,9 +193,9 @@ export function workerAttachment(plan: string, session: string, text: string): s // Pi/OpenAI: supervisor-authored report reviews, separate from goal completion. export const reportReviewDescription = "Review an owned worker report after inspecting its actual artifacts. Quote the assigned goal/task and evidence from files (optional saved-session entryId selects decoded message text). State observations and unmet requirements; use accepted, changes_requested or blocked. Changes requested need a concrete continuation. Text quotes are checked, not their relevance or quality. Non-text evidence needs a nonempty capture and specific observation. Delivery stays pending until the worker saves the visible review. Acceptance never completes a goal or wakes/closes the worker."; export const reportReviewContent = (report: string, sessionFile: string, sources: string[], observation: string, unmet: string, verdict: string, continuation: string) => `Worker review: ${verdict}\nReport: ${report}\nSaved session: ${sessionFile}\n\nAssigned goal/task:\n${sources[0]}\n\nEvidence:\n${sources.slice(1).join("\n\n")}\n\nInspected: ${observation}\nUnmet: ${unmet}\nContinuation: ${continuation || "none"}\nThis is a report review, not CompleteGoal.\n— Pi supervisor`; -export const pendingReportReviews = (reports: string[]) => `Pending worker reviews: ${reports.join(", ")}. Inspect their saved reports and actual artifacts, then use review_subagent. Independent authorized work may continue; receipts and generic replies do not resolve reviews.`; -export function workerReview(plan: string, session: string, text: string): string { - return `Worker event for ${plan}, exact Intercom session ${session}:\n${text}\nThis is a report, not completion approval. Inspect actual artifacts and saved messages; if correction is needed, send it to the same session. Preserve its visible review conversation. Respect pauses; do not reply merely to acknowledge.`; +export const pendingReportReviews = (reports: string[]) => `Pending worker reviews: ${reports.join(", ")}. Inspect reports and artifacts; use review_subagent. Independent authorized work may continue.`; +export function workerReview(report: string, text: string): string { + return `Worker report ${report}:\n${text}`; } export function manualReview(planPath: string, text: string): string { diff --git a/test/goals.test.ts b/test/goals.test.ts index 976bcf5..bfc05fa 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -1290,21 +1290,32 @@ it("reviews a saved worker revision through inspection, silent delivery, retry a parent.hooks.get("session_start")({}, parent.ctx); // Reconcile the missed stop from real saved worker history. await parent.command("status"); expect(parent.ctx.ui.notify.mock.lastCall?.[0]).toContain(missed); + await parent.hooks.get("agent_settled")({}, parent.ctx); form.report = missed; await review(); // Old-plan blocked review remains deliverable after retargeting. await parent.command("status"); expect(parent.ctx.ui.notify.mock.lastCall?.[0]).not.toContain(missed); expect(parent.ctx.ui.notify.mock.lastCall?.[0]).toContain(nextReport); + const afterReview = parent.messages.length; + await parent.hooks.get("agent_settled")({}, parent.ctx); + parent.hooks.get("session_start")({}, parent.ctx); + await parent.hooks.get("agent_settled")({}, parent.ctx); + expect(parent.messages).toHaveLength(afterReview); // Shrinking/restoring the same backlog does not wake again. const ordinary = (id: string, text: string, links = {}, sender = workerId) => { const details = { from: { id: sender }, message: { id, timestamp: Date.now(), content: { text }, ...links } }; parent.ctx.sessionManager.getBranch().push({ type: "custom_message", id, customType: "intercom_message", content: text, details }); parent.hooks.get("message_end")({ message: { role: "custom", customType: "intercom_message", content: text, details } }, parent.ctx); }; + const beforeOrdinary = parent.messages.length; ordinary("ordinary-a", "Partial output needs review"); ordinary("ordinary-b", "Corrected output needs review", { supersedes: "ordinary-a" }); ordinary("retry-b", "Corrected output needs review", { retryOf: "ordinary-b" }); ordinary("retry-again", "Corrected output needs review", { retryOf: "retry-b" }); ordinary("ack-only", "OK"); ordinary("foreign", "Unowned report", {}, "foreign-peer"); - parent.hooks.get("session_start")({}, parent.ctx); await parent.command("status"); + expect(parent.messages).toHaveLength(beforeOrdinary); // No second body beside Intercom's saved/displayed original. + parent.hooks.get("session_start")({}, parent.ctx); + await parent.hooks.get("agent_settled")({}, parent.ctx); + expect(parent.messages).toHaveLength(beforeOrdinary + 1); // The new/revised obligation still wakes once. + await parent.command("status"); const pending = parent.ctx.ui.notify.mock.lastCall?.[0]; expect(pending).toContain(`${workerId}:ordinary-b`); for (const excluded of ["ordinary-a", "retry-b", "retry-again", "ack-only", "foreign-peer"]) expect(pending).not.toContain(excluded); From 4599b5ad9ff89c0338b6c1e6ded363b7753cc8fd Mon Sep 17 00:00:00 2001 From: wassname2 Date: Tue, 15 Sep 2026 17:01:10 +0800 Subject: [PATCH 3/6] Refresh same-plan context without repeating ownership approval --- src/index.ts | 4 ++++ src/prompts.ts | 1 + test/rpc-review.test.ts | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/index.ts b/src/index.ts index 7a1030c..58f2c24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -691,6 +691,10 @@ export default function mainSupervisor(pi: ExtensionAPI) { let text: string; try { text = readFileSync(target, "utf8"); } catch { ctx.ui.notify(`Cannot read plan at ${target}.`, "error"); 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.plan === target && state.mode !== "chat") { + notice = true; fullPlanContextDue = true; refresh(ctx); + ctx.ui.notify(nativeMessages.samePlanRestored, "info"); 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; diff --git a/src/prompts.ts b/src/prompts.ts index ca4cf76..31ddef7 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -268,6 +268,7 @@ export function attachNotice(planPath: string, solo: boolean, notedWorker: strin } export const nativeMessages = { + samePlanRestored: "Plan context refreshed; mode and worker binding unchanged. No new work authorized.", workerPause: (paused: boolean) => `Worker ${paused ? "paused" : "unpaused"} locally; no new task submitted and no approval authority granted.`, taskRequired: "Supply an explicit bounded proposed task for a new worker context.", modelDescription: "User preference for agent-led configuration and verification, not a launch override.", diff --git a/test/rpc-review.test.ts b/test/rpc-review.test.ts index ee370b6..2ac1ba3 100644 --- a/test/rpc-review.test.ts +++ b/test/rpc-review.test.ts @@ -185,6 +185,12 @@ describe("RPC review flow", () => { expect(client.messages.some(event => event.type === "message_end" && (event.message as any)?.role === "user" && (event.message as any)?.content[0]?.text === content)).toBe(true); expect(supervisor.messages.filter(message => message.role === "user" && messageText(message.content) === content)).toHaveLength(1); } + const restoreStart = client.messages.length; + client.send({ type: "prompt", id: "restore-same-plan", message: `/goals attach ${planPath}` }); + await client.waitFor(message => message.type === "response" && message.id === "restore-same-plan", restoreStart); + expect(client.messages.slice(restoreStart).filter(isSelect)).toEqual([]); + expect(client.messages.slice(restoreStart).some(message => message.type === "extension_ui_request" && message.method === "notify" && JSON.stringify(message).includes("mode and worker binding unchanged"))).toBe(true); + expect(requests).toHaveLength(beforeReady + 1); const beforeNotice = requests.length, noticeStart = client.messages.length; client.send({ type: "prompt", id: "attachment-notice", message: "/fixture-attachment-notice" }); const attachment = await client.waitFor(message => message.type === "message_end" && (message.message as any)?.customType === "pi-goals-supervision", noticeStart); From ed050658e381ce0caaabcb2da15c2934b38b140f Mon Sep 17 00:00:00 2001 From: wassname2 Date: Tue, 15 Sep 2026 17:20:27 +0800 Subject: [PATCH 4/6] Block unverified external plan adoption without granting authority --- README.md | 4 +++- src/index.ts | 16 +++++++------- src/prompts.ts | 8 +++---- test/goals.test.ts | 52 +++++++++++++++++++++++++++------------------- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 56ed406..1a94278 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,9 @@ The parent and worker keep separate native conversations. Worker attachment and `OpenGoalWorker` supplies startup only to a newly created stock Pi context. An existing live binding receives no message, so opening it does not replace its conversation or editor draft. The new worker calls `AttachGoalPlan`, reports its exact Intercom identity/model/saved-session path, and waits for a direct parent assignment. Revisions use that same session. A model preference is an instruction for agent-led configuration and verification, not a CLI override; later human changes take precedence. -There is no custom fresh/recover operation. Inspect stopped workers' saved history and partial results, preserve drafts/queued input, and confirm the exact writer stopped before stock `project.close`/`project.open`. Stock close checks ownership and idle state, but cannot establish editor-draft safety. If uncertain, retain the pane and inspect it. Before a new goals-managed worker, archive obsolete worker identity notes into Log and clear/reattach the plan after confirming stop; do not discard history or replay completed work. Automatic ownership transfer is not provided. — Pi/OpenAI +There is no custom fresh/recover operation. Inspect stopped workers' saved history and partial results, preserve drafts/queued input, and confirm the exact writer stopped before stock `project.close`/`project.open`. Stock close checks ownership and idle state, but cannot establish editor-draft safety. If uncertain, retain the pane and inspect it. Preserve history and completed work. Automatic ownership transfer remains unresolved. — Pi/OpenAI + +`/goals attach` now rejects a plan that is not already current in this context, including `attach solo` and reattachment after Clear. The public roster cannot establish its supervisor's ownership; a checkbox or missing roster row is not proof. The command leaves current authority unchanged and provides read-only inspection controls. Keep the original supervisor context when available rather than clearing it to reconnect. Same-current-plan refresh and its separate explicit stopped-writer confirmation for solo recovery remain available. This guard does not solve generic adoption or cross-parent transfer. — Pi/OpenAI ## Context delivery diff --git a/src/index.ts b/src/index.ts index 58f2c24..44c5270 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,6 @@ import { FOLD_LINE, foldPlan, GOAL_LINE, planRequirements as requirements } from import { planViews } from "./plan-view.js"; import { attachGoalPlanDescription, - attachNotice, childPlanAttached, childPlanRole, completeGoalDescription, @@ -311,12 +310,13 @@ export default function mainSupervisor(pi: ExtensionAPI) { pi.sendUserMessage(prompt, { deliverAs: "followUp" }); } else pi.sendMessage({ customType: "pi-goals-supervision", content, display: true }, { deliverAs: "nextTurn" }); } - async function confirmOwnership(ctx: ExtensionContext, target: string, text: string, solo = true): Promise { + async function confirmOwnership(ctx: ExtensionContext, target: string, text: string): Promise { + if (target !== state.plan || state.mode === "chat") { ctx.ui.notify(nativeMessages.externalOwnershipUnknown(target, state.worker), "warning"); return false; } if (opening) { 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"; - const choice = await ctx.ui.select(solo ? "Confirm all other writers for the current and target plans are stopped (inspect Intercom and their native 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"]); + const confirmation = "Worker confirmed stopped"; + const choice = await ctx.ui.select("Confirm all other writers for the current and target plans are stopped (inspect Intercom and their native panes). A missing handle is not proof. Take over in this session?", [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; } @@ -695,14 +695,12 @@ export default function mainSupervisor(pi: ExtensionAPI) { notice = true; fullPlanContextDue = true; refresh(ctx); ctx.ui.notify(nativeMessages.samePlanRestored, "info"); 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; + if (!(await confirmOwnership(ctx, target, text))) return; const worker = noted ? { sessionFile: resolve(ctx.cwd, noted) } : state.workerStopped ? state.worker : undefined; - state = { mode: solo ? "solo" : "planning", plan: target, worker, workerStopped: solo || (!noted && state.workerStopped) }; + state = { mode: "solo", plan: target, worker, workerStopped: true }; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx); - if (solo) enterSolo(ctx); - else send(attachNotice(target, false, noted)); + enterSolo(ctx); return; } if (command === "exit") { diff --git a/src/prompts.ts b/src/prompts.ts index 31ddef7..b4de76c 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -263,11 +263,11 @@ export const soloRole = "Solo mode: implement the approved plan directly; do not 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."}`; -} - export const nativeMessages = { + externalOwnershipUnknown: (path: string, worker?: { intercomId?: string; sessionFile?: string; paneId?: string; identity?: { paneId?: string } }) => { + const pane = worker?.identity?.paneId || worker?.paneId; + return `Cannot verify ownership of ${path}: the supported Intercom roster does not identify per-plan supervisors; a missing row is not exit proof. Original supervisor unknown. Current context and authority unchanged; no adoption or takeover authorized. Read-only inspection: read({path:${JSON.stringify(path)}}). ${worker ? `Current worker only (not proof of the target's owner): ${worker.intercomId ? `intercom action:list, locate exact ID ${worker.intercomId}. ` : ""}${worker.sessionFile ? `read({path:${JSON.stringify(worker.sessionFile)}}). ` : ""}${pane ? `herdr pane process-info --pane ${JSON.stringify(pane)}. ` : ""}` : ""}Use /goals status for current references. Return to the original supervisor's saved context only when independently identified; no target can be inferred here.`; + }, samePlanRestored: "Plan context refreshed; mode and worker binding unchanged. No new work authorized.", workerPause: (paused: boolean) => `Worker ${paused ? "paused" : "unpaused"} locally; no new task submitted and no approval authority granted.`, taskRequired: "Supply an explicit bounded proposed task for a new worker context.", diff --git a/test/goals.test.ts b/test/goals.test.ts index bfc05fa..7275e47 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -332,15 +332,14 @@ it("rejects an existing zero-byte evidence file", async () => { expect(result.content[0].text).toContain("Empty evidence"); expect(readFileSync(f.path, "utf8")).toBe(before); }); -it("requires actual nonempty evidence, distinguishes manual ticks, and retains reviewed markers through Clear/reattach", async () => { +it("requires actual nonempty evidence, distinguishes manual ticks, and retains reviewed markers through same-context restoration", async () => { const f = fixture(); await f.draft(); await f.command("ready"); const complete = (goal: string, evidence: string[], signal?: AbortSignal) => f.tools.get("CompleteGoal").execute("t", { goal, evidence, observation: "Inspected exact saved bytes" }, signal, undefined, f.ctx); expect((await complete("first output", ["missing.log"])).content[0].text).toContain("Evidence unavailable"); mkdirSync(join(f.ctx.cwd, "evidence")); writeFileSync(join(f.ctx.cwd, "evidence/pass.log"), "actual fixture bytes\n"); expect((await complete("first output", ["evidence/pass.log"], AbortSignal.abort())).content[0].text).toContain("Cancelled"); await complete("first output", ["evidence/pass.log"]); - await f.command("clear"); - f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped"); await f.command(`attach ${f.path}`); await f.command("ready"); + await f.command(`attach ${f.path}`); writeFileSync(f.path, readFileSync(f.path, "utf8").replace("[ ] goal: second", "[x] goal: second")); f.hooks.get("session_start")({}, f.ctx); expect(f.ctx.ui.setStatus).toHaveBeenLastCalledWith("goals", "👀 1/2 goals"); @@ -575,21 +574,22 @@ it("requires confirmed worker stop before solo takeover and never lets two write expect(text).toContain("self-verification"); }); -it("attaches an existing plan without restarting completed work, and restores its noted worker session", async () => { +it("leaves an unverified external plan and its noted worker untouched", async () => { const f = fixture(); const existing = join(f.ctx.cwd, "existing.md"); writeFileSync(existing, "# Plan\n- preferred worker model: deepseek flash\n- worker session: /tmp/attach-child.jsonl\n- [ ] goal: attached goal\n\n## Log\n- previous progress kept\n"); f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped"); await f.command(`attach ${existing}`); - expect(f.entries.at(-1).data.mode).toBe("planning"); - expect(f.entries.at(-1).data.plan).toBe(existing); - expect(f.messages.at(-1).message.content).toContain("without restarting completed work"); - expect(f.messages.at(-1).message.content).toContain("/tmp/attach-child.jsonl"); + expect(f.entries).toEqual([]); + expect(f.messages).toEqual([]); + expect(f.ctx.ui.select).not.toHaveBeenCalled(); + expect(readFileSync(existing, "utf8")).toContain("previous progress kept"); + expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Original supervisor unknown"), "warning"); }); -it("attaches directly into solo mode and reports the recorded session in status", async () => { - const f = fixture(); - const existing = join(f.ctx.cwd, "existing.md"); +it("retains same-current-plan solo recovery and reports the recorded session in status", async () => { + const f = fixture(); await f.draft(); + const existing = f.path; writeFileSync(existing, "# Plan\n- worker session: /tmp/attach-child.jsonl\n- [ ] goal: attached goal\n\n## Log\n"); f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command(`attach ${existing} solo`); @@ -621,9 +621,10 @@ it.each(["exit", "quit", "clear", "menu"])("%s exits planning with the draft pre expect(f.ctx.ui.setWidget).toHaveBeenLastCalledWith("goals", undefined); expect(readFileSync(f.path, "utf8")).toContain("first output"); expect(f.messages.length).toBe(before); // notify only, no model turn started - f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped"); await f.command(`attach ${f.path}`); - expect(f.entries.at(-1).data.mode).toBe("planning"); + expect(f.entries.at(-1).data.mode).toBe("chat"); + expect(f.messages.length).toBe(before); + expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("Cannot verify ownership"), "warning"); }); it("records the preferred worker model as a visible plan preference", async () => { @@ -647,7 +648,7 @@ it.each(["solo", "attach"])("%s takeover cannot bypass confirmation or survive a expect(f.entries.at(-1).data.workerStopped).not.toBe(true); }); -it("attach solo requires stop confirmation for a noted worker even in a fresh session", async () => { +it("external attach solo cannot turn a noted worker or stop checkbox into ownership proof", async () => { const f = fixture(); const path = join(f.ctx.cwd, "saved.md"); writeFileSync(path, `# Plan\n- worker session: /tmp/known.jsonl\n${f.plan}`); f.ctx.ui.select.mockResolvedValueOnce("Cancel"); @@ -655,21 +656,27 @@ it("attach solo requires stop confirmation for a noted worker even in a fresh se expect(f.entries).toHaveLength(0); f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command(`attach ${path} solo`); - expect(f.entries.at(-1).data).toMatchObject({ mode: "solo", workerStopped: true, worker: { sessionFile: "/tmp/known.jsonl" } }); + expect(f.entries).toHaveLength(0); + expect(f.ctx.ui.select).not.toHaveBeenCalled(); + expect(f.messages).toEqual([]); expect(readFileSync(path, "utf8")).toContain("worker session: /tmp/known.jsonl"); }); -it("retains the stopped session reference across plan changes", async () => { +it("retains current solo authority and stopped-session reference when external adoption is blocked", async () => { const f = fixture(); await f.draft(); await f.command("ready"); await f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" }); f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); const other = join(f.ctx.cwd, "another.md"); writeFileSync(other, "- [ ] goal: next\n## Log\n"); + const before = f.entries.at(-1), messageCount = f.messages.length; f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped"); await f.command(`attach ${other}`); - expect(f.entries.at(-1).data).toMatchObject({ mode: "planning", plan: other, workerStopped: true, worker: { sessionFile: "/tmp/prior.jsonl" } }); - await f.command("ready"); + expect(f.entries.at(-1).data).toMatchObject({ mode: "solo", plan: f.path, workerStopped: true, worker: { sessionFile: "/tmp/prior.jsonl" } }); + expect(f.entries.at(-1)).toBe(before); expect(f.messages).toHaveLength(messageCount); + expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining('read({path:"/tmp/prior.jsonl"})'), "warning"); + expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining('herdr pane process-info --pane "native-pane"'), "warning"); + expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("locate exact ID child"), "warning"); const response = await f.tools.get("OpenGoalWorker").execute("open", { task: "next task" }, undefined, undefined, f.ctx); - expect(response.content[0].text).toContain("already recorded"); + expect(response.content[0].text).toContain("solo"); expect(f.entries.at(-1).data.workerStopped).toBe(true); }); @@ -922,7 +929,7 @@ it("changed plan or shutdown during takeover never grants solo permission", asyn expect(f.entries.at(-1).data.mode).toBe("planning"); }); -it("requires explicit supervisor ownership confirmation when attaching an existing plan", async () => { +it("blocks unknown external ownership without offering an attestation or launching work", async () => { const f = fixture(); const path = join(f.ctx.cwd, "shared.md"); writeFileSync(path, f.plan); f.ctx.ui.select.mockResolvedValueOnce("Cancel"); @@ -930,7 +937,10 @@ it("requires explicit supervisor ownership confirmation when attaching an existi expect(f.entries).toHaveLength(0); f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped"); await f.command(`attach ${path}`); - expect(f.entries.at(-1).data).toMatchObject({ mode: "planning", plan: path }); + expect(f.entries).toHaveLength(0); + expect(f.ctx.ui.select).not.toHaveBeenCalled(); + expect(f.messages).toEqual([]); + expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("supported Intercom roster does not identify per-plan supervisors"), "warning"); }); it("does not approve cancelled goals or display current completion for an unavailable plan", async () => { From 8ca35bce33ecb340399e05164d7f0015215352d5 Mon Sep 17 00:00:00 2001 From: wassname2 Date: Tue, 15 Sep 2026 17:37:57 +0800 Subject: [PATCH 5/6] Preserve worker binding through the existing solo transition --- src/index.ts | 4 +--- test/goals.test.ts | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 44c5270..879c7a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -697,9 +697,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { } const noted = /^-\s*worker session:\s*(\S+)/im.exec(foldPlan(text))?.[1]; if (!(await confirmOwnership(ctx, target, text))) return; - const worker = noted ? { sessionFile: resolve(ctx.cwd, noted) } : state.workerStopped ? state.worker : undefined; - state = { mode: "solo", plan: target, worker, workerStopped: true }; - generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx); + if (noted && !state.worker) state.worker = { sessionFile: resolve(ctx.cwd, noted) }; enterSolo(ctx); return; } diff --git a/test/goals.test.ts b/test/goals.test.ts index 7275e47..a2cda24 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -665,7 +665,9 @@ it("external attach solo cannot turn a noted worker or stop checkbox into owners it("retains current solo authority and stopped-session reference when external adoption is blocked", async () => { const f = fixture(); await f.draft(); await f.command("ready"); await f.launch({ id: "child", sessionFile: "/tmp/prior.jsonl" }); - f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command("solo"); + const binding = f.entries.at(-1).data.worker; + f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped"); await f.command(`attach ${f.path} solo`); + expect(f.entries.at(-1).data.worker).toEqual(binding); const other = join(f.ctx.cwd, "another.md"); writeFileSync(other, "- [ ] goal: next\n## Log\n"); const before = f.entries.at(-1), messageCount = f.messages.length; f.ctx.ui.select.mockResolvedValueOnce("Previous supervisor confirmed stopped"); From 695381de3abce2411dddd7545d2dc14512c8dd5f Mon Sep 17 00:00:00 2001 From: wassname2 Date: Tue, 15 Sep 2026 17:46:15 +0800 Subject: [PATCH 6/6] Drop redundant package manifest literal assertions --- test/package.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/package.test.ts b/test/package.test.ts index fcf242f..5d51b2a 100644 --- a/test/package.test.ts +++ b/test/package.test.ts @@ -6,14 +6,11 @@ import { expect, it } from "vitest"; it("declares current entry and bundled extension resources that exist after install", () => { const manifest = JSON.parse(readFileSync("package.json", "utf8")); - expect(manifest.pi.extensions[0]).toBe("./src/index.ts"); for (const path of manifest.pi.extensions) expect(existsSync(resolve(path)), path).toBe(true); for (const name of ["pi-subagents", "pi-intercom", "@jl1990/pi-scheduler"]) { expect(manifest.dependencies[name]).toBeTruthy(); expect(manifest.bundleDependencies).toContain(name); } - expect(manifest.dependencies["pi-subagents"]).toBe("0.66.0"); - expect(manifest.bundledDependencies).toBeUndefined(); expect(JSON.parse(readFileSync("package-lock.json", "utf8")).packages[""].bundleDependencies).toEqual(manifest.bundleDependencies); });