mirror of
https://github.com/wassname/pi-plan.git
synced 2026-07-29 11:25:03 +08:00
sign-off ticks the goal [x] itself + document that the judge reads the working tree, not HEAD
Dogfood exit interview: agent bookkeeping is the drift point (tick after accept was the step most likely forgotten), and 'committed artifact' language implied the judge sees HEAD. Tick is exact-subject match via the existing GOAL_LINE regex; on drift the result explicitly asks the agent to tick, so neither path is silent. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -90,9 +90,11 @@ Latency target came from the SLO review; keep the existing client API.
|
||||
--no-session --no-extensions`, tools `read,bash,grep,find,ls`, edit/write excluded) with the whole
|
||||
plan file and the claimed goal. The judge finds the goal, re-derives from the cited artifacts rather
|
||||
than trusting claims, runs `verify` if the goal names one, and returns `VERDICT: accept | reject`
|
||||
plus what's missing.
|
||||
plus what's missing. It inspects the live working tree, not HEAD: uncommitted work counts, and
|
||||
committing before sign-off is for durable evidence, not for the judge's visibility.
|
||||
|
||||
- accept: a sign-off line is appended to `## Log` and the agent ticks the goal `[x]` itself. The
|
||||
- accept: a sign-off line is appended to `## Log` and the goal is ticked `[x]` in the same write
|
||||
(exact goal-line match only; on wording drift the result asks the agent to tick it). The
|
||||
tool-written log line is the audit trail; a hand-tick without one shows in the diff.
|
||||
- reject: the goal stays open and the agent gets the missing list.
|
||||
- judge ran but failed/errored/timed out, or returned no VERDICT line: accepted inconclusive,
|
||||
|
||||
+33
-7
@@ -15,8 +15,9 @@
|
||||
*
|
||||
* The judge subsumes what v1 did in code: goal matching (no findGoal), evidence validation (a
|
||||
* placeholder gets rejected in words), verify execution, and format reading. The extension's only
|
||||
* write is appending a sign-off line to ## Log — the audit trail. The agent ticks [x] itself; a
|
||||
* hand-tick without a matching tool-written log line is visible in the diff.
|
||||
* writes are the sign-off: append a log line to ## Log (the audit trail) and tick the goal [x] when
|
||||
* an exact goal line matches (on drift the agent ticks, and the result says so). A hand-tick
|
||||
* without a matching tool-written log line is visible in the diff either way.
|
||||
*
|
||||
* Judge ran but failed/errored/timed out, or returned no VERDICT line => accepted_inconclusive: the
|
||||
* working agent is never blocked on judge infra; the log line says the judge ran but failed. There
|
||||
@@ -233,8 +234,21 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
(task) => runJudge(task, judgeModel, ctx.cwd, signal),
|
||||
);
|
||||
if (outcome.logEntry) {
|
||||
writePlan(ctx, appendLog(readPlan(ctx), `${stamp()} ${outcome.logEntry}`));
|
||||
// Sign-off write: tick the goal [x] (exact-subject match; dogfood showed agent bookkeeping
|
||||
// is the drift point) and append the audit log line, one write. On wording drift the tick
|
||||
// falls to the agent and the result says so -- both paths are explicit, never silent.
|
||||
let updated = readPlan(ctx);
|
||||
let tickNote = "";
|
||||
if (outcome.logEntry.startsWith("signed off")) {
|
||||
const ticked = tickGoal(updated, params.goal);
|
||||
updated = ticked ?? updated;
|
||||
tickNote = ticked
|
||||
? `\n\nGoal ticked [x] in ${PLAN_REL}.`
|
||||
: `\n\nNo exact goal line matched your wording -- tick it [x] in ${PLAN_REL} yourself.`;
|
||||
}
|
||||
writePlan(ctx, appendLog(updated, `${stamp()} ${outcome.logEntry}`));
|
||||
updateWidget(ctx);
|
||||
return result(outcome.resultText + tickNote, outcome.isError);
|
||||
}
|
||||
return result(outcome.resultText, outcome.isError);
|
||||
},
|
||||
@@ -300,7 +314,7 @@ export async function decideSignOff(
|
||||
if (judge.error) {
|
||||
const partial = judge.output ? `\n\npartial judge output:\n${judge.output}` : "";
|
||||
return {
|
||||
resultText: `Judge ran but failed (${judge.error}). Accepted inconclusive — logged. Tick the goal [x] in ${PLAN_REL} yourself.${partial}`,
|
||||
resultText: `Judge ran but failed (${judge.error}). Accepted inconclusive — logged.${partial}`,
|
||||
isError: false,
|
||||
logEntry: `signed off "${input.goal}" (judge inconclusive: ran but failed: ${oneLine(judge.error)})`,
|
||||
};
|
||||
@@ -312,7 +326,7 @@ export async function decideSignOff(
|
||||
|
||||
if (verdict === "accept") {
|
||||
return {
|
||||
resultText: `Sign-off ACCEPTED. Tick the goal [x] in ${PLAN_REL} (the log line is already appended).\n\n--- judge ---\n${reasoning}`,
|
||||
resultText: `Sign-off ACCEPTED (log line appended).\n\n--- judge ---\n${reasoning}`,
|
||||
isError: false,
|
||||
logEntry: `signed off "${input.goal}" (judge accept)`,
|
||||
};
|
||||
@@ -327,13 +341,25 @@ export async function decideSignOff(
|
||||
}
|
||||
// No VERDICT line: same fail-forward as a judge error -- the judge ran but didn't answer.
|
||||
return {
|
||||
resultText: `Judge returned no VERDICT line. Accepted inconclusive — logged. Tick the goal [x] in ${PLAN_REL} yourself.\n\n--- judge ---\n${reasoning || "(no output)"}`,
|
||||
resultText: `Judge returned no VERDICT line. Accepted inconclusive — logged.\n\n--- judge ---\n${reasoning || "(no output)"}`,
|
||||
isError: false,
|
||||
logEntry: `signed off "${input.goal}" (judge inconclusive: no VERDICT line)`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Append one line under ## Log (creating the section at EOF if absent). The extension's only write. */
|
||||
/** Tick the goal line whose subject exactly matches `goal` (trimmed, case-insensitive) to [x].
|
||||
* Null when there is no unique exact match (wording drift / duplicates) -- the caller then asks the
|
||||
* agent to tick it itself. Reuses GOAL_LINE; deliberately NOT fuzzy, that's the judge's job. */
|
||||
export function tickGoal(plan: string, goal: string): string | null {
|
||||
const lines = plan.split("\n");
|
||||
const want = goal.trim().toLowerCase();
|
||||
const hits = lines.flatMap((l, i) => (GOAL_LINE.exec(l)?.[2].trim().toLowerCase() === want ? [i] : []));
|
||||
if (hits.length !== 1) return null;
|
||||
lines[hits[0]] = lines[hits[0]].replace(/\[[ xX/-]\]/, "[x]");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Append one line under ## Log (creating the section at EOF if absent). */
|
||||
export function appendLog(text: string, entry: string): string {
|
||||
const lines = text.split("\n");
|
||||
const line = `- ${entry}`;
|
||||
|
||||
+5
-3
@@ -96,9 +96,11 @@ export const completeGoalDescription =
|
||||
"table plus how to read it, a metric plus what it shows -- not a bare claim). The read must show " +
|
||||
"success POSITIVELY happened, not just that failures were avoided. Then call this with the goal's " +
|
||||
"text (the line after 'goal:'; small wording drift is fine). A fresh read-only judge inspects the " +
|
||||
"repo, runs the goal's verify command if it names one, and returns accept or reject with what's " +
|
||||
"missing. On accept (or if the judge itself is unavailable), a sign-off line is appended to ## Log " +
|
||||
"and YOU tick the goal [x]. On reject the goal stays open.";
|
||||
"LIVE WORKING TREE (uncommitted changes included; committing first is for durability, not " +
|
||||
"visibility), runs the goal's verify command if it names one, and returns accept or reject with " +
|
||||
"what's missing. On accept (or if the judge itself failed), a sign-off line is appended to ## Log " +
|
||||
"and the goal is ticked [x] for you; the result says if you must tick it yourself. On reject the " +
|
||||
"goal stays open.";
|
||||
|
||||
export const completeGoalParamDescription = "The goal's text: the line after 'goal:' in the plan file.";
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tickGoal } from "../src/index.js";
|
||||
|
||||
const plan = `# Plan
|
||||
|
||||
## Goals
|
||||
|
||||
1. [/] goal: Implement the cache layer
|
||||
- tasks:
|
||||
1. [x] wire client
|
||||
2. [ ] goal: Ship the docs
|
||||
|
||||
## Log
|
||||
`;
|
||||
|
||||
describe("tickGoal (sign-off ticks the goal; agent only ticks on wording drift)", () => {
|
||||
it("ticks the exact-matching goal line, case-insensitive, leaving subtasks alone", () => {
|
||||
const out = tickGoal(plan, "implement the CACHE layer");
|
||||
expect(out).toContain("1. [x] goal: Implement the cache layer");
|
||||
expect(out).toContain("1. [x] wire client"); // subtask untouched (was already x)
|
||||
expect(out).toContain("2. [ ] goal: Ship the docs"); // other goal untouched
|
||||
});
|
||||
|
||||
it("returns null on wording drift (fuzzy matching is the judge's job, not TypeScript's)", () => {
|
||||
expect(tickGoal(plan, "Implement caching")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the subject matches more than one goal line", () => {
|
||||
const dup = `${plan}3. [ ] goal: Ship the docs\n`;
|
||||
expect(tickGoal(dup, "Ship the docs")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user