diff --git a/README.md b/README.md index eea744f..1764166 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Supervisors and workers can use ordinary stock async helpers, with one writer pe `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. -When a moved/cloned supervisor inherits a worker binding, or a recorded worker appears stopped, the supervisor inspects `worker_view`, Intercom/native status and relevant jobs. It then calls `OpenGoalWorker` with `replaceStopped.observation`: a concise model judgment citing what it inspected. pi-goals records that basis, preserves saved history/reviews, releases the runtime binding and opens a newly correlated worker. No human infrastructure modal is used. Absence alone is weak evidence, but it is not a reason to stop indefinitely; resolve material contradictions, otherwise decide and continue. Raw `project.open` is not a goals-worker recovery path because it lacks attachment and stop correlation. — Pi/OpenAI +`OpenGoalWorker` delegates pane ownership to stock open. When stock opens a new pane, pi-goals preserves and supersedes any recorded worker binding and correlates the replacement. When stock reports an existing pane, pi-goals preserves the current binding and returns that result for normal supervisor handling. This recovers moved/cloned supervisors without a human infrastructure modal or extension-level liveness gate. pi-goals owns attachment, report and stop correlation—not generic writer concurrency. Raw `project.open` is not a goals-worker recovery path because it lacks those hooks. — 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 diff --git a/src/index.ts b/src/index.ts index 1527758..2f90daa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -859,11 +859,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { }); pi.registerTool({ name: "OpenGoalWorker", label: "Open native goal worker", description: nativeMessages.openDescription, - parameters: Type.Object({ - task: Type.String({ minLength: 1 }), - model: Type.Optional(Type.String({ description: nativeMessages.modelDescription })), - replaceStopped: Type.Optional(Type.Object({ observation: Type.String({ minLength: 1, description: nativeMessages.replacementObservationDescription }) })), - }), + parameters: Type.Object({ task: Type.String({ minLength: 1 }), model: Type.Optional(Type.String({ description: nativeMessages.modelDescription })) }), async execute(_id, params, signal, _update, ctx) { if (state.child || state.mode !== "supervising" || !state.plan) return result(goalToolBlocked(state.mode)); // TODO(2026-11+, Pi): Recheck pi-subagents/project-panes v1's one-pane-per-cwd limit before adding multiple visible workers. @@ -876,21 +872,24 @@ export default function mainSupervisor(pi: ExtensionAPI) { const self = peers.filter(peer => peer.pid === process.pid); if (self.length !== 1) return result(nativeMessages.noIdentity); if (preflight !== generation || opening || signal?.aborted) return result(messages.cancelled); - if (state.worker) { - if (!params.replaceStopped?.observation.trim()) return result(nativeMessages.replacementNeedsInspection(state.worker, state.worker.intercomId ? peers.some(peer => peer.id === state.worker?.intercomId) : undefined)); - pi.appendEntry(WORKER_RELEASE, { plan: state.plan, worker: structuredClone(state.worker), observation: params.replaceStopped.observation.trim(), releasedBy: identity(ctx), at: new Date().toISOString() }); - state.worker = undefined; state.workerStopped = true; workerRevision++; generation++; save(); refresh(ctx); - } + const superseded = state.worker ? structuredClone(state.worker) : undefined; const preference = params.model?.trim() || notedPlanValue("preferred worker model"); const model = preference && (preference.includes("/") || !/^(?:none|\(none|default|inherit|not stated)\b/i.test(preference)) ? preference : undefined; const plan = state.plan; - if (opening || state.worker || signal?.aborted) return result(messages.cancelled); + if (opening || signal?.aborted) return result(messages.cancelled); const requestId = randomUUID(); state.worker = { requestId, parentId: self[0].id, task: params.task }; state.workerStopped = false; workerRevision++; opening = true; save(); try { // Stock open sends startup only to a newly created context; existing panes receive nothing. const pane = await openProjectPane({ cwd: ctx.cwd, message: workerAssignment(plan, self[0].id, requestId, params.task, model), focus: false, signal }); - if (pane.ok && state.plan === plan && state.worker?.requestId === requestId) { state.worker.paneId = pane.data.binding.paneId; save(); } + if (pane.ok && state.plan === plan && state.worker?.requestId === requestId) { + if (pane.data.disposition === "already-open" && superseded) state.worker = superseded; + else { + if (superseded) pi.appendEntry(WORKER_RELEASE, { plan, worker: superseded, supersededBy: identity(ctx), task: params.task, at: new Date().toISOString() }); + state.worker.paneId = pane.data.binding.paneId; + } + save(); refresh(ctx); + } return result(pane.ok ? JSON.stringify({ disposition: pane.data.disposition, paneId: pane.data.binding.paneId, projectRoot: pane.data.binding.projectRoot, bindingPath: pane.data.bindingPath }) + nativeMessages.openReceipt : JSON.stringify(pane)); } catch (error) { return result(nativeMessages.openFailed + String(error)); } finally { opening = false; } diff --git a/src/prompts.ts b/src/prompts.ts index 288f7ab..ebf1b14 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -161,7 +161,7 @@ const helperGuidance = "Use ordinary stock async helpers when useful, not anothe export const childPlanRole = "You are the delegated implementation worker. Save evidence and report progress for your delegated work; leave plan maintenance to the parent. 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. Call ReportGoalEvent when there is a meaningful result or status change, including a later blocker or completion after progress. Do not repeat unchanged events. review_request, blocker and completion require formal parent review. decision asks the parent to choose or steer directly without that form. progress, running, waiting, receipt and no_change do not require review; use progress when work changed but the correct instruction is simply to continue. Treat any proposed change to protected project intent, editorial/publication authority, core research design or evaluation principles as a decision, not an ordinary implementation choice. Put the canonical summary and exact artifact paths in the event. When waiting, name the child/job you await, its owner or handle, and what will wake you. Ending a turn while followed work continues is not task completion. Then stay open for live messages. Do not exit or use caller_ping; unsent editor drafts are not visible in model context." + " " + helperGuidance; export function readyApproved(workerName: string, planPath: string, notedWorker: string | undefined, plan: string, supervisorId: string): string { const launch = notedWorker - ? `Inspect recorded history ${notedWorker} and actual writer state. If live, steer that exact Intercom session; do not replace its conversation. If stopped, preserve history and drafts, then call OpenGoalWorker with a concise replaceStopped observation; it records the judgment and releases the old runtime binding before replacement. Do not bypass worker ownership with project.open or wait for a human to interpret infrastructure state.` + ? `Inspect recorded history ${notedWorker} and actual writer state. Reuse it when useful. When replacement is the better route, call OpenGoalWorker: stock decides whether a pane is new, and pi-goals preserves/supersedes the recorded binding only for a newly opened replacement. Coordinate possible concurrent writers through normal supervisor judgment and stock controls; pi-goals does not gate generic writer concurrency. Do not bypass worker correlation with raw project.open.` : `Use OpenGoalWorker with a bounded proposed task for '${workerName}'. It uses stock project.open, not subagent execution. A new worker attaches and waits; after inspecting its report, send the authorized task through exact-session Intercom.`; return `[pi-goals: approval — Ready]\nReady approved this plan: ${planPath}. Stay here as supervisor. ${launch} Confirm your actual Intercom UUID with status/list; your Pi session ID ${supervisorId} is a distinct field. Await explicit worker attachment and a report with actual Intercom UUID, saved-session path and resolved model. worker_view must show the correlated saved session before assignment. Never create a replacement goals-worker through raw Intercom openProjectPaneIfMissing or subagent project.open: those panes are not parent-owned and their automatic stop events cannot be supervised. Use OpenGoalWorker for an interactive worker, or a bounded stock helper for authorized non-pane work. Inspect results and steer corrections in the same owned session. A receipt, roster row or idle pane is not attachment, writer exit or completion.\n\n${quotedPlan(planPath, foldPlan(plan), "working set before Log")}`; } @@ -185,7 +185,7 @@ Humour is a reflective meta-learning mechanism, not decoration. At natural check You can speculate and brainstorm around uncertainty or unexpected results. Label guesses as guesses, consider alternative explanations, and look for a useful way to tell them apart. Keep exploration brief, open-minded and fun: take a step back, play with surprising ideas, question the current framing, and enjoy exploring the broader perspective while staying connected to the agreed goal. Take uncertainty as an invitation to investigate, not something to hide. Have room to play with ideas, question yourself and the worker, and appreciate a good surprise. Investigate surprising results, find mistaken assumptions, make complicated ideas simpler, and disagree usefully rather than agree politely. Keep the work moving without turning supervision into paperwork. A little affectionate teasing is welcome when it fits, and workers can push back too. Keep the humor friendly and the criticism specific. -- Pi/Astra Use OpenGoalWorker for the native project pane and stock Intercom only for exact-session assignment/report/steering after correlated attachment. Never create a goals-worker with raw Intercom openProjectPaneIfMissing or subagent project.open. A roster row is not attachment; worker_view must show the attached saved session before assignment, otherwise automatic stop supervision is unavailable. Use a bounded stock helper for authorized non-pane work rather than invent an orphan goals-worker. Do not use subagent as a second goals-worker backend. Supervise only this plan's attached worker and owned helpers; foreign agents may be coordinated with, but never stopped, retasked, closed or reviewed without explicit user authority. ${helperGuidance} A stored binding is not proof of liveness; missing runtime state is not proof of stop. Verify actual Intercom identities with list/status; your Pi session ID is ${supervisorId}, a distinct field. Require artifact paths, saved verification and blocker/error reports. When the worker stops for any reason, inspect actual artifacts and saved messages before approving or correcting it in the same open session. A recap or receipt alone sends no instruction and proves no action. Record actual pane identity, '- worker session:' and '- worker intercom session:' with provenance. CompleteGoal belongs only to this parent or explicitly confirmed solo self-verification. -Keep normal tools and honor human model changes. The human can inspect, talk to and change /model in the worker pane directly; treat direct human instructions and the worker's current model as authoritative rather than assuming an agent changed them. Do not revert either unless the human asks. Inherit by default. If the user supplies a model preference to the supervisor, pass it explicitly to the agent through OpenGoalWorker's model instruction or exact-session Intercom steering; let the agent configure it through supported controls and verify its actual choice. project.open itself has no model override; a requested model is not proof of configuration. Report a specific unavailable choice without silently substituting or stalling unrelated authorized work. After compaction reread the plan. Lost connection or exhausted credits does not erase work. Preserve drafts and saved sessions; confirm other writers stopped before solo takeover. Revisions use ordinary Intercom in the same context. New workers attach/report and wait for your direct assignment; verify current execution authorization before sending it. For stopped-worker replacement or a cloned/moved supervisor session, inspect saved history, Intercom/native status and partial work, preserve any editor draft/queued input, then use your judgment and call OpenGoalWorker with replaceStopped.observation. It records the basis and releases the old runtime binding. Never replace through raw project.open. Absence alone is weak evidence, not a reason to stop: combine the available evidence, raise a genuinely material contradiction, otherwise decide and keep work moving. If a material conflict remains genuinely unobservable, retain the pane, use an isolated or bounded helper for safe work, and raise the exact defect without stopping unrelated goals; do not invent recovery controls. Do not reapply historical preferences over later human choices. Never replace an unreviewed conversation or start a duplicate writer.`; +Keep normal tools and honor human model changes. The human can inspect, talk to and change /model in the worker pane directly; treat direct human instructions and the worker's current model as authoritative rather than assuming an agent changed them. Do not revert either unless the human asks. Inherit by default. If the user supplies a model preference to the supervisor, pass it explicitly to the agent through OpenGoalWorker's model instruction or exact-session Intercom steering; let the agent configure it through supported controls and verify its actual choice. project.open itself has no model override; a requested model is not proof of configuration. Report a specific unavailable choice without silently substituting or stalling unrelated authorized work. After compaction reread the plan. Lost connection or exhausted credits does not erase work. Preserve drafts and saved sessions; confirm other writers stopped before solo takeover. Revisions use ordinary Intercom in the same context. New workers attach/report and wait for your direct assignment; verify current execution authorization before sending it. For stopped-worker replacement or a cloned/moved supervisor session, inspect saved history and partial work when useful, then use your judgment and call OpenGoalWorker. A newly opened replacement supersedes the recorded runtime binding while preserving its history; an already-open stock pane preserves the current binding. pi-goals owns attachment/report correlation, not generic writer concurrency; coordinate other writers through normal stock controls without turning uncertainty into a human gate. Never replace through raw project.open because it lacks goal stop correlation. If infrastructure fails, use an isolated or bounded helper for safe work and raise the exact defect without stopping unrelated goals; do not invent recovery controls. Do not reapply historical preferences over later human choices. Never replace an unreviewed conversation or start a duplicate writer.`; } // Routine notices quote only selected goal lines; full context stops at Log. const goalLines = (text: string) => foldPlan(text).split("\n").filter(line => GOAL_LINE.test(line)).join("\n"); @@ -280,7 +280,7 @@ export function pauseExitNotice(worker: { intercomId?: string; sessionFile?: str 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. If stopped, inspect saved history, Intercom/native status and partial work, preserve drafts, then call OpenGoalWorker with replaceStopped.observation and continue. Do not wait for the human to interpret routine infrastructure state. Preserve later human model choices.` : `Use OpenGoalWorker for '${workerName}' with a bounded proposed task.`} Continue only unfinished goals; never replay a completed assignment; retain saved progress and scheduler edits.`; + return `User authorized continuation of ${planPath}. ${worker ? `Reuse the existing session ${worker.sessionFile ?? "unknown"} / Intercom ${worker.intercomId ?? "unknown"} when useful; otherwise call OpenGoalWorker; a newly opened replacement preserves and supersedes that recorded binding. Use normal supervisor judgment and stock controls for possible concurrent writers; do not ask the human to interpret routine infrastructure state. Preserve later human model choices.` : `Use OpenGoalWorker for '${workerName}' with a bounded proposed task.`} Continue only unfinished goals; never replay a completed assignment; 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." + " " + helperGuidance; export function soloNotice(planPath: string): string { @@ -295,9 +295,7 @@ export const nativeMessages = { 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.", - openDescription: "After Ready, open a native project pane with a bounded proposed task. Stock open sends startup only into a newly created Pi context; existing panes receive no message. The worker must AttachGoalPlan and report, then wait for an explicit exact-session assignment. Inherit model defaults unless the user supplies a preference for agent-led configuration. Receipts are not attachment. Revisions use Intercom. If this supervisor was cloned/moved or a worker is already recorded, inspect available history/status and supply replaceStopped.observation when your judgment is that the old writer stopped. The tool records that basis, preserves history/reviews, releases the runtime binding and continues without a human modal.", - replacementObservationDescription: "Concise supervisor judgment that the recorded worker stopped, citing the inspected saved history, Intercom/native status, direct user statement or job state. Absence alone is weak evidence; contradictory material evidence should be resolved, not hidden.", - replacementNeedsInspection: (worker: { intercomId?: string; sessionFile?: string; paneId?: string; requestId?: string; identity?: { paneId?: string } }, listed: boolean | undefined) => `A worker is already recorded: Intercom ${worker.intercomId ?? "unconfirmed"} (${listed === undefined ? "roster identity unavailable" : listed ? "currently listed" : "not in the current roster"}); pane ${worker.identity?.paneId || worker.paneId || "unconfirmed"}; saved session ${worker.sessionFile ?? "unconfirmed"}; request ${worker.requestId ?? "unconfirmed"}. Inspect worker_view plus available native/job state, then decide. If stopped, call OpenGoalWorker again with replaceStopped: { observation: "" }. Do not stop goal work at this diagnostic: the tool records the judgment and continues without asking the human to interpret infrastructure state.`, + openDescription: "After Ready, ask stock project.open for a native worker pane with a bounded proposed task. A newly opened worker must AttachGoalPlan and report, then wait for exact-session assignment; pi-goals preserves and supersedes any prior runtime binding only after stock opens that replacement. An existing pane receives no startup, so its current binding is preserved and the result is returned for normal supervisor handling. Inherit model defaults unless the user supplies a preference for agent-led configuration. pi-goals owns attachment/report/stop correlation, not generic writer concurrency.", disconnected: "Intercom disconnected; current liveness is unknown. Inspect saved history and available pane/job state, then use supervisor judgment: reconnect/steer if live, or record a stopped-worker replacement observation and continue. Do not wait indefinitely on transport uncertainty.", shuttingDown: "Worker session shutting down; inspect its last saved messages. No goal sign-off inferred.", noAssistant: "Worker run ended without an assistant result; inspect saved messages.", diff --git a/test/goals.test.ts b/test/goals.test.ts index dbea559..1e645cc 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -98,9 +98,9 @@ it("reserves formal reviews for approvals, completion and genuine blockers", () expect(role).toContain("never stopped, retasked, closed or reviewed without explicit user authority"); expect(role).toContain("Humour is a reflective meta-learning mechanism"); expect(role).toContain("The human can inspect, talk to and change /model in the worker pane directly"); - expect(role).toContain("replaceStopped.observation"); - expect(role).toContain("Absence alone is weak evidence, not a reason to stop"); + expect(role).toContain("pi-goals owns attachment/report correlation, not generic writer concurrency"); expect(role).not.toContain("human confirmation"); + expect(role).not.toContain("replaceStopped"); expect(role).toContain("Never wait on an inferred or nonexistent pane"); expect(goalCheckInWake).toContain("reserve formal review for completion, bounded artifact approval or a genuine accepted blocker"); }); @@ -1227,28 +1227,34 @@ it("opens no-focus, records explicit attachment only, and wakes review only for expect(f.messages).toHaveLength(cleared); }); -it("records supervisor judgment before replacing an inherited worker binding", async () => { +it("supersedes an inherited worker binding when the supervisor opens a replacement", async () => { const f = fixture(); await f.draft(); await f.command("ready"); await f.launch({ id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", task: "Old task" }); const oldRequest = f.entries.at(-1).data.worker.requestId; const selects = f.ctx.ui.select.mock.calls.length; - const inspect = await f.tools.get("OpenGoalWorker").execute("inspect", { task: "Continue the approved plan" }, undefined, undefined, f.ctx); - expect(inspect.content[0].text).toContain("Intercom old-worker (not in the current roster)"); - expect(inspect.content[0].text).toContain("replaceStopped"); - expect(openProjectPane).toHaveBeenCalledTimes(1); - const observation = "User said the prior worker is gone; worker_view ends five days ago and exact Intercom ID is absent."; - const opened = await f.tools.get("OpenGoalWorker").execute("replacement", { task: "Continue the approved plan", replaceStopped: { observation } }, undefined, undefined, f.ctx); + const opened = await f.tools.get("OpenGoalWorker").execute("replacement", { task: "Continue the approved plan" }, undefined, undefined, f.ctx); expect(opened.content[0].text).toContain('"disposition":"opened"'); const replacement = f.entries.at(-1).data.worker; expect(replacement).toMatchObject({ paneId: "native-pane", parentId: "parent-intercom", task: "Continue the approved plan" }); expect(replacement.requestId).not.toBe(oldRequest); const release = f.ctx.sessionManager.getBranch().find((entry: any) => entry.customType === "pi-goals-worker-release"); - expect(release?.data).toMatchObject({ plan: f.path, worker: { intercomId: "old-worker", requestId: oldRequest }, observation }); + expect(release?.data).toMatchObject({ plan: f.path, worker: { intercomId: "old-worker", requestId: oldRequest }, task: "Continue the approved plan" }); expect(f.entries.some(entry => entry.data.worker?.intercomId === "old-worker")).toBe(true); // saved state history is retained expect(f.ctx.ui.select).toHaveBeenCalledTimes(selects); expect(openProjectPane).toHaveBeenCalledTimes(2); }); +it("preserves the current worker binding when stock reports an existing pane", async () => { + const f = fixture(); await f.draft(); await f.command("ready"); + await f.launch({ id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", task: "Old task" }); + const previous = structuredClone(f.entries.at(-1).data.worker); + vi.mocked(openProjectPane).mockResolvedValueOnce({ ok: true, data: { bindingPath: "/existing/binding.json", disposition: "already-open", binding: { paneId: "existing-pane", projectRoot: f.ctx.cwd, command: "pi" } } }); + const opened = await f.tools.get("OpenGoalWorker").execute("existing", { task: "Proposed replacement" }, undefined, undefined, f.ctx); + expect(opened.content[0].text).toContain('"disposition":"already-open"'); + expect(f.entries.at(-1).data.worker).toEqual(previous); + expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-release")).toBe(false); +}); + it("automatically reports worker turn end and pauses a rejected attachment", async () => { const f = fixture(); const path = join(f.ctx.cwd, "supplied.md"); writeFileSync(path, f.plan); await f.tools.get("AttachGoalPlan").execute("attach", { path, parent: "live-parent", requestId: "owned-request" }, undefined, undefined, f.ctx); @@ -1311,7 +1317,7 @@ it("ordinary project peer explicitly attaches as worker, never gaining approval expect(f.channel.publish).toHaveBeenLastCalledWith(expect.objectContaining({ type: "stopped", text: "Result at output.txt" }), { audience: "capable" }); }); -it("pending or failed native opening never permits an unconfirmed second writer", async () => { +it("blocks concurrent opening and lets stock pane ownership resolve a retry", async () => { const f = fixture(); await f.draft(); await f.command("ready"); const tool = f.tools.get("OpenGoalWorker"); expect(tool.parameters.properties.task.minLength).toBe(1); @@ -1320,12 +1326,15 @@ it("pending or failed native opening never permits an unconfirmed second writer" vi.mocked(openProjectPane).mockImplementationOnce(() => new Promise((_resolve, reject) => { release = () => reject(new Error("connection lost after open")); })); const opening = f.tools.get("OpenGoalWorker").execute("open", { task: "first" }, undefined, undefined, f.ctx); await waitFor(() => Boolean(release)); + const concurrent = await f.tools.get("OpenGoalWorker").execute("concurrent", { task: "second" }, undefined, undefined, f.ctx); + expect(concurrent.content[0].text).toContain("already opening"); + expect(openProjectPane).toHaveBeenCalledTimes(1); await f.command("solo"); expect(f.ctx.ui.notify).toHaveBeenLastCalledWith(expect.stringContaining("still pending"), "warning"); release(); expect((await opening).content[0].text).toContain("possible live writer"); const again = await f.tools.get("OpenGoalWorker").execute("open", { task: "again" }, undefined, undefined, f.ctx); - expect(again.content[0].text).toContain("already recorded"); - expect(openProjectPane).toHaveBeenCalledTimes(1); + expect(again.content[0].text).toContain('"disposition":"opened"'); + expect(openProjectPane).toHaveBeenCalledTimes(2); }); it("leaves an existing stock pane unbound instead of replacing or retasking it", async () => {