mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Notify on goal, task and evidence plan edits; stay silent on identity bookkeeping
Field calibration from three supervisor reports: the useful plan-change catch was a worker-ticked task the short-view hash no longer surfaced, while identity-line edits caused duplicate empty reviews. The notify digest keeps goals, tasks and evidence above the Log and drops worker identity lines. Reopening a signed goal still clears its sign-off. Co-Authored-By: Pi/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
+4
-4
@@ -126,7 +126,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
|||||||
clearTimeout(planEditTimer);
|
clearTimeout(planEditTimer);
|
||||||
planEditTimer = undefined;
|
planEditTimer = undefined;
|
||||||
const snapshot = readPlan();
|
const snapshot = readPlan();
|
||||||
if (snapshot.text !== undefined) planHash = digest(planViews(snapshot.text).short);
|
if (snapshot.text !== undefined) planHash = digest(planViews(snapshot.text).notify);
|
||||||
if (state.child || state.mode !== "supervising" || !state.plan) return;
|
if (state.child || state.mode !== "supervising" || !state.plan) return;
|
||||||
const stamp = generation;
|
const stamp = generation;
|
||||||
// Watch the directory so atomic plan replacement remains observable. This is an event hook:
|
// Watch the directory so atomic plan replacement remains observable. This is an event hook:
|
||||||
@@ -143,7 +143,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
|||||||
const snapshot = readPlan();
|
const snapshot = readPlan();
|
||||||
if (snapshot.text === undefined) { ctx.ui.notify(snapshot.error!, "warning"); return; }
|
if (snapshot.text === undefined) { ctx.ui.notify(snapshot.error!, "warning"); return; }
|
||||||
refresh(ctx);
|
refresh(ctx);
|
||||||
const hash = digest(planViews(snapshot.text).short);
|
const hash = digest(planViews(snapshot.text).notify);
|
||||||
if (hash === planHash) return;
|
if (hash === planHash) return;
|
||||||
planHash = hash;
|
planHash = hash;
|
||||||
notice = true;
|
notice = true;
|
||||||
@@ -340,7 +340,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
|||||||
if (found >= 0) lines[found] = pref;
|
if (found >= 0) lines[found] = pref;
|
||||||
else { const title = lines.findIndex((line) => /^#\s/.test(line)); lines.splice(title >= 0 ? title + 1 : 0, 0, 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"));
|
writeFileSync(state.plan, lines.join("\n"));
|
||||||
planHash = digest(planViews(planText()).short);
|
planHash = digest(planViews(planText()).notify);
|
||||||
refresh(ctx);
|
refresh(ctx);
|
||||||
ctx.ui.notify(ref ? `Preferred worker model set to ${ref} in plan preferences. The supervisor selects it at launch and verifies the resolved model; the worker pane's own model is chosen with /model in that pane.` : "Preferred worker model cleared.", "info");
|
ctx.ui.notify(ref ? `Preferred worker model set to ${ref} in plan preferences. The supervisor selects it at launch and verifies the resolved model; the worker pane's own model is chosen with /model in that pane.` : "Preferred worker model cleared.", "info");
|
||||||
return;
|
return;
|
||||||
@@ -437,7 +437,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
|||||||
lines.splice(log + 1, 0, "", completionLog(params.goal, params.observation, evidence, state.mode === "solo"));
|
lines.splice(log + 1, 0, "", completionLog(params.goal, params.observation, evidence, state.mode === "solo"));
|
||||||
writeFileSync(state.plan, `${lines.join("\n").trimEnd()}\n`);
|
writeFileSync(state.plan, `${lines.join("\n").trimEnd()}\n`);
|
||||||
state.signoffs[key(matches[0].subject)] = { evidence, observation: params.observation };
|
state.signoffs[key(matches[0].subject)] = { evidence, observation: params.observation };
|
||||||
planHash = digest(planViews(planText()).short);
|
planHash = digest(planViews(planText()).notify);
|
||||||
save(); refresh(ctx);
|
save(); refresh(ctx);
|
||||||
const remaining = goals(planText()).some((goal) => goal.status !== "cancelled" && (goal.status !== "done" || !state.signoffs[key(goal.subject)]));
|
const remaining = goals(planText()).some((goal) => goal.status !== "cancelled" && (goal.status !== "done" || !state.signoffs[key(goal.subject)]));
|
||||||
return result(completionResult(matches[0].subject, ctx.sessionManager.getSessionId(), remaining, state.mode === "solo"));
|
return result(completionResult(matches[0].subject, ctx.sessionManager.getSessionId(), remaining, state.mode === "solo"));
|
||||||
|
|||||||
+7
-2
@@ -1,6 +1,11 @@
|
|||||||
// Pi/OpenAI: Preserve plan wording; omit history and, in the short view, task/evidence details.
|
// Pi/OpenAI: Preserve plan wording; omit history and, in the short view, task/evidence details.
|
||||||
export function planViews(plan: string): { short: string; long: string } {
|
// The notify view governs plan-change events: goals, tasks, evidence and inferences are
|
||||||
|
// content worth a supervisor review; worker identity bookkeeping is not (field report,
|
||||||
|
// LUCID3 supervisor 2026-09-10: two identical review events for a session-path edit).
|
||||||
|
export function planViews(plan: string): { short: string; notify: string; long: string } {
|
||||||
const long = plan.split(/^#{1,6}\s+(?:Log|Appendix|Appendices|Appendixes|Interview|Learnings|Papercuts)\b.*$/mi)[0].trim();
|
const long = plan.split(/^#{1,6}\s+(?:Log|Appendix|Appendices|Appendixes|Interview|Learnings|Papercuts)\b.*$/mi)[0].trim();
|
||||||
|
const identity = /^-\s*(?:active worker|worker session|worker intercom session):/i;
|
||||||
|
const notify = long.split("\n").filter((line) => !identity.test(line)).join("\n").trim();
|
||||||
const kept: string[] = [];
|
const kept: string[] = [];
|
||||||
let omittedIndent: number | null = null;
|
let omittedIndent: number | null = null;
|
||||||
let omittedHeading: number | null = null;
|
let omittedHeading: number | null = null;
|
||||||
@@ -24,5 +29,5 @@ export function planViews(plan: string): { short: string; long: string } {
|
|||||||
}
|
}
|
||||||
kept.push(line);
|
kept.push(line);
|
||||||
}
|
}
|
||||||
return { short: kept.join("\n").trim(), long };
|
return { short: kept.join("\n").trim(), notify, long };
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -447,18 +447,22 @@ it.each(["missing", "empty", "directory"])("%s plan snapshots never erase signof
|
|||||||
expect(f.changed()).toBe(0);
|
expect(f.changed()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores post-completion maintenance but reviews a real requirement or manual reopening", async () => {
|
it("ignores post-completion maintenance but reviews evidence, requirement or manual reopening changes", async () => {
|
||||||
const f = fixture(); await f.draft(); await f.command("ready");
|
const f = fixture(); await f.draft(); await f.command("ready");
|
||||||
writeFileSync(join(f.ctx.cwd, "proof.log"), "PASS\n");
|
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);
|
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");
|
const signed = readFileSync(f.path, "utf8");
|
||||||
await f.atomicWrite(signed.replace("## Log", " - evidence: proof.log\n## Log\n- recap: finished"));
|
await f.atomicWrite(signed.replace("## Log", "## Log\n- recap: finished"));
|
||||||
await delay(250);
|
await delay(250);
|
||||||
expect(f.changed()).toBe(0);
|
expect(f.changed()).toBe(0); // Log-only edits are history, not requirements
|
||||||
await f.atomicWrite(signed.replace("## Log", "- discriminator: exact bytes and trailing newline\n## Log"));
|
// 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).
|
||||||
|
await f.atomicWrite(signed.replace("## Log", " - evidence: proof.log\n## Log\n- recap: finished"));
|
||||||
await waitFor(() => f.changed() === 1);
|
await waitFor(() => f.changed() === 1);
|
||||||
await f.atomicWrite(signed.replace("[x] goal: first", "[ ] goal: first"));
|
await f.atomicWrite(signed.replace("## Log", "- discriminator: exact bytes and trailing newline\n## Log"));
|
||||||
await waitFor(() => f.changed() === 2);
|
await waitFor(() => f.changed() === 2);
|
||||||
|
await f.atomicWrite(signed.replace("[x] goal: first", "[ ] goal: first"));
|
||||||
|
await waitFor(() => f.changed() === 3);
|
||||||
expect(f.entries.at(-1).data.signoffs["first output"]).toBeUndefined();
|
expect(f.entries.at(-1).data.signoffs["first output"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ it("omits only named worker identity fields from review while retaining them in
|
|||||||
expect(planViews(base.replace("[ ]", "[x]")).short).not.toBe(planViews(base).short);
|
expect(planViews(base.replace("[ ]", "[x]")).short).not.toBe(planViews(base).short);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("notifies on goal and task changes but not on identity bookkeeping or log edits", () => {
|
||||||
|
const base = "# Plan\n- [ ] goal: result\n## Task list\n- [ ] run it\n- worker session: /saved.jsonl\n## Log\nfirst entry";
|
||||||
|
const baseView = planViews(base).notify;
|
||||||
|
// identity bookkeeping: silent
|
||||||
|
expect(planViews(base.replace("/saved.jsonl", "/moved.jsonl")).notify).toBe(baseView);
|
||||||
|
// log edits: silent
|
||||||
|
expect(planViews(base.replace("first entry", "second entry")).notify).toBe(baseView);
|
||||||
|
// worker ticking a task: review event (field catch, LUCID3 2026-09-10)
|
||||||
|
expect(planViews(base.replace("- [ ] run it", "- [x] run it")).notify).not.toBe(baseView);
|
||||||
|
// goal edits: review event
|
||||||
|
expect(planViews(base.replace("[ ] goal: result", "[x] goal: result")).notify).not.toBe(baseView);
|
||||||
|
});
|
||||||
|
|
||||||
it("stops at history and preserves a manual goal tick", () => {
|
it("stops at history and preserves a manual goal tick", () => {
|
||||||
const view = planViews("# Plan\n1. [x] goal: result\n## Log\n1. [ ] goal: historical");
|
const view = planViews("# Plan\n1. [x] goal: result\n## Log\n1. [ ] goal: historical");
|
||||||
expect(view.short).toContain("[x] goal: result");
|
expect(view.short).toContain("[x] goal: result");
|
||||||
|
|||||||
Reference in New Issue
Block a user