pi-goal-list-loop-audit 0.25.2 → 0.25.3

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.
@@ -922,3 +922,125 @@ export function lastShippedAtMs(cwd: string): number | null {
922
922
  }
923
923
  return best;
924
924
  }
925
+
926
+ // =================================================================
927
+ // v0.25.3: list-philosophy rework — cross-mode recommendation +
928
+ // /list depth rollups
929
+ // =================================================================
930
+
931
+ /**
932
+ * Detect a mode mismatch between what the user described and the mode
933
+ * they invoked. Returns a recommendation string for the drafting
934
+ * injection, or undefined when the seed fits the mode.
935
+ *
936
+ * The canonical failure this prevents (real incidents 2026-07-24):
937
+ * "close 76 weak points, one commit each" folded into ONE wrapper goal
938
+ * with an aggregate "≥ 76 commits" contract → auto-committer squash →
939
+ * literal count fails → auditor correctly disapproves finished work.
940
+ */
941
+ export function crossRecommendMode(seed: string, mode: "goal" | "list"): string | undefined {
942
+ const s = seed.trim();
943
+ if (!s) return undefined;
944
+ // Aggregate seed: "N items/findings/weak points/screens/todos/fixes"
945
+ // (+ "each" / "one commit" flavor) — the wrapper-goal anti-pattern.
946
+ const aggregate = s.match(/(\d+)\s*(?:items?|findings?|weak[\s-]points?|screens?|todos?|fix(?:es)?|tasks?|issues?)/i);
947
+ const n = aggregate ? Number(aggregate[1]) : 0;
948
+ if (n >= 5) {
949
+ return (
950
+ `[MODE CHECK — this seed names ${n} discrete items${/each|one commit|as a tasklist/i.test(s) ? ' ("each"/"tasklist" phrasing)' : ""}. ` +
951
+ `Do NOT fold them into ONE wrapper ${mode === "list" ? "list item" : "goal"} with an aggregate contract ("≥ ${n} commits") — ` +
952
+ `the auto-committer squashes commits and the literal count fails even when the work is done (the 2026-07-24 76-weak-points incident). ` +
953
+ `Propose ${n} SHORT /list items via propose_goal_draft items[] — each item closes exactly ONE finding with its own per-item contract. ` +
954
+ `Any aggregate re-audit becomes the FINAL /goal, not the first.]`
955
+ );
956
+ }
957
+ if (mode === "list") {
958
+ if (/\b(?:take|takes|taking)\s+(?:a\s+)?(?:few|several|\d+)\s+hours?\b/i.test(s) || /\b(?:multi-hour|deep (?:audit|research|dive)|all day|over the weekend)\b/i.test(s)) {
959
+ return (
960
+ `[MODE CHECK — this seed sounds like multi-hour work. /list items are SHORT (minutes, one focused change). ` +
961
+ `Either break it into ≤ 30-minute items via items[], or tell the user this fits /goal better — one big task, ` +
962
+ `ends on auditor approval. If the user overrides ("as a list item anyway"), comply.]`
963
+ );
964
+ }
965
+ } else {
966
+ if (/^(?:fix|typo|rename|bump|remove|delete|clean ?up|tweak)\b/i.test(s) && s.length < 80 && !/\bhours?\b|\ball\b|\bevery\b|\beach\b/i.test(s)) {
967
+ return (
968
+ `[MODE CHECK — this seed sounds like a five-minute cleanup. A full audited /goal may be overkill; ` +
969
+ `suggest /list (queue of short items) or the tasklist plugin. If the user wants the audit anyway, comply.]`
970
+ );
971
+ }
972
+ }
973
+ return undefined;
974
+ }
975
+
976
+ /** /list depth rollup: how deep is the queue, how stale is the head,
977
+ * how long do items actually take (from archived list-policy goals). */
978
+ export interface ListDepthStats {
979
+ queueDepth: number;
980
+ oldestItemId?: string;
981
+ oldestAgeMs?: number;
982
+ avgDurationMs?: number;
983
+ durationSamples: number;
984
+ }
985
+
986
+ export function computeListDepth(
987
+ queue: Array<{ id: string; addedAt: string }>,
988
+ ledgerEntries: Array<{ type: string; value?: any }>,
989
+ nowMs: number,
990
+ ): ListDepthStats {
991
+ let oldestItemId: string | undefined;
992
+ let oldestAgeMs: number | undefined;
993
+ for (const item of queue) {
994
+ const added = Date.parse(item.addedAt);
995
+ if (Number.isNaN(added)) continue;
996
+ const age = nowMs - added;
997
+ if (oldestAgeMs === undefined || age > oldestAgeMs) {
998
+ oldestAgeMs = age;
999
+ oldestItemId = item.id;
1000
+ }
1001
+ }
1002
+ // Average item duration from the ledger's list-policy goals (most
1003
+ // recent 10 with both timestamps).
1004
+ const finals = new Map<string, { createdAt?: string; updatedAt?: string; policy?: string; status?: string }>();
1005
+ for (const e of ledgerEntries) {
1006
+ if (e.type === "state" && e.value?.goal?.id) {
1007
+ finals.set(String(e.value.goal.id), e.value.goal);
1008
+ }
1009
+ }
1010
+ const durations: number[] = [];
1011
+ for (const g of finals.values()) {
1012
+ if (g.policy !== "list") continue;
1013
+ if (g.status !== "complete" && g.status !== "archived") continue;
1014
+ const c = Date.parse(g.createdAt ?? "");
1015
+ const u = Date.parse(g.updatedAt ?? "");
1016
+ if (Number.isNaN(c) || Number.isNaN(u) || u < c) continue;
1017
+ durations.push(u - c);
1018
+ }
1019
+ const recent = durations.slice(-10);
1020
+ const avgDurationMs = recent.length > 0 ? Math.round(recent.reduce((a, b) => a + b, 0) / recent.length) : undefined;
1021
+ return {
1022
+ queueDepth: queue.length,
1023
+ oldestItemId,
1024
+ oldestAgeMs,
1025
+ avgDurationMs,
1026
+ durationSamples: recent.length,
1027
+ };
1028
+ }
1029
+
1030
+ function fmtAge(ms: number): string {
1031
+ const mins = Math.round(ms / 60000);
1032
+ if (mins < 60) return `${mins}m`;
1033
+ const hours = Math.round(mins / 60);
1034
+ if (hours < 24) return `${hours}h`;
1035
+ return `${Math.floor(hours / 24)}d ${Math.round(hours % 24)}h`;
1036
+ }
1037
+
1038
+ /** Contract item 7's exact headline format, then detail lines. */
1039
+ export function formatListDepth(stats: ListDepthStats): string {
1040
+ const oldest = stats.oldestAgeMs !== undefined ? fmtAge(stats.oldestAgeMs) : "—";
1041
+ const avg = stats.avgDurationMs !== undefined ? fmtAge(stats.avgDurationMs) : "—";
1042
+ const lines = [`queue depth: ${stats.queueDepth} · oldest: ${oldest} · avg duration: ${avg}`];
1043
+ if (stats.oldestItemId) lines.push(`oldest item: ${fmtAge(stats.oldestAgeMs!)} (id ${stats.oldestItemId})`);
1044
+ if (stats.durationSamples > 0) lines.push(`avg item duration: ${fmtAge(stats.avgDurationMs!)} (from last ${stats.durationSamples} archived)`);
1045
+ return lines.join("\n");
1046
+ }
@@ -39,6 +39,10 @@ import {
39
39
  isFullAuditObjective,
40
40
  lastShippedAtMs,
41
41
  resolveEffectiveAggressiveSettings,
42
+ computeListDepth,
43
+ ledgerPath,
44
+ crossRecommendMode,
45
+ formatListDepth,
42
46
  shouldSuppressHeartbeatForRecentShip,
43
47
  mergeSettings,
44
48
  parseListImport,
@@ -92,6 +96,7 @@ import {
92
96
  } from "../goal-settings.js";
93
97
  import {
94
98
  discoverGllaProjects,
99
+ parseLedgerEntries,
95
100
  filterPremature,
96
101
  formatRollupJson,
97
102
  formatRollupTable,
@@ -562,7 +567,15 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
562
567
  if (target === "list") {
563
568
  tmpl = tmpl.replace(
564
569
  "[GOAL DRAFTING]",
565
- "[GOAL DRAFTING — the confirmed goal goes into the /list LIST, it does not activate immediately. If the user wants MANY things added at once (a plan, a checklist, 'these 50 tasks'), propose them ALL AT ONCE with the items[] parameter — one Confirm for the whole batch, never 50 separate proposals.]",
570
+ "[LIST DRAFTING — the confirmed item goes into the /list LIST, it does not activate immediately. " +
571
+ "/list items are SHORT tasks, not multi-hour objectives: each item should fit comfortably in a single agent run " +
572
+ "(minutes of work, a single focused change). The list's long-running property is QUEUE DEPTH — hundreds of short " +
573
+ "items activated one at a time over days/weeks — never any single item's scope. " +
574
+ "If the user describes work that would take hours, propose breaking it into multiple /list items, or suggest /goal " +
575
+ "for the big version. When the user has many items to enqueue at once ('queue these 50 audits'), propose them ALL AT " +
576
+ "ONCE with the items[] parameter — one Confirm for the whole batch, never 50 separate proposals. Each items[] entry " +
577
+ "is still a SHORT task — never an aggregate wrapper ('land all N findings' with a '≥N commits' contract is the " +
578
+ "canonical anti-pattern: the auto-committer squashes, the count fails, the auditor disapproves finished work).]",
566
579
  );
567
580
  }
568
581
  } catch {
@@ -573,6 +586,12 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
573
586
  // is blocked until the user has replied at least once (see message_start).
574
587
  if (seed) {
575
588
  tmpl = buildSeedGrillMessage(tmpl, seed, tool);
589
+ // v0.25.3: cross-mode recommendation — catch wrapper-goal seeds and
590
+ // mode mismatches BEFORE the draft crystallizes.
591
+ if (target === "goal" || target === "list") {
592
+ const xr = crossRecommendMode(seed, target);
593
+ if (xr) tmpl += `\n\n${xr}`;
594
+ }
576
595
  }
577
596
  try {
578
597
  extensionApi?.sendUserMessage(tmpl, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
@@ -852,6 +871,20 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
852
871
  const sub = (parts[0] ?? "").toLowerCase();
853
872
  const rest = args.trim().slice(sub.length).trim();
854
873
 
874
+ if (sub === "depth") {
875
+ // v0.25.3: long-running state at a glance — queue depth, oldest item
876
+ // age, average item duration from archived list-policy goals.
877
+ let entries: Array<{ type: string; value?: any }> = [];
878
+ try {
879
+ entries = parseLedgerEntries(fs.readFileSync(ledgerPath(ctx.cwd), "utf-8"));
880
+ } catch {
881
+ /* no ledger yet */
882
+ }
883
+ const stats = computeListDepth(listQueue(), entries, Date.now());
884
+ ctx.ui.notify(`/list depth: ${formatListDepth(stats)}`, "info");
885
+ return;
886
+ }
887
+
855
888
  if (sub === "resume") {
856
889
  // Resume the list's head. The head activates AS the active goal, so this
857
890
  // is the same motion as /goal resume — named for the surface the user is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
4
4
  "description": "Goal. Loop. Audit. Done. — 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 — only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",
@@ -1,5 +1,58 @@
1
1
  # Goal drafting — pi-goal-list-loop-audit
2
2
 
3
+ # Long-running philosophy
4
+
5
+ The three modes are NOT redundant — each has a distinct source of
6
+ long-running-ness:
7
+
8
+ | Mode | Item size | Long-running by | Typical lifetime |
9
+ |---|---|---|---|
10
+ | `/goal` | ONE big multi-hour task | Scope | Hours |
11
+ | `/list` | N items × short (minutes each) | Queue depth | Hours → days → weeks |
12
+ | `/loop` | 1 metric × infinite polish | Bounds | Until plateau/stop/finish |
13
+
14
+ Pick the mode by where the long-running property lives, then draft for
15
+ THAT mode:
16
+
17
+ - **`/goal` is the multi-hour mode.** Its long-running property is SCOPE:
18
+ one big task that spans multiple agent runs, requires deep research, or
19
+ would take hours end-to-end. It ends only when the auditor approves the
20
+ verification contract. If the work is short enough to fit in a single
21
+ agent run (a focused change, a single audit, a small refactor), prefer
22
+ `/list` instead.
23
+ - **`/list` items are short tasks, not multi-hour objectives.** Each item
24
+ should fit comfortably in a single agent run — minutes of work, a
25
+ single focused change. The list's long-running property is QUEUE DEPTH:
26
+ hundreds of short items, activated one at a time, pushed over days or
27
+ weeks. If the user describes work that would take hours, break it up
28
+ into multiple `/list` items — or suggest `/goal` for the big version.
29
+ - **`/loop` is metric-driven infinite polish** — its long-running
30
+ property is open bounds; it ends on plateau, bounds, `/loop stop`, or
31
+ `/loop finish`.
32
+
33
+ ## Cross-recommend `/goal` ↔ `/list`
34
+
35
+ While drafting, watch the seed's shape and recommend the right mode:
36
+
37
+ - **Aggregate seeds belong in `/list` as N items, never as ONE wrapper
38
+ goal.** The canonical failure (real incidents, 2026-07-24): "close
39
+ every weak point in X.md (76 items, one commit each)" or "land all 40
40
+ findings as a tasklist" got folded into ONE goal with an aggregate
41
+ contract ("≥ 76 commits") — the auto-committer squashed commits, the
42
+ literal count failed, the auditor correctly disapproved finished work.
43
+ When the seed contains "N items/findings/weak points/screens" + "each"
44
+ + "one commit", propose N SHORT items via `propose_goal_draft`
45
+ `items[]`, each with its OWN per-item contract ("close IMP-AUD3-68:
46
+ Map.svelte:1528 missing role" — impossible to squash), and let any
47
+ re-audit pass be the FINAL `/goal`, not the first.
48
+ - **Multi-hour seeds in `/list`** ("this will take hours", "deep audit",
49
+ "research all 22 screens"): suggest `/goal` — or break the work into
50
+ ≤ 30-minute items.
51
+ - **Five-minute seeds in `/goal`** ("fix typo in X", "bump version"):
52
+ suggest `/list` or the tasklist plugin instead — a full audited goal is
53
+ overkill.
54
+ - The user can always override ("no, as a list item anyway") — comply.
55
+
3
56
  `[GOAL DRAFTING]`
4
57
 
5
58
  The user invoked `/goal` with no objective. Your job is to turn their vague
@@ -1,5 +1,16 @@
1
1
  # Loop drafting — pi-goal-list-loop-audit
2
2
 
3
+ # Long-running philosophy
4
+
5
+ `/loop` is the metric-driven infinite-polish mode. Its long-running
6
+ property is BOUNDS: one metric, polished forever, ending only on plateau,
7
+ bounds, `/loop stop`, or `/loop finish`. The other modes long-run
8
+ differently — `/goal` by scope (one big multi-hour task), `/list` by
9
+ queue depth (hundreds of short items, minutes each). If the user's work
10
+ is a single big task with a done state, that's `/goal`; if it's many
11
+ small tasks, that's `/list`; only true open-ended metric improvement
12
+ belongs here.
13
+
3
14
  `[LOOP DRAFTING]`
4
15
 
5
16
  The user invoked `/loop` with no arguments. Your job is to turn their rough