pi-goal-list-loop-audit 0.24.3 → 0.24.5

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.
package/README.md CHANGED
@@ -60,7 +60,7 @@ matches `/list show`.
60
60
  /list cancel # stop the whole list: abort the active item + drop all waiting
61
61
  /loop # draft the loop (agent grills; measure is test-run before you confirm)
62
62
  /loop start "keep polishing the UI" # infinite metricless loop (v0.23.6): no plateau, no cap — ends at time=/tokens= or /loop stop
63
- /loop respec # infinite metricless loop reconciling the codebase against the root SPEC.md / spec.md (v0.24.3)
63
+ /loop respec # infinite metricless loop reconciling the codebase against the root SPEC.md / spec.md (v0.24.3) — 2 specs = you pick, 0 specs = drafting, 1 spec = auto-start (v0.24.4)
64
64
  /loop start "reduce TODOs" measure="grep -c TODO src.txt | head -1" direction=min
65
65
  /loop start "shrink the bundle" measure="..." direction=min time=4 tokens=500000 # arbitrary bounds
66
66
  /loop start "reduce TODOs" measure="..." direction=min branch=1 # scratch-branch mode
@@ -1,5 +1,5 @@
1
1
  /**
2
- * pi-goal-list-loop-audit — v0.1.0
2
+ * pi-goal-list-loop-audit — v0.24.5
3
3
  * extensions/goal-loop-core.ts
4
4
  *
5
5
  * Shared types, state machine, JSONL persistence, helpers.
@@ -703,3 +703,44 @@ export function classifySessionCtx(ownerSession: unknown, ownerLive: boolean, se
703
703
  if (!ownerSession || !ownerLive) return "claim";
704
704
  return sessionManager === ownerSession ? "refresh" : "foreign";
705
705
  }
706
+
707
+ // =================================================================
708
+ // v0.24.5: tool-visibility self-heal
709
+ // =================================================================
710
+ //
711
+ // Root cause (INCIDENT-COMPLETION-BLACKHOLE-2026-07-23): external
712
+ // extensions like pi-plugin-list-selector-modlist call pi.setActiveTools
713
+ // with a frozen tool snapshot at session_start. When glla's session_start
714
+ // handler runs BEFORE theirs (load order), our lazily-registered agent
715
+ // tools get registered, briefly auto-activated, then wiped from the
716
+ // model-facing active set on the very next pi.setActiveTools call from
717
+ // modlist. Commands, widget, watchdog keep working (they don't go
718
+ // through the tool registry), but every agent tool — complete_goal,
719
+ // propose_loop_draft, etc. — answers "Tool not found" to the model.
720
+ //
721
+ // Self-heal: any handler that triggers registerAgentTools must also
722
+ // ensure the registered tool names are present in pi.getActiveTools(),
723
+ // re-adding any missing ones via pi.setActiveTools. Once per session,
724
+ // notify the user naming the external allowlist as the likely culprit
725
+ // so they can fix their profile once and silence it.
726
+
727
+ export const GLLA_TOOL_NAMES = [
728
+ "complete_goal",
729
+ "pause_goal",
730
+ "complete_task",
731
+ "update_task_status",
732
+ "propose_goal_draft",
733
+ "propose_loop_draft",
734
+ "propose_loop_refine",
735
+ "list_add",
736
+ "list_activate",
737
+ "list_status",
738
+ "propose_task_list",
739
+ ] as const;
740
+
741
+ export type GllaToolName = (typeof GLLA_TOOL_NAMES)[number];
742
+
743
+ export function missingGllaTools(activeNames: readonly string[]): readonly GllaToolName[] {
744
+ const active = new Set(activeNames);
745
+ return GLLA_TOOL_NAMES.filter((n) => !active.has(n));
746
+ }
@@ -302,15 +302,21 @@ export function parseLoopStartArgs(raw: string): {
302
302
  /** Root-only spec candidates, in priority order. No fuzzy search. */
303
303
  export const RESPEC_SPEC_CANDIDATES = ["SPEC.md", "spec.md"] as const;
304
304
 
305
- /** Resolve the project spec in the root only; null when absent. */
306
- export function resolveSpecFile(cwd: string): string | null {
305
+ /** Resolve every root spec candidate that exists (priority order). */
306
+ export function resolveSpecFiles(cwd: string): string[] {
307
+ const found: string[] = [];
307
308
  for (const name of RESPEC_SPEC_CANDIDATES) {
308
309
  const p = join(cwd, name);
309
310
  try {
310
- if (existsSync(p) && statSync(p).isFile()) return p;
311
+ if (existsSync(p) && statSync(p).isFile()) found.push(p);
311
312
  } catch { /* unreadable — keep looking */ }
312
313
  }
313
- return null;
314
+ return found;
315
+ }
316
+
317
+ /** Resolve the project spec in the root only; null when absent. */
318
+ export function resolveSpecFile(cwd: string): string | null {
319
+ return resolveSpecFiles(cwd)[0] ?? null;
314
320
  }
315
321
 
316
322
  /**
@@ -63,6 +63,7 @@ import {
63
63
  shouldAutoResumeOnSessionStart,
64
64
  statusLabel,
65
65
  writeGoalMd,
66
+ missingGllaTools,
66
67
  } from "../goal-loop-core.js";
67
68
  import { runGoalCompletionAuditor } from "../goal-loop-auditor.js";
68
69
  import {
@@ -82,8 +83,7 @@ import {
82
83
  parseLoopStartArgs,
83
84
  parseMetric,
84
85
  LOOP_DEFAULTS,
85
- RESPEC_SPEC_CANDIDATES,
86
- resolveSpecFile,
86
+ resolveSpecFiles,
87
87
  respecTarget,
88
88
  type LoopState,
89
89
  } from "../goal-loop-forever.js";
@@ -1425,14 +1425,37 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
1425
1425
  ctx.ui.notify("A loop is already active. /loop stop first.", "warning");
1426
1426
  return;
1427
1427
  }
1428
- const specPath = resolveSpecFile(ctx.cwd);
1429
- if (!specPath) {
1430
- ctx.ui.notify(
1431
- `/loop respec: no spec in the project root (looked for ${RESPEC_SPEC_CANDIDATES.join(", ")}). Create one, or use /loop start "<target>" directly.`,
1432
- "warning",
1428
+ const specs = resolveSpecFiles(ctx.cwd);
1429
+ if (specs.length === 0) {
1430
+ // No spec → the target is undetermined; grill instead of dead-ending
1431
+ // on an error (v0.24.4).
1432
+ ctx.ui.notify("No SPEC.md / spec.md in the project root — drafting the loop target with you (or bootstrap a spec first).", "info");
1433
+ await startDrafting(
1434
+ ctx,
1435
+ "loop",
1436
+ "reconcile the codebase against the project spec — but NO SPEC.md / spec.md exists in the root. Grill the user: should the first work be bootstrapping a SPEC.md from the current code (then reconcile against it), or is the reconciliation target better stated in prose? Challenge vague answers.",
1433
1437
  );
1434
1438
  return;
1435
1439
  }
1440
+ let specPath = specs[0]!;
1441
+ if (specs.length > 1) {
1442
+ // Two specs = ambiguous — never silently pick (v0.24.4). One
1443
+ // slash-bar select, plus a nudge to consolidate.
1444
+ const names = specs.map((p) => path.basename(p));
1445
+ const choice = await ctx.ui.select(
1446
+ "Both SPEC.md and spec.md exist in the root — which one is the spec?",
1447
+ names,
1448
+ );
1449
+ if (choice === undefined) {
1450
+ ctx.ui.notify("respec cancelled.", "info");
1451
+ return;
1452
+ }
1453
+ specPath = specs[names.indexOf(choice)]!;
1454
+ ctx.ui.notify(
1455
+ `Using ${path.basename(specPath)} as the spec. Both files exist — worth consolidating; the loop treats only ${path.basename(specPath)} as the spec.`,
1456
+ "info",
1457
+ );
1458
+ }
1436
1459
  const target = respecTarget(path.basename(specPath));
1437
1460
  await startLoopFromConfig(ctx, {
1438
1461
  target,
@@ -2707,6 +2730,29 @@ export default function (pi: ExtensionAPI): void {
2707
2730
  // "no active goal" if called).
2708
2731
  let registeredCtx: ExtensionContext | null = null;
2709
2732
 
2733
+ // v0.24.5 tool-visibility self-heal: surface the notify exactly once
2734
+ // per session so the user learns about an external allowlist once and
2735
+ // can fix their profile to silence it.
2736
+ let toolHealNotified = false;
2737
+ function ensureAgentToolsActive(pi: ExtensionAPI, ctx: ExtensionContext): void {
2738
+ try {
2739
+ const active = pi.getActiveTools();
2740
+ const missing = missingGllaTools(active);
2741
+ if (missing.length === 0) return;
2742
+ pi.setActiveTools([...active, ...missing]);
2743
+ if (!toolHealNotified) {
2744
+ toolHealNotified = true;
2745
+ const list = missing.join(", ");
2746
+ ctx.ui.notify(
2747
+ `glla: ${missing.length} agent tool(s) were hidden by an external tool allowlist (e.g. a modlist profile) and have been re-activated (${list}). Add them to your allowlist profile to silence this.`,
2748
+ "warning",
2749
+ );
2750
+ }
2751
+ } catch {
2752
+ // Older pi without getActiveTools/setActiveTools — nothing we can do.
2753
+ }
2754
+ }
2755
+
2710
2756
  pi.on("message_start", async (event: any, _ctx: ExtensionContext) => {
2711
2757
  // v0.14.0 drafting floor: count real user replies while drafting. Our
2712
2758
  // own injected draft prompt arrives as a user message — skip that one.
@@ -2751,6 +2797,7 @@ export default function (pi: ExtensionAPI): void {
2751
2797
  registerAgentTools(pi, ctx);
2752
2798
  registeredCtx = ctx;
2753
2799
  }
2800
+ ensureAgentToolsActive(pi, ctx);
2754
2801
  warnOnCommandCollision(ctx);
2755
2802
  warnIfAuditorProviderRisky(ctx);
2756
2803
  // Restore gate (v0.21.0): a fresh session ("startup"/"new", or a pi too
@@ -2831,6 +2878,7 @@ export default function (pi: ExtensionAPI): void {
2831
2878
  registerAgentTools(pi, ctx);
2832
2879
  registeredCtx = ctx;
2833
2880
  }
2881
+ ensureAgentToolsActive(pi, ctx);
2834
2882
  // Nudge accounting: a supervising turn with zero tool calls is a nudge
2835
2883
  // (no real progress); 3 consecutive → pause. Tool-use turns reset it.
2836
2884
  if (isSupervising()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.24.3",
3
+ "version": "0.24.5",
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",