From 54268feb26fdec1b04c4c14fbf648a40de192e77 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:04:02 +0800 Subject: [PATCH] Wake supervisors only for material plan changes Co-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com> --- AGENTS.md | 3 +- .../2026-09-20-supervision-intent-findings.md | 18 ++++++-- src/index.ts | 30 +++++++++---- src/notice-display.ts | 4 +- src/plan-view.ts | 11 +++-- src/prompts.ts | 7 ++- test/goals.test.ts | 44 +++++++++++++++---- test/notice-display.test.ts | 4 ++ test/plan-view.test.ts | 21 ++++++--- 9 files changed, 106 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7ad4df0..d5c2ec0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,8 @@ The supervisor should: - Put all model-facing prompts in `src/prompts.ts`, in conversation order. Preserve the user's verbatim requirements. - `/goals` opens actions. New plan starts a discussion without an objective form. Unknown commands never start planning. A changed settled draft opens the approval dialogue; unchanged discussion does not repeatedly reopen it. - Keep goal titles/status in widgets; omit subtask text. Tasks and evidence remain in the plan. -- Keep startup/compaction plan context, short upkeep reminders and visible check-ins. Avoid unchanged-plan repetition and identity-only review turns. +- Keep startup/compaction plan context, short upkeep reminders and visible check-ins. Record task/evidence bookkeeping passively; wake the supervisor only for changed requirements or goal status. — wassname (Pi wording) +- A secret-display restriction does not block an authorized credential-backed command: use the project's existing loader without exposing values, and ask the human only when authorization, the credential or execution permission is absent. — wassname (Pi wording) - Keep recoverable solo mode: confirm other writers stopped before taking over. Solo completion is self-verification. - Record distinct runtime ID, Intercom ID and saved-session path with provenance. A handle or delivery receipt is not proof of liveness or action. User model changes are authorized; do not silently restore an old preference. diff --git a/slop/reviews/2026-09-20-supervision-intent-findings.md b/slop/reviews/2026-09-20-supervision-intent-findings.md index 55c259d..42764d9 100644 --- a/slop/reviews/2026-09-20-supervision-intent-findings.md +++ b/slop/reviews/2026-09-20-supervision-intent-findings.md @@ -68,13 +68,23 @@ Current supervisor instructions say: Only the third choice creates formal review state. The scheduled default and worker-event text repeat this rule. The manifold review churn is evidence about an older loaded prompt or model noncompliance, not the current intended flow. -### 5. Plan edits still create avoidable supervisor turns +### 5. Task and evidence edits no longer create supervisor turns -`watchPlan` hashes `planViews(plan).notify`. `planViews` removes Log and worker-identity lines, but retains task and evidence edits above Log. Any such edit triggers a plan-change turn even when `planRequirements` says requirements did not change. The injected prompt says evidence-only edits do not revoke approval, but the model still has to process and answer the wake. +Production observation before this change: -This can explain the observed sequence “plan changed” → “checkbox update applied” → “No change.” It remains a likely source of recap noise. A later change should retain wakes for goal status and requirement changes while ignoring evidence-only and subtask-only edits. That change needs a focused test because goal checkbox updates must remain observable. +> “plan changed” → “checkbox update applied” → “No change.” -### 6. Check-in behavior is mixed but often follows intent +`planViews` now separates the full pre-Log activity view from the material notification view. Task checkboxes and evidence paths change the activity hash and create one collapsed passive record for the next ordinary turn; they do not wake the model. Goal checkbox status, requirements, discriminators, scope and preferred worker model still change the notification hash and wake the supervisor. Tests distinguish evidence/subtask edits from goal-status and requirement changes. + +### 6. Credential display restrictions were mistaken for execution restrictions + +Production observation, manifold-steer: + +> “Blocked before queue submission: harness denies `--env-file .env`.” + +The supervisor then asked the human to run an otherwise authorized judge command, even though the project could load the credential without displaying it. The user clarified that `python-dotenv` or shell-sourcing `.env` exists for this purpose. Shared worker/supervisor guidance now distinguishes displaying secret bytes from running an authorized credential-backed command. It requires the existing project loader, forbids reading, printing or sending secret values, and asks the human only when authorization, the credential or execution permission is actually absent. + +### 7. Check-in behavior is mixed but often follows intent Observed good behavior: diff --git a/src/index.ts b/src/index.ts index b56f1f6..179dfaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ import { pausedRole, pauseExitNotice, pendingReportReviews, + planActivityRecorded, planChangedReview, planContext, planDocument, @@ -125,6 +126,12 @@ export default function mainSupervisor(pi: ExtensionAPI) { let planWatcher: FSWatcher | undefined; let planEditTimer: ReturnType | undefined; let planHash = ""; + let planActivityHash = ""; + const syncPlanHashes = (text: string) => { + const views = planViews(text); + planHash = digest(views.notify); + planActivityHash = digest(views.activity); + }; const save = () => pi.appendEntry(STATE, structuredClone(state)); // Missing, empty and failed reads are unavailable snapshots, never an empty authoritative plan. const readPlan = () => { @@ -269,13 +276,11 @@ export default function mainSupervisor(pi: ExtensionAPI) { clearTimeout(planEditTimer); planEditTimer = undefined; const snapshot = readPlan(); - if (snapshot.text !== undefined) planHash = digest(planViews(snapshot.text).notify); + if (snapshot.text !== undefined) syncPlanHashes(snapshot.text); if (state.child || state.mode !== "supervising" || !state.plan) return; const stamp = generation; - // Watch the directory so atomic plan replacement remains observable. This is an event hook: - // plan-change reviews, not another scheduled loop (the hourly job is pi-scheduler's). A - // short debounce coalesces bursts. The notification view excludes Log and worker identity; - // requirement changes additionally request active-plan context. + // Atomic replacements are observable; task/evidence bookkeeping is recorded passively. — Pi/OpenAI + // Goal status or requirement changes wake the supervisor and request current plan context. try { planWatcher = watch(dirname(state.plan), { persistent: false }, () => { if (stamp !== generation) return; @@ -287,8 +292,15 @@ export default function mainSupervisor(pi: ExtensionAPI) { if (snapshot.text === undefined) { ctx.ui.notify(snapshot.error!, "warning"); return; } clearChangedFinalReview(snapshot.text); refresh(ctx); - const hash = digest(planViews(snapshot.text).notify); - if (hash === planHash) return; + const views = planViews(snapshot.text); + const activityHash = digest(views.activity); + if (activityHash === planActivityHash) return; + planActivityHash = activityHash; + const hash = digest(views.notify); + if (hash === planHash) { + send(planActivityRecorded(state.plan!), false, true); + return; + } planHash = hash; notice = true; fullPlanContextDue ||= requirements(snapshot.text) !== requirements(lastWorkingSet); @@ -765,7 +777,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { if (found >= 0) lines[found] = pref; else { const title = lines.findIndex((line) => /^#\s/.test(line)); lines.splice(title >= 0 ? title + 1 : 0, 0, pref); } writeFileSync(state.plan, lines.join("\n")); - planHash = digest(planViews(planText()).notify); + syncPlanHashes(planText()); refresh(ctx); notice = true; fullPlanContextDue = true; ctx.ui.notify(`Preferred worker model recorded as ${ref}; not yet configured. Pass it to the agent in its assignment or live steering for configuration through supported controls, then verify its actual model.`, "info"); @@ -1054,7 +1066,7 @@ export default function mainSupervisor(pi: ExtensionAPI) { writeFileSync(path, `${lines.join("\n").trimEnd()}\n`); state.finalReview = undefined; finalReviewTurnDigest = undefined; - planHash = digest(planViews(planText()).notify); + syncPlanHashes(planText()); save(); refresh(ctx); const remaining = Boolean(unfinishedGoals(planText())); if (!remaining) requestCheckInRemoval(ctx); diff --git a/src/notice-display.ts b/src/notice-display.ts index 6dd42b3..85bb7e2 100644 --- a/src/notice-display.ts +++ b/src/notice-display.ts @@ -7,7 +7,9 @@ const PROMPT = "pi-goals-prompt"; const COMPACT = "pi-goals-compact-prompt"; function noticeLabel(content: string) { - return content.includes("\nPlan changed") ? "Plan changed · review requested" + return content.includes("[pi-goals: plan activity]") ? "Plan activity recorded" + : content.includes("\nPlan requirements or goal status changed") ? "Plan changed · review requested" + : content.includes("\nPlan changed") ? "Plan changed · review requested" : content.includes("## Selected worker-stop reviews") ? "Selected worker-stop reviews" : content.includes("## Worker stop review:") ? "Worker stop review" : content.includes("## Worker status:") ? "Worker status" diff --git a/src/plan-view.ts b/src/plan-view.ts index 33c199f..b6a8614 100644 --- a/src/plan-view.ts +++ b/src/plan-view.ts @@ -1,7 +1,10 @@ -// Pi/OpenAI: Review task/evidence changes, but omit history and worker identity bookkeeping. -import { foldPlan } from "./plan.js"; +// Pi/OpenAI: Record task/evidence activity, but wake only for requirements or goal status. +import { foldPlan, GOAL_LINE, planRequirements } from "./plan.js"; -export function planViews(plan: string): { notify: string } { +export function planViews(plan: string): { notify: string; activity: string } { const identity = /^[ \t]*[-*]\s*(?:active worker|worker session|worker intercom session):/i; - return { notify: foldPlan(plan).split("\n").filter(line => !identity.test(line)).join("\n").trim() }; + const activity = foldPlan(plan).split("\n").filter(line => !identity.test(line)).join("\n").trim(); + const status = activity.split("\n").filter(line => GOAL_LINE.test(line)); + const preferences = activity.split("\n").filter(line => /^\s*[-*]\s*preferred worker model:/i.test(line)); + return { activity, notify: [planRequirements(plan), ...status, ...preferences].filter(Boolean).join("\n") }; } diff --git a/src/prompts.ts b/src/prompts.ts index ab244fc..8a0f0ab 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -140,7 +140,7 @@ export const discuss = "Type your changes in chat; the draft stays open."; // Ready and explicit native peer attachment. No worker environment or agent-file contract. export const attachGoalPlanDescription = "Attach the absolute plan path explicitly supplied by the parent. On first attachment or explicit same-parent plan/request change, supply the exact existing Intercom parent UUID and newly assigned requestId. Changes require live parent verification; a different parent cannot take over. Omit these fields only to restore unchanged plan context. Preserve session history and prior reviews. Read the plan without rewriting it. Restores plan context; grants no parent completion authority. No discovery or worker launch."; export const reportGoalEventDescription = "Report one meaningful event for the current delegated worker run. Events remain visible and deduplicated but never create formal review by themselves. Use review_request when asking to stop for approval, completion when the assigned task appears complete, blocker when autonomous progress cannot continue, and decision when parent judgment is needed while work can remain open. The supervisor may steer or permit an in-flight plan edit without a form; only the supervisor can choose full review when allowing a stop may be justified. Routine work and queued jobs are progress or waiting."; -const helperGuidance = "Use ordinary stock async helpers when useful, not another interactive goals-worker. Check stock capabilities before launch, including external-CLI runner availability. Keep one writer per cwd or isolated worktree and follow results/failures through the owning session. Supervise only the worker attached to this plan and helpers launched by its owner. Other agents, panes, jobs and schedules are foreign: coordinate when useful, but do not retask, pause, stop, close or review them unless the user explicitly assigns that authority. Pause blocks new owner launch/resume requests; already-dispatched owned workflows may continue, so inspect or stop them through their owner. When tooling, pane, subagent or harness infrastructure fails, inspect the exact native state, understand and fix the cause when practical, and report any remaining loss of visibility or control. Do not claim to wait for a pane unless native status shows that exact pane exists and is closing. Continue unaffected authorized work; a stale binding or unavailable pane need not block a bounded stock helper in an isolated worktree, with the parent retaining goal authority. If the requested model is unavailable, use another model only when the plan or user already approved it and verify the actual model. Infrastructure becomes a blocker only after authorized stock alternatives fail or the fallback would change a protected decision, ownership, spending or the user-visible result. Never silently switch to CLI or foreground fallback."; +const helperGuidance = "Use ordinary stock async helpers when useful, not another interactive goals-worker. Check stock capabilities before launch, including external-CLI runner availability. Keep one writer per cwd or isolated worktree and follow results/failures through the owning session. Supervise only the worker attached to this plan and helpers launched by its owner. Other agents, panes, jobs and schedules are foreign: coordinate when useful, but do not retask, pause, stop, close or review them unless the user explicitly assigns that authority. Pause blocks new owner launch/resume requests; already-dispatched owned workflows may continue, so inspect or stop them through their owner. When tooling, pane, subagent or harness infrastructure fails, inspect the exact native state, understand and fix the cause when practical, and report any remaining loss of visibility or control. Do not claim to wait for a pane unless native status shows that exact pane exists and is closing. Continue unaffected authorized work; a stale binding or unavailable pane need not block a bounded stock helper in an isolated worktree, with the parent retaining goal authority. A tool being unable to display a secret file does not make an already authorized credential-backed command impossible: use the project's existing loader, such as python-dotenv or a shell-sourced .env, without reading, printing or sending secret values. Ask the human only when authorization or the credential is missing, or command execution itself is denied; do not ask them to run an otherwise authorized command for you. If the requested model is unavailable, use another model only when the plan or user already approved it and verify the actual model. Infrastructure becomes a blocker only after authorized stock alternatives fail or the fallback would change a protected decision, ownership, spending or the user-visible result. Never silently switch to CLI or foreground fallback."; 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. No event creates review paperwork by itself: the parent normally steers, retries or permits an in-flight plan edit, and chooses full review only when it may allow you to stop. 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 @@ -187,8 +187,11 @@ export function upkeep(planPath: string, text: string, supervisorRound?: number) export function planContext(mode: string, path: string | undefined, text: string, tier: "short" | "medium" | "full" = "full"): string { return `[pi-goals: context resync]\nCurrent goal mode: ${mode}. Earlier role messages are historical; this current role governs. Read the plan file for details and earlier evidence; do not restart completed work.\n\n${quotedPlan(path, tier === "full" ? foldPlan(text) : goalLines(text), tier === "full" ? "active plan above Log" : "unfinished or unreviewed goal lines")}`; } +export function planActivityRecorded(planPath: string): string { + return `[pi-goals: plan activity]\nTask or evidence bookkeeping changed at ${planPath}. Recorded without waking the supervisor; goal status and requirements are unchanged.`; +} export function planChangedReview(planPath: string, text = ""): string { - return `[pi-goals: reminder — plan changed]\nPlan changed: inspect current requirements, completion claims and evidence at ${planPath}. Evidence-only edits do not revoke execution approval. Continue only unfinished authorized work; respect pauses and do not assume approval for changed scope. [x] is reported done, not reviewed. [✓] records parent review through CompleteGoal. When requirements change, inspect the evidence and reopen affected reviewed goals with [ ] or [/] if necessary; status is not automatically invalidated. Do not start a duplicate writer.${text ? `\n\n${quotedPlan(planPath, goalLines(text), "selected goal lines")}` : ""}`; + return `[pi-goals: reminder — plan changed]\nPlan changed: requirements or goal status changed; inspect current requirements, completion claims and evidence at ${planPath}. Continue only unfinished authorized work; respect pauses and do not assume approval for changed scope. [x] is reported done, not reviewed. [✓] records parent review through CompleteGoal. When requirements change, inspect the evidence and reopen affected reviewed goals with [ ] or [/] if necessary; status is not automatically invalidated. Do not start a duplicate writer.${text ? `\n\n${quotedPlan(planPath, goalLines(text), "selected goal lines")}` : ""}`; } export function workerAttachment(plan: string, session: string, text: string): string { return `Worker attachment for ${plan}, exact Intercom session ${session}:\n${text}\nMetadata only; no acknowledgement or review turn requested.`; diff --git a/test/goals.test.ts b/test/goals.test.ts index 1122ca3..a3ba10d 100644 --- a/test/goals.test.ts +++ b/test/goals.test.ts @@ -105,6 +105,9 @@ it("leaves stop events informal until the supervisor chooses full review", () => expect(role).toContain("pi-goals owns attachment/report correlation, not generic writer concurrency"); expect(role).toContain("Only your third choice creates review paperwork"); expect(role).toContain("Never wait on an inferred or nonexistent pane"); + expect(role).toContain("unable to display a secret file does not make an already authorized credential-backed command impossible"); + expect(role).toContain("python-dotenv or a shell-sourced .env"); + expect(role).toContain("without reading, printing or sending secret values"); expect(goalCheckInWake).toContain("Use formal review only when you choose to allow it to stop"); }); @@ -495,6 +498,33 @@ old progress`; f.shutdown(); }); +it("records task and evidence bookkeeping without waking, but wakes for goal status", async () => { + const f = fixture(); await f.draft(); + const plan = `# Plan +## Goals +- [ ] goal: first output + - discriminator: output exists + - tasks: + - [ ] run it + - evidence: + - old.log +- [ ] goal: second output + +## Log +`; + writeFileSync(f.path, plan); await f.command("ready"); + await f.atomicWrite(plan.replace("- [ ] run it", "- [x] run it").replace("old.log", "new.log")); + await waitFor(() => f.messages.some(m => m.message?.content?.includes("[pi-goals: plan activity]"))); + const activity = f.messages.find(m => m.message?.content?.includes("[pi-goals: plan activity]")); + expect(activity.options).toEqual({ deliverAs: "nextTurn" }); + expect(activity.message.content).toContain("Recorded without waking the supervisor"); + expect(f.changed()).toBe(0); + await f.atomicWrite(readFileSync(f.path, "utf8").replace("[ ] goal: first", "[/] goal: first")); + await waitFor(() => f.changed() === 1); + expect(f.messages.find(m => m.message?.content?.includes("Plan changed: requirements or goal status changed"))?.options).toMatchObject({ deliverAs: "followUp" }); + f.shutdown(); +}); + it("delivers changed plans while coalescing only its own pending notice", async () => { const f = fixture(); await f.draft(); await f.command("ready"); f.ctx.hasPendingMessages.mockReturnValue(true); // An unrelated queued prompt must not suppress the notice. @@ -817,25 +847,23 @@ it.each(["missing", "empty", "directory"])("%s plan snapshots remain unavailable expect(f.changed()).toBe(0); }); -it("ignores post-completion maintenance but reviews evidence, requirement or manual reopening changes", async () => { +it("ignores post-completion history, records evidence, and reviews requirement or reopening changes", async () => { const f = fixture(); await f.draft(); await f.command("ready"); writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n"); for (const goal of ["first output", "second output"]) await f.tools.get("CompleteGoal").execute("c", { goal, evidence: ["proof.log"], observation: "PASS" }, undefined, undefined, f.ctx); const signed = readFileSync(f.path, "utf8"); await f.atomicWrite(signed.replace("## Log", "## Log\n- recap: finished")); await delay(250); - expect(f.changed()).toBe(0); // Log-only edits are history, not requirements - // Worker-authored evidence above the Log must surface: a supervisor caught a worker's - // contradictory evidence block through exactly this event (LUCID3, 2026-09-10). + expect(f.changed()).toBe(0); // Log-only edits are history, not requirements. await f.atomicWrite(signed.replace("## Log", " - evidence: proof.log\n## Log\n- recap: finished")); - await waitFor(() => f.changed() === 1); - f.hooks.get("message_end")({ message: { role: "user", content: f.messages.at(-1).message.content } }); + await waitFor(() => f.messages.some(m => m.message?.content?.includes("[pi-goals: plan activity]"))); + expect(f.changed()).toBe(0); await f.atomicWrite(signed.replace("## Log", "- discriminator: exact bytes and trailing newline\n## Log")); - await waitFor(() => f.changed() === 2); + await waitFor(() => f.changed() === 1); expect(readFileSync(f.path, "utf8")).toContain("[✓] goal: first output"); // Supervisor decides whether changed requirements require reopening. f.hooks.get("message_end")({ message: { role: "user", content: f.messages.at(-1).message.content } }); await f.atomicWrite(signed.replace("[✓] goal: first", "[ ] goal: first")); - await waitFor(() => f.changed() === 3); + await waitFor(() => f.changed() === 2); expect(readFileSync(f.path, "utf8")).toContain("[ ] goal: first output"); }); diff --git a/test/notice-display.test.ts b/test/notice-display.test.ts index 559b7ee..7d2ce2b 100644 --- a/test/notice-display.test.ts +++ b/test/notice-display.test.ts @@ -27,6 +27,10 @@ it("collapses mirrored prompts only in the UI, expands the exact text, and resto const prompt = { role: "custom", customType: "pi-goals-prompt", content, display: true }; expect(renderPrompt(prompt, { expanded: false }, theme).render(80).join("\n")).toContain("Plan changed · review requested"); expect(renderPrompt(prompt, { expanded: true }, theme).render(80)).toEqual(new Markdown(content, 0, 0, getMarkdownTheme()).render(80)); + const activity = "[pi-goals: plan activity]\nTask or evidence bookkeeping changed."; + display.passive(activity); + expect(pi.sendMessage).toHaveBeenLastCalledWith({ customType: "pi-goals-prompt", content: activity, display: true }, { deliverAs: "nextTurn" }); + expect(renderPrompt({ role: "custom", customType: "pi-goals-prompt", content: activity }, { expanded: false }, theme).render(80).join("\n")).toContain("Plan activity recorded"); const entry = { type: "custom", customType: "pi-goals-notice", data: { content } }; const collapsed = render(entry, { expanded: false }, theme); diff --git a/test/plan-view.test.ts b/test/plan-view.test.ts index 7b5bfb7..c621ed7 100644 --- a/test/plan-view.test.ts +++ b/test/plan-view.test.ts @@ -1,13 +1,20 @@ import { expect, it } from "vitest"; import { planViews } from "../src/plan-view.js"; -it.each(["", " "])("ignores %sindented identity bookkeeping and Log edits, but reviews tasks and goals", indent => { - const base = `# Plan\n- [ ] goal: result\n - tasks:\n - [ ] run it\n${indent}- worker session: /saved.jsonl\n## Log\nfirst entry`; - const view = planViews(base).notify; - expect(planViews(base.replace("/saved.jsonl", "/moved.jsonl")).notify).toBe(view); - expect(planViews(base.replace("first entry", "second entry")).notify).toBe(view); - expect(planViews(base.replace("- [ ] run it", "- [x] run it")).notify).not.toBe(view); - expect(planViews(base.replace("[ ] goal: result", "[x] goal: result")).notify).not.toBe(view); +it.each(["", " "])("separates passive %sindented bookkeeping from material plan changes", indent => { + const base = `# Plan\n- preferred worker model: fast/model\n- [ ] goal: result\n - discriminator: output is readable\n - tasks:\n - [ ] run it\n - evidence:\n - old.log\n${indent}- worker session: /saved.jsonl\n## Log\nfirst entry`; + const views = planViews(base); + expect(planViews(base.replace("/saved.jsonl", "/moved.jsonl"))).toEqual(views); + expect(planViews(base.replace("first entry", "second entry"))).toEqual(views); + const task = planViews(base.replace("- [ ] run it", "- [x] run it")); + expect(task.activity).not.toBe(views.activity); + expect(task.notify).toBe(views.notify); + const evidence = planViews(base.replace("old.log", "new.log")); + expect(evidence.activity).not.toBe(views.activity); + expect(evidence.notify).toBe(views.notify); + expect(planViews(base.replace("[ ] goal: result", "[x] goal: result")).notify).not.toBe(views.notify); + expect(planViews(base.replace("output is readable", "output is legible")).notify).not.toBe(views.notify); + expect(planViews(base.replace("fast/model", "strong/model")).notify).not.toBe(views.notify); }); it("uses Log as the boundary even when Interview precedes Goals", () => {