From 0a35869be9e5cc31417c77fd69e76d7e7b5bd757 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:07:14 +0800 Subject: [PATCH] Show sampled fortune after substantive goal updates without model work Append a UI-only saved entry after supervisor or solo final updates; omit short unchanged waits. Remove the prompt instructions to sample fortunes, and cover the non-context renderer and lifecycle. Co-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com> --- README.md | 2 +- src/index.ts | 3 ++- src/notice-display.ts | 19 +++++++++++++++++++ src/prompts.ts | 3 +-- test/goals.test.ts | 13 +++++++++++++ test/notice-display.test.ts | 13 +++++++++++++ 6 files changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6e1b038..8f29526 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Give occasional, unprompted, plain-English updates to the user at existing check After human Ready, the supervisor should establish and verify one scheduled check-in, and be reminded if it has no working reminder. It decides the cadence according to the supervision required, editing the existing task rather than adding timers. Reliably followed long work can need fewer checks; drift or stalled work can need more. Check-ins are also a good time to update the user. -A short Markdown checklist and an occasional joke, kaomoji or cowsay are welcome: they are funny and easy to spot when scrolling. Automatic updates should include a random row from `fortune.txt`. +A short Markdown checklist and an occasional joke or cowsay are welcome. For substantial supervisor/solo final updates, pi-goals displays a random line from `~/.pi/agent/skills/ml-debug/fortune.txt` (when that file exists) with a kaomoji as a separate, saved UI entry after the response. It is not sent to the model or added to formal evidence. Short unchanged waits do not get one. diff --git a/src/index.ts b/src/index.ts index b8e60c8..dc2933e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { Markdown, truncateToWidth } from "@earendil-works/pi-tui"; import { INTERCOM_EXTENSION_REGISTER_EVENT, type IntercomExtensionChannel, type IntercomExtensionRegistration } from "pi-intercom/extension-api.js"; import { openProjectPane } from "pi-subagents/project-panes"; import { Type } from "typebox"; -import { noticeDisplay } from "./notice-display.js"; +import { noticeDisplay, substantiveUpdate } from "./notice-display.js"; import { FOLD_LINE, foldPlan, GOAL_LINE, planRequirements as requirements } from "./plan.js"; import { planViews } from "./plan-view.js"; import { @@ -614,6 +614,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { if (last?.role === "assistant" && (last.errorMessage || ["error", "aborted"].includes(last.stopReason))) requestedPlanReview = undefined; const text = last?.role === "assistant" ? last.errorMessage || last.content.filter(part => part.type === "text").map(part => part.text).join("\n") || last.stopReason : nativeMessages.noAssistant; reportStop(text, last?.role === "assistant" && last.stopReason === "aborted" ? "aborted" : last?.role === "assistant" && (last.stopReason === "error" || last.errorMessage) ? "blocker" : "unclassified", true, true); + if (!state.child && ["supervising", "solo"].includes(state.mode) && last?.role === "assistant" && last.stopReason === "stop" && substantiveUpdate(text)) notices.fortune(); finalReviewTurnDigest = undefined; refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); }); pi.on("agent_start", (_event, ctx) => { agentRunActive = true; diff --git a/src/notice-display.ts b/src/notice-display.ts index 85bb7e2..65d1b9c 100644 --- a/src/notice-display.ts +++ b/src/notice-display.ts @@ -1,10 +1,22 @@ // Pi/OpenAI: Pi converts routine custom notices to the same user-role model input; legacy mirrored prompts remain exact. +import { randomInt } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { type ExtensionAPI, type ExtensionContext, getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent"; import { Markdown, truncateToWidth } from "@earendil-works/pi-tui"; const NOTICE = "pi-goals-notice"; const PROMPT = "pi-goals-prompt"; const COMPACT = "pi-goals-compact-prompt"; +const FORTUNE = "pi-goals-fortune"; +const kaomoji = ["(˶ᵔ ᵕ ᵔ˶)", "( •̀ᴗ•́ )و", "( ̄▽ ̄)", "(。•̀ᴗ-)✧"]; +const fortunePath = join(homedir(), ".pi/agent/skills/ml-debug/fortune.txt"); + +export function substantiveUpdate(text: string): boolean { + const update = text.trim(); + return update.length > 30 && !/^(?:no (?:material )?change|still waiting|waiting for|nothing (?:new|changed))\b/i.test(update); +} function noticeLabel(content: string) { return content.includes("[pi-goals: plan activity]") ? "Plan activity recorded" @@ -32,8 +44,15 @@ export function noticeDisplay(pi: ExtensionAPI) { }; }; pi.registerEntryRenderer(NOTICE, (entry, { expanded }, theme) => render((entry.data as { content: string }).content, expanded, theme)); + pi.registerEntryRenderer(FORTUNE, (entry) => new Markdown((entry.data as { content: string }).content, 0, 0, getMarkdownTheme())); pi.registerMessageRenderer(PROMPT, (message, { expanded }, theme) => render(typeof message.content === "string" ? message.content : message.content.filter(part => part.type === "text").map(part => part.text).join("\n"), expanded, theme)); return { + fortune() { + if (!existsSync(fortunePath)) return; + const lines = readFileSync(fortunePath, "utf8").split(/\r?\n/).filter(Boolean); + if (!lines.length) throw new Error(`Empty fortune file: ${fortunePath}`); + pi.appendEntry(FORTUNE, { content: `${kaomoji[randomInt(kaomoji.length)]} ${lines[randomInt(lines.length)]}` }); + }, prompt(content: string) { pi.sendMessage({ customType: PROMPT, content, display: true }, { triggerTurn: true, deliverAs: "followUp" }); }, diff --git a/src/prompts.ts b/src/prompts.ts index 22f9817..c0777fa 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -110,7 +110,6 @@ const supervisorJob = "Take responsibility for advancing the user's goal and pre export function supervisor(workerName: string, planPath: string, supervisorId: string): string { return `You are the goal supervisor in the main chat for ${planPath}. ${supervisorJob}\n${waitingGuidance}\nInspect actual artifacts, saved verification, applicable AGENTS.md and skills yourself; delegate implementation to '${workerName}'. Use worker_view for compact saved history. Investigate blocked/waiting/done claims using recent saved tool calls with arguments and results, then current child/job status when needed. History proves a launch or watch at that time, not current liveness. A worker ending its turn may still await work; verify follow-up and change ineffective instructions. Give brief visible assessments with judgment. Infer protected decisions from User voice, applicable instructions and prior choices: publication or editorial approval, the core experiment, evaluation principles, scope and spending are examples, not a fixed list. Put a proposed change to one first, explain its effect and get explicit user approval. You may maintain the plan but must not weaken or change the goal to accept worker output. When a worker asks to stop or reports completion/blockage, choose among: steer/retry in the same session; permit an in-flight plan edit while work remains open; or use full review_subagent evidence because you may allow the worker to stop. Only your third choice creates review paperwork. Never wait on an inferred or nonexistent pane. -In many substantive user-facing updates, sample one random line with shuf -n 1 ~/.pi/agent/skills/ml-debug/fortune.txt; a kaomoji is welcome too. Keep the update about the goal and the next action; omit the fortune from formal evidence. -- wassname Investigate surprises: question your framing, label guesses, consider competing explanations and test what distinguishes them. Leave room for exploration, friendly humour and worker pushback; keep criticism specific. -- PI/OpenAI Use OpenGoalWorker for the native project pane and stock Intercom only for exact-session assignment/report/steering after correlated attachment. Pass cwd to target a real, already-authorized task directory or worktree; omission uses this supervisor's cwd. Stock has one binding per canonical cwd, not multiple slots in the same cwd. Supervisors can stay in the shared project directory; independently supervised native workers need distinct real task directories/worktrees. Choose the worker cwd rather than moving the supervisor or retrying an occupied binding. Keep one writer per checkout and follow the planned integration approach. Inspect artifacts and project status in the returned worker projectRoot (retained in /goals status), not automatically in the supervisor's directory. Directory existence does not authorize work. 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 model guidance, pass it through OpenGoalWorker or exact-session Intercom and verify the actual setting before assignment. ${nativeModelControls} 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.`; @@ -162,7 +161,7 @@ export function finalReview(planPath: string, text: string): string { } // Check-ins. The installed scheduler owns storage/timing/UI; only new default wakes are one line. -export const goalCheckInWake = "Goal check-in: only while supervising unfinished authorized goals, read the plan and latest worker evidence. Keep the user's outcome and preferences in view. Does the result demonstrate that outcome, including its failure discriminators? If not, diagnose and give the next useful instruction. Investigate claimed blockers through existing project setup and tools before escalating; verify action rather than an acknowledgement. Let a genuinely running, followed job proceed without paperwork. Use formal review only when considering a stop. When results change, briefly show the user what they mean and what happens next. Include a random line from `shuf -n 1 ~/.pi/agent/skills/ml-debug/fortune.txt`; kaomoji welcome. Keep unchanged waits to one line and slow reliable check-ins. Respect pauses, do not replay completed work or create a timer from this wake."; +export const goalCheckInWake = "Goal check-in: only while supervising unfinished authorized goals, read the plan and latest worker evidence. Keep the user's outcome and preferences in view. Does the result demonstrate that outcome, including its failure discriminators? If not, diagnose and give the next useful instruction. Investigate claimed blockers through existing project setup and tools before escalating; verify action rather than an acknowledgement. Let a genuinely running, followed job proceed without paperwork. Use formal review only when considering a stop. When results change, briefly show the user what they mean and what happens next. Keep unchanged waits to one line and slow reliable check-ins. Respect pauses, do not replay completed work or create a timer from this wake."; export const schedulerMessages = { unconfirmed: "Owned check-in removal unconfirmed: no fresh scheduler result could be observed in this saved session. The request is cancelled; later results will not trigger removal. Inspect /schedules all and use exact owned IDs with /schedule-remove.", unavailable: "Owned check-in removal unavailable: verified @jl1990/pi-scheduler commands are not loaded. No model turn or replacement timer was started. Inspect /schedules all.", diff --git a/test/goals.test.ts b/test/goals.test.ts index c927b66..3f2734c 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -77,6 +77,19 @@ function fixture(child = false) { return { ctx, pi, hooks, tools, commands, messages, command, get path() { return path; }, plan, draft, shutdown, changed, atomicWrite, get entries() { return entries.filter(entry => entry.customType === "pi-goals-main-supervisor-v1"); }, start, launch, channel, event: (event: any) => registration.onEvent(event) }; } +it("saves an automatic non-context fortune after a substantive supervisor update, not an unchanged wait", async () => { + const f = fixture(); await f.draft(); await f.command("ready"); + const entries = f.ctx.sessionManager.getBranch(); + const complete = (text: string) => f.hooks.get("agent_end")({ messages: [{ role: "assistant", stopReason: "stop", content: [{ type: "text", text }] }] }, f.ctx); + complete("The held-out prediction beat the matched shuffled control on the same examples; next I will inspect error cases."); + const fortune = entries.filter(entry => entry.customType === "pi-goals-fortune"); + expect(fortune).toHaveLength(1); + expect(fortune[0].type).toBe("custom"); + expect(fortune[0].data.content).toMatch(/ -- /); + complete("No change: waiting on the followed job."); + expect(entries.filter(entry => entry.customType === "pi-goals-fortune")).toHaveLength(1); +}); + it("shows incremental VCC Markdown without raw tool results or compaction dumps", async () => { initTheme("dark"); const f = fixture(true), history = f.ctx.sessionManager.getBranch(), timestamp = new Date().toISOString(); diff --git a/test/notice-display.test.ts b/test/notice-display.test.ts index 7d2ce2b..6228f49 100644 --- a/test/notice-display.test.ts +++ b/test/notice-display.test.ts @@ -67,3 +67,16 @@ it("collapses mirrored prompts only in the UI, expands the exact text, and resto expect(transform(rolePrompt, { messageType: "user" })).toBe("[pi-goals] Goal instructions"); expect(entry.data.content).toBe(content); }); + +it("renders a saved sampled fortune without sending it to the model", () => { + initTheme("dark"); + const pi = { registerMarkdownTransformer: vi.fn(), registerEntryRenderer: vi.fn(), registerMessageRenderer: vi.fn(), appendEntry: vi.fn(), sendMessage: vi.fn() }; + const display = noticeDisplay(pi as unknown as ExtensionAPI); + display.fortune(); + const [type, data] = pi.appendEntry.mock.calls[0]; + expect(type).toBe("pi-goals-fortune"); + expect(data.content).toMatch(/ -- /); + const renderer = pi.registerEntryRenderer.mock.calls.find(([name]) => name === type)?.[1]; + expect(renderer({ data }, { expanded: false }, {})).toBeInstanceOf(Markdown); + expect(pi.sendMessage).not.toHaveBeenCalled(); +});