pi-goal-list-loop-audit 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -478,6 +478,75 @@ export function countOpenAuditFindings(cwd: string): number {
|
|
|
478
478
|
* reasonable answers exist) are presented, never touched. DECIDE lines use
|
|
479
479
|
* "- [?]" so they never inflate the loop's open-findings measure.
|
|
480
480
|
*/
|
|
481
|
+
/** v0.31.0: /list audit — the collect-then-drain audit (user design
|
|
482
|
+
* 2026-07-31: "this command could run a project audit, collect a bunch of
|
|
483
|
+
* tasks, then do them all too"). Division of labor:
|
|
484
|
+
* /goal audit — one audited unit: audit + fix in the SAME pass (small scopes).
|
|
485
|
+
* /list audit — audit once, then every open finding becomes its own queued,
|
|
486
|
+
* individually-audited list item (the actionables stop living
|
|
487
|
+
* only inside findings.md — the user's "audits don't make a
|
|
488
|
+
* list of actionables" pain).
|
|
489
|
+
* /loop audit — forever fix-first cadence for living codebases.
|
|
490
|
+
* The collection item FIXES NOTHING: every fix lands as its own list item with
|
|
491
|
+
* its own isolated audit trail. DECIDE findings are presented at collection
|
|
492
|
+
* completion, never queued — a decision is not a task (the agent can't "do" it).
|
|
493
|
+
* The marker survives restarts inside the objective itself (no schema change):
|
|
494
|
+
* the completion fan-out matches on it.
|
|
495
|
+
*/
|
|
496
|
+
export const LIST_AUDIT_COLLECT_MARKER = "[LIST-AUDIT-COLLECT]";
|
|
497
|
+
|
|
498
|
+
export function listAuditCollectTarget(focus?: string): string {
|
|
499
|
+
const scope = focus && focus.trim() ? focus.trim() : "the whole project";
|
|
500
|
+
return `${LIST_AUDIT_COLLECT_MARKER} Run ONE project audit pass that COLLECTS work — the follow-up fixes are queued as separate list items, so this pass changes no code. Scope: ${scope}. (1) Run a FRESH audit pass over the codebase — spawn Explore subagents for breadth — hunting real problems: bugs, broken flows, regressions, drift between docs and code, dead code, security holes. Not style nits, not speculative refactors. (2) Append every NEW finding to ${AUDIT_FINDINGS_REL} (create the file on the first finding; append-only — never delete, rewrite, or reorder existing lines; never re-report a finding already listed), classified: "- [ ] FIX: SEVERITY: short description (file:line)" for bugs and polish — and "- [?] DECIDE: short description (what the choice is, what each side costs)" for direction, trade-offs, and scope questions where two reasonable answers exist. (3) Change NOTHING — no fixes, no refactors, no drive-by edits: the orchestrator queues each open FIX finding as its own list item after this pass completes, and each fix lands with its own commit and its own audit. (4) DECIDE findings are listed in the completion report — they are presented to the user, never queued and never silently fixed. (5) Honesty law: never fabricate findings to look busy; if the pass is genuinely clean, say so plainly — an empty findings set is a success, not a failure. Done when: the audit pass is complete and every finding it surfaced is appended to ${AUDIT_FINDINGS_REL} with the right classification (or the report states plainly that nothing was found).`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** One parsed open finding from the audit findings file. */
|
|
504
|
+
export interface AuditFindingLine {
|
|
505
|
+
/** Raw finding text with the checkbox + optional "FIX:" prefix stripped. */
|
|
506
|
+
text: string;
|
|
507
|
+
/** Severity rank for sorting: 0 = CRITICAL … 4 = unclassified. */
|
|
508
|
+
rank: number;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const AUDIT_SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
512
|
+
|
|
513
|
+
/** v0.31.0: parse findings.md into the fan-out shape — OPEN boxes become
|
|
514
|
+
* actionable findings (severity-sorted, stable within a rank), DECIDE boxes
|
|
515
|
+
* ("- [?]") are returned separately for presentation (never queued). Tolerates
|
|
516
|
+
* the /loop audit format (no "FIX:" prefix): any open box that isn't a
|
|
517
|
+
* decision is actionable.
|
|
518
|
+
*/
|
|
519
|
+
export function parseAuditFindingsForFanout(md: string): { open: AuditFindingLine[]; decisions: string[] } {
|
|
520
|
+
const open: AuditFindingLine[] = [];
|
|
521
|
+
const decisions: string[] = [];
|
|
522
|
+
md.split("\n").forEach((line, idx) => {
|
|
523
|
+
const decide = line.match(/^\s*-\s*\[\?\]\s*(.*)$/);
|
|
524
|
+
if (decide) {
|
|
525
|
+
const text = (decide[1] ?? "").replace(/^DECIDE:\s*/i, "").trim();
|
|
526
|
+
if (text) decisions.push(text);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const box = line.match(/^\s*-\s*\[ \]\s*(.*)$/);
|
|
530
|
+
if (!box) return;
|
|
531
|
+
const text = (box[1] ?? "").replace(/^FIX:\s*/i, "").trim();
|
|
532
|
+
if (!text) return;
|
|
533
|
+
const sev = text.match(/^([A-Z]+)\s*:/);
|
|
534
|
+
const rank = sev ? AUDIT_SEVERITY_ORDER.indexOf(sev[1]!) : -1;
|
|
535
|
+
open.push({ text, rank: rank >= 0 ? rank : AUDIT_SEVERITY_ORDER.length + (idx / 100000) });
|
|
536
|
+
});
|
|
537
|
+
// Severity first; stable within a rank (file order) via the fractional idx.
|
|
538
|
+
open.sort((a, b) => a.rank - b.rank);
|
|
539
|
+
return { open, decisions };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** v0.31.0: the list-item text for one finding — short objective + a checkable
|
|
543
|
+
* Done when (the fix commit exists AND the box is checked with its hash, so
|
|
544
|
+
* findings.md stays honest as the drain proceeds).
|
|
545
|
+
*/
|
|
546
|
+
export function listAuditFanoutItemText(finding: string): string {
|
|
547
|
+
return `Fix audit finding: ${finding} — Done when: the fix is committed on the current branch with the repo's configured identity, and this finding's box in ${AUDIT_FINDINGS_REL} is checked ("- [x] … — fixed in <commit>").`;
|
|
548
|
+
}
|
|
549
|
+
|
|
481
550
|
export function projectAuditTarget(focus?: string): string {
|
|
482
551
|
const scope = focus && focus.trim() ? focus.trim() : "the whole project";
|
|
483
552
|
return `Run ONE project audit pass and leave the project in a known state. Scope: ${scope}. (1) Run a FRESH audit pass over the codebase — spawn Explore subagents for breadth — hunting real problems: bugs, broken flows, regressions, drift between docs and code, dead code, security holes. Not style nits, not speculative refactors. (2) Append every NEW finding to ${AUDIT_FINDINGS_REL} (create the file on the first finding; append-only — never delete, rewrite, or reorder existing lines; never re-report a finding already listed), classified: "- [ ] FIX: SEVERITY: short description (file:line)" for bugs and polish — whether to fix these is NOT a decision — and "- [?] DECIDE: short description (what the choice is, what each side costs)" for direction, trade-offs, and scope questions where two reasonable answers exist. (3) Fix every NEW FIX finding from this pass — real fixes, committed with the repo's configured identity on the current branch (no invented identities or branches) — then check the box: "- [x] … — fixed in <commit>". (4) Change NOTHING for DECIDE findings — present them in the completion report instead. (5) Honesty law: never fabricate findings to look busy; never check a box without the fix commit existing; never silently turn a DECIDE into a fix. Done when: the audit pass is complete, every new FIX finding has a fix commit and a checked box in ${AUDIT_FINDINGS_REL}, and every DECIDE finding is listed in the file and presented in the completion report.`;
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -175,6 +175,10 @@ import {
|
|
|
175
175
|
countOpenAuditFindings,
|
|
176
176
|
AUDIT_FINDINGS_REL,
|
|
177
177
|
projectAuditTarget,
|
|
178
|
+
LIST_AUDIT_COLLECT_MARKER,
|
|
179
|
+
listAuditCollectTarget,
|
|
180
|
+
parseAuditFindingsForFanout,
|
|
181
|
+
listAuditFanoutItemText,
|
|
178
182
|
type LoopTickOutcome,
|
|
179
183
|
HELD_ON_RESTORE,
|
|
180
184
|
type LoopState,
|
|
@@ -1228,6 +1232,62 @@ function autoArbitrateStackedState(ctx: ExtensionContext): void {
|
|
|
1228
1232
|
);
|
|
1229
1233
|
}
|
|
1230
1234
|
|
|
1235
|
+
/** v0.31.0: /list audit completion fan-out — read the audit findings file,
|
|
1236
|
+
* queue every OPEN finding as its own list item (severity-sorted, deduped
|
|
1237
|
+
* against the live queue), present DECIDE findings without queueing them.
|
|
1238
|
+
* Confirm-gated like every bulk import (v0.23.7: the user reads what lands
|
|
1239
|
+
* in the queue); a decline leaves the findings open for a later re-run.
|
|
1240
|
+
*/
|
|
1241
|
+
async function fanOutListAuditFindings(ctx: ExtensionContext): Promise<void> {
|
|
1242
|
+
let md = "";
|
|
1243
|
+
try {
|
|
1244
|
+
md = fs.readFileSync(path.join(ctx.cwd, AUDIT_FINDINGS_REL), "utf-8");
|
|
1245
|
+
} catch {
|
|
1246
|
+
/* no findings file — the audit was clean or never wrote */
|
|
1247
|
+
}
|
|
1248
|
+
const { open, decisions } = parseAuditFindingsForFanout(md);
|
|
1249
|
+
// Dedupe against the live queue (a re-run must not double-queue a finding
|
|
1250
|
+
// that's already waiting) — match on the finding text's first 60 chars.
|
|
1251
|
+
const queuedText = listQueue().map((i) => i.objective).join("\n");
|
|
1252
|
+
const fresh = open.filter((f) => !queuedText.includes(f.text.slice(0, 60)));
|
|
1253
|
+
const alreadyQueued = open.length - fresh.length;
|
|
1254
|
+
const decideNote =
|
|
1255
|
+
decisions.length > 0
|
|
1256
|
+
? `\n${decisions.length} DECIDE finding(s) need YOU (not queued — a decision is not a task):\n` +
|
|
1257
|
+
decisions.slice(0, 10).map((d) => ` ? ${d.slice(0, 110)}`).join("\n")
|
|
1258
|
+
: "";
|
|
1259
|
+
if (fresh.length === 0) {
|
|
1260
|
+
ctx.ui.notify(
|
|
1261
|
+
open.length > 0
|
|
1262
|
+
? `Audit collected ${open.length} open finding(s) — all already queued.${decideNote}`
|
|
1263
|
+
: `Audit complete — no open findings; the project is clean, nothing to queue.${decideNote}`,
|
|
1264
|
+
"info",
|
|
1265
|
+
);
|
|
1266
|
+
appendLedger(ctx.cwd, "list_audit_fanout_empty", { open: open.length, decisions: decisions.length });
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const preview = fresh.map((f, i) => ` ${i + 1}. ${f.text.slice(0, 110)}`).join("\n");
|
|
1270
|
+
let confirmed = true;
|
|
1271
|
+
if (ctx.hasUI) {
|
|
1272
|
+
try {
|
|
1273
|
+
confirmed = await ctx.ui.confirm(`Queue ${fresh.length} audit finding(s) as list items?`, preview);
|
|
1274
|
+
} catch {
|
|
1275
|
+
confirmed = false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (!confirmed) {
|
|
1279
|
+
appendLedger(ctx.cwd, "list_audit_fanout_declined", { findings: fresh.length });
|
|
1280
|
+
ctx.ui.notify(`Fan-out declined — the findings stay open in ${AUDIT_FINDINGS_REL}; /list audit re-queues them any time.`, "info");
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const n = enqueueItems(ctx, fresh.map((f) => listAuditFanoutItemText(f.text)), "list audit fan-out");
|
|
1284
|
+
appendLedger(ctx.cwd, "list_audit_fanout", { queued: n, alreadyQueued, decisions: decisions.length });
|
|
1285
|
+
ctx.ui.notify(
|
|
1286
|
+
`Queued ${n} finding(s) — the list drains them fix by fix, each with its own audited commit.${alreadyQueued > 0 ? ` (${alreadyQueued} already queued.)` : ""}${decideNote}`,
|
|
1287
|
+
"info",
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1231
1291
|
function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?: string): void {
|
|
1232
1292
|
if (!state.goal) return;
|
|
1233
1293
|
const goal = state.goal;
|
|
@@ -1253,9 +1313,15 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
|
|
|
1253
1313
|
// (v0.2.0 bug: bare /list next silently consumed TWO items, found by the
|
|
1254
1314
|
// pick-any-item verification in v0.10.0).
|
|
1255
1315
|
if (goal.policy === "list" && status === "complete") {
|
|
1316
|
+
// v0.31.0: a /list audit collection item completed → fan the open
|
|
1317
|
+
// findings out into the queue (async — Confirm-gated). When the queue
|
|
1318
|
+
// was empty, enqueueItems activates the first fix itself, so the
|
|
1319
|
+
// list-complete / reviewer noise below must NOT fire for this item.
|
|
1320
|
+
const isListAuditCollect = goal.objective.includes(LIST_AUDIT_COLLECT_MARKER);
|
|
1321
|
+
if (isListAuditCollect) void fanOutListAuditFindings(ctx);
|
|
1256
1322
|
const advanced = activateNextListItem(ctx);
|
|
1257
1323
|
// v0.26.0: the queue just EMPTIED on a completion → list-complete.
|
|
1258
|
-
if (!advanced) {
|
|
1324
|
+
if (!advanced && !isListAuditCollect) {
|
|
1259
1325
|
fireReviewer(ctx, { kind: "list", goalId: goal.id, objective: goal.objective, terminal: "goal-complete" });
|
|
1260
1326
|
// v0.29.0: the well ran dry — point at the project-audit loop. A
|
|
1261
1327
|
// suggestion, not an action: consent, never auto-start (v0.28.28).
|
|
@@ -2084,6 +2150,26 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2084
2150
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
2085
2151
|
const rest = args.trim().slice(sub.length).trim();
|
|
2086
2152
|
|
|
2153
|
+
if (sub === "audit") {
|
|
2154
|
+
// v0.31.0: /list audit [focus] — collect-then-drain (user design
|
|
2155
|
+
// 2026-07-31: "run a project audit, collect a bunch of tasks, then do
|
|
2156
|
+
// them all too"). The audit item COLLECTS findings (changes no code);
|
|
2157
|
+
// its completion fans each open finding out into the queue and the
|
|
2158
|
+
// list drains them fix by fix, each with its own isolated audit.
|
|
2159
|
+
// Distinct from /goal audit (fix-in-pass, one audited unit) and
|
|
2160
|
+
// /loop audit (forever fix-first cadence).
|
|
2161
|
+
const objective = listAuditCollectTarget(rest || undefined);
|
|
2162
|
+
const n = enqueueItems(ctx, [objective], "/list audit");
|
|
2163
|
+
if (n === 0) return; // zombie-twin guard already explained itself
|
|
2164
|
+
ctx.ui.notify(
|
|
2165
|
+
"Audit collection item queued — it CHANGES NO CODE: it appends findings to " +
|
|
2166
|
+
AUDIT_FINDINGS_REL +
|
|
2167
|
+
", and on completion each open finding becomes its own list item (fixes drain one audited commit at a time). DECIDE findings are presented to you, never queued.",
|
|
2168
|
+
"info",
|
|
2169
|
+
);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2087
2173
|
if (sub === "depth") {
|
|
2088
2174
|
// v0.25.3: long-running state at a glance — queue depth, oldest item
|
|
2089
2175
|
// age, average item duration from archived list-policy goals.
|
|
@@ -5382,9 +5468,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5382
5468
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdReview(args, ctx); },
|
|
5383
5469
|
});
|
|
5384
5470
|
pi.registerCommand("list", {
|
|
5385
|
-
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear | /list cancel",
|
|
5471
|
+
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list audit [focus] (collect findings, then drain them as items) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear | /list cancel",
|
|
5386
5472
|
getArgumentCompletions: completions([
|
|
5387
5473
|
["show", "display the waiting items"],
|
|
5474
|
+
["audit", "collect-then-drain: audit the project, queue every finding as its own item"],
|
|
5388
5475
|
["resume", "resume the paused list item (the list's head)"],
|
|
5389
5476
|
["next", "activate the next item (or /list next <n> for position n)"],
|
|
5390
5477
|
["remove", "remove an item: /list remove <n>"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|