pi-goal-list-loop-audit 0.27.7 → 0.27.9

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.
@@ -104,6 +104,7 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
104
104
  "You are the independent completion auditor for pi-goal-list-loop-audit.",
105
105
  "The executor claims the goal is complete. Your job is to decide whether the user's objective is actually satisfied.",
106
106
  "Be skeptical and semantic. Do not approve from paperwork, intent, file count, word count, build success, or a plausible summary alone.",
107
+ "Chunk output near context-full: prefer focused, evidence-quote-first replies (one tool call at a time, raw output inline) over mega-replies that hit the output-token cap. The orchestrator's auto-continue fires on stop_reason=\"length\" but pre-empting by chunking is cheaper than recovering from the cap.",
107
108
  "Use read/grep/find/ls/bash as needed to inspect real artifacts. Do not mutate files or run destructive commands.",
108
109
  "If the work is only an alpha scaffold, generated template, shallow draft, proxy milestone, or lacks the user-facing value requested, disapprove.",
109
110
  "If any explicit requirement is missing, weakly verified, contradicted, or not inspectable with the available evidence, disapprove.",
@@ -231,12 +231,19 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
231
231
  });
232
232
  // v0.27.1: what survives the pause — the first question at a pause is
233
233
  // "did I lose the work?". Answer it on the card.
234
+ // v0.27.9: when the goal has no telemetry yet (restored-in-fresh-session
235
+ // before the first turn), render "awaiting first turn" instead of "saved"
236
+ // — the latter was misleading because no work was ever "saved" before the
237
+ // session ended.
234
238
  const spent: string[] = [];
235
239
  const tokUsed = g.usage?.tokensUsed ?? 0;
236
- if (tokUsed > 0) spent.push(`${fmtTokens(tokUsed)} tok spent`);
237
240
  const audits = g.auditHistory?.length ?? 0;
241
+ if (tokUsed > 0) spent.push(`${fmtTokens(tokUsed)} tok spent`);
238
242
  if (audits > 0) spent.push(`${audits} audit${audits === 1 ? "" : "s"}`);
239
- const savedLine = `saved${spent.length > 0 ? ` — ${spent.join(" · ")}` : ""} · resumes exactly here`;
243
+ const hasTelemetry = spent.length > 0;
244
+ const savedLine = hasTelemetry
245
+ ? `saved — ${spent.join(" · ")} · resumes exactly here`
246
+ : `awaiting first turn — resumes exactly here`;
240
247
  if (g.pauseSuggestedAction) {
241
248
  lines.push(`├─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
242
249
  const wrapped = wrap(g.pauseSuggestedAction, budget, 3);
@@ -83,6 +83,18 @@ export interface Settings {
83
83
  * Always wins over subagentModelStrategy — the managed override is written
84
84
  * WITH this pin regardless of strategy. */
85
85
  subagentModelOverrides?: Record<string, string>;
86
+ /** v0.27.9: per-tool overrides — allowlist (force tools visible despite
87
+ * an external modlist), hidden (force tools hidden even when allowed by
88
+ * the session), and per-tool config (Record<toolName, Record<key, value>>
89
+ * — extensible for tool-specific knobs like timeouts, formats, etc.). */
90
+ toolOverrides?: {
91
+ /** Tools that MUST be active even when an external allowlist hides them. */
92
+ allow?: string[];
93
+ /** Tools that MUST be hidden even when the session allows them. */
94
+ hide?: string[];
95
+ /** Per-tool configuration knobs (extensible). */
96
+ perToolConfig?: Record<string, Record<string, unknown>>;
97
+ };
86
98
  }
87
99
 
88
100
  export const DEFAULT_SETTINGS: Settings = {
@@ -146,6 +158,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
146
158
  "stallShortWords",
147
159
  "stallSimilarityThreshold",
148
160
  "postaudit",
161
+ "toolOverrides",
149
162
  ];
150
163
 
151
164
  /** Where each effective setting comes from (for the /glla display). */
@@ -679,7 +679,7 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
679
679
  function fireReviewer(
680
680
  ctx: ExtensionContext,
681
681
  source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
682
- opts: { manual?: boolean; mode?: "off" | "default" | "auto" | "aggressive" | "report" } = {},
682
+ opts: { manual?: boolean; mode?: "off" | "on" | "auto" | "aggressive" } = {},
683
683
  ): void {
684
684
  try {
685
685
  const settings = loadSettings(ctx.cwd);
@@ -3068,12 +3068,12 @@ async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
3068
3068
  const parts = args.trim().split(/\s+/).filter(Boolean);
3069
3069
  const id = parts[0] ?? "";
3070
3070
  const modeArg = parts[1];
3071
- const validModes = ["off", "default", "auto", "aggressive", "report"] as const;
3071
+ const validModes = ["off", "on", "auto", "aggressive"] as const;
3072
3072
  const mode = (validModes as readonly string[]).includes(modeArg ?? "")
3073
3073
  ? (modeArg as typeof validModes[number])
3074
3074
  : undefined;
3075
3075
  if (modeArg && !mode) {
3076
- ctx.ui.notify(`Unknown mode "${modeArg}" — use off | default | auto | aggressive | report.`, "warning");
3076
+ ctx.ui.notify(`Unknown mode "${modeArg}" — use off | on | auto | aggressive.`, "warning");
3077
3077
  return;
3078
3078
  }
3079
3079
  if (!id) {
@@ -3101,6 +3101,98 @@ async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
3101
3101
  fireReviewer(ctx, { kind: "goal", goalId, objective, terminal: "goal-complete" }, { manual: true, mode });
3102
3102
  }
3103
3103
 
3104
+ /** v0.27.9: /glla tooloverride <action> [args] — per-tool override menu.
3105
+ * Actions:
3106
+ * list show current allow/hide/perToolConfig
3107
+ * allow <tool> force <tool> visible despite modlist
3108
+ * hide <tool> force <tool> hidden despite session
3109
+ * unallow <tool> remove from allow list
3110
+ * unhide <tool> remove from hide list
3111
+ * set <tool> <key>=<value> write perToolConfig[tool][key]
3112
+ * unset <tool> <key> remove perToolConfig[tool][key]
3113
+ * Example: /glla tooloverride allow bash hide write_file set bash timeout=60 */
3114
+ async function cmdToolOverride(args: string, ctx: ExtensionContext): Promise<void> {
3115
+ const settings = loadSettings(ctx.cwd);
3116
+ const current = settings.toolOverrides ?? {};
3117
+ const parts = args.trim().split(/\s+/).filter(Boolean);
3118
+ const action = parts[0];
3119
+ if (!action || action === "list" || action === "show") {
3120
+ const allow = current.allow ?? [];
3121
+ const hide = current.hide ?? [];
3122
+ const cfg = current.perToolConfig ?? {};
3123
+ const out = `toolOverrides (project):\n allow: ${allow.length ? allow.join(", ") : "(none)"}\n hide: ${hide.length ? hide.join(", ") : "(none)"}\n perToolConfig: ${Object.keys(cfg).length ? JSON.stringify(cfg) : "(none)"}`;
3124
+ ctx.ui.notify(out, "info");
3125
+ return;
3126
+ }
3127
+ const apply = (patch: Partial<NonNullable<Settings["toolOverrides"]>>) => {
3128
+ saveSettings("project", ctx.cwd, { toolOverrides: { ...current, ...patch } });
3129
+ };
3130
+ if (action === "allow" || action === "hide" || action === "unallow" || action === "unhide") {
3131
+ const tool = parts[1];
3132
+ if (!tool) {
3133
+ ctx.ui.notify(`Usage: /glla tooloverride ${action} <tool>`, "warning");
3134
+ return;
3135
+ }
3136
+ if (action === "allow") {
3137
+ const allow = current.allow ?? [];
3138
+ if (!allow.includes(tool)) apply({ allow: [...allow, tool] });
3139
+ ctx.ui.notify(`toolOverrides.allow += ${tool}`, "info");
3140
+ } else if (action === "hide") {
3141
+ const hide = current.hide ?? [];
3142
+ if (!hide.includes(tool)) apply({ hide: [...hide, tool] });
3143
+ ctx.ui.notify(`toolOverrides.hide += ${tool}`, "info");
3144
+ } else if (action === "unallow") {
3145
+ apply({ allow: (current.allow ?? []).filter((t) => t !== tool) });
3146
+ ctx.ui.notify(`toolOverrides.allow -= ${tool}`, "info");
3147
+ } else {
3148
+ apply({ hide: (current.hide ?? []).filter((t) => t !== tool) });
3149
+ ctx.ui.notify(`toolOverrides.hide -= ${tool}`, "info");
3150
+ }
3151
+ return;
3152
+ }
3153
+ if (action === "set" || action === "unset") {
3154
+ const tool = parts[1];
3155
+ const kv = parts[2];
3156
+ if (!tool || !kv) {
3157
+ ctx.ui.notify(`Usage: /glla tooloverride ${action} <tool> <key>[=<value>]`, "warning");
3158
+ return;
3159
+ }
3160
+ const cfg = { ...(current.perToolConfig ?? {}) };
3161
+ const toolCfg = { ...(cfg[tool] ?? {}) };
3162
+ if (action === "set") {
3163
+ const eq = kv.indexOf("=");
3164
+ if (eq < 0) {
3165
+ ctx.ui.notify(`set needs key=value: got "${kv}"`, "warning");
3166
+ return;
3167
+ }
3168
+ const k = kv.slice(0, eq);
3169
+ const v: unknown = parseToolOverrideValue(kv.slice(eq + 1));
3170
+ toolCfg[k] = v;
3171
+ } else {
3172
+ delete toolCfg[kv];
3173
+ }
3174
+ cfg[tool] = toolCfg;
3175
+ apply({ perToolConfig: cfg });
3176
+ ctx.ui.notify(`toolOverrides.perToolConfig.${tool} ${action === "set" ? "set" : "unset"}`, "info");
3177
+ return;
3178
+ }
3179
+ ctx.ui.notify(`Unknown tooloverride action: ${action}. Use: list | allow | hide | unallow | unhide | set | unset.`, "warning");
3180
+ }
3181
+
3182
+ /** Parse a tool-override value: numbers, booleans, JSON objects/arrays, else string. */
3183
+ function parseToolOverrideValue(s: string): unknown {
3184
+ const trimmed = s.trim();
3185
+ if (trimmed === "true") return true;
3186
+ if (trimmed === "false") return false;
3187
+ if (trimmed === "null") return null;
3188
+ if (/^-?\d+$/.test(trimmed)) return Number(trimmed);
3189
+ if (/^-?\d+\.\d+$/.test(trimmed)) return Number(trimmed);
3190
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
3191
+ try { return JSON.parse(trimmed); } catch { /* fall through */ }
3192
+ }
3193
+ return trimmed;
3194
+ }
3195
+
3104
3196
  /** v0.27.5: /glla reviewer | postaudit — the post-completion audit config menu
3105
3197
  * (project-scoped). Reads the dual-write settings (postaudit wins over the
3106
3198
  * legacy reviewer key), and writes back to whichever key was read first —
@@ -3129,8 +3221,8 @@ async function cmdReviewerSettings(ctx: ExtensionContext): Promise<void> {
3129
3221
  try {
3130
3222
  if (choice.startsWith("Enabled")) save({ enabled: !cfg.enabled });
3131
3223
  else if (choice.startsWith("Mode")) {
3132
- // v0.27.5: 5-state cycle off → default → auto → aggressive → report → off
3133
- const order: Array<"off" | "default" | "auto" | "aggressive" | "report"> = ["off", "default", "auto", "aggressive", "report"];
3224
+ // v0.27.9: 4-state cycle off → on → auto → aggressive → off
3225
+ const order: Array<"off" | "on" | "auto" | "aggressive"> = ["off", "on", "auto", "aggressive"];
3134
3226
  const i = order.indexOf(cfg.mode as typeof order[number]);
3135
3227
  const next = order[(i + 1) % order.length]!;
3136
3228
  save({ mode: next });
@@ -3257,6 +3349,11 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
3257
3349
  await cmdReviewerSettings(ctx);
3258
3350
  return;
3259
3351
  }
3352
+ // v0.27.9: per-tool overrides — sub-mode `tooloverride <action> <tool>`
3353
+ if (/^tooloverride\b/.test(trimmed)) {
3354
+ await cmdToolOverride(trimmed.slice("tooloverride".length).trim(), ctx);
3355
+ return;
3356
+ }
3260
3357
  if (!trimmed) {
3261
3358
  if (ctx.hasUI) {
3262
3359
  await openSettingsUI(ctx);
@@ -3663,13 +3760,32 @@ export default function (pi: ExtensionAPI): void {
3663
3760
  // v0.24.5 tool-visibility self-heal: surface the notify exactly once
3664
3761
  // per session so the user learns about an external allowlist once and
3665
3762
  // can fix their profile to silence it.
3763
+ // v0.27.9: also applies toolOverrides.allow / toolOverrides.hide from
3764
+ // .pi-glla/settings.json — the project's per-tool policy wins over the
3765
+ // external allowlist (allow) or over the session default (hide).
3666
3766
  let toolHealNotified = false;
3667
3767
  function ensureAgentToolsActive(pi: ExtensionAPI, ctx: ExtensionContext): void {
3668
3768
  try {
3669
3769
  const active = pi.getActiveTools();
3670
3770
  const missing = missingGllaTools(active);
3671
- if (missing.length === 0) return;
3672
- pi.setActiveTools([...active, ...missing]);
3771
+ const overrides = loadSettings(ctx.cwd).toolOverrides;
3772
+ let next = [...active, ...missing];
3773
+ let changed = missing.length > 0;
3774
+ // Apply per-tool allowlist — force tools visible despite an external modlist.
3775
+ if (overrides?.allow && overrides.allow.length > 0) {
3776
+ const toAdd = overrides.allow.filter((t) => !next.includes(t));
3777
+ if (toAdd.length > 0) {
3778
+ next = [...next, ...toAdd];
3779
+ changed = true;
3780
+ }
3781
+ }
3782
+ // Apply per-tool hide — force tools hidden even when the session allows them.
3783
+ if (overrides?.hide && overrides.hide.length > 0) {
3784
+ const before = next.length;
3785
+ next = next.filter((t) => !overrides.hide!.includes(t));
3786
+ if (next.length !== before) changed = true;
3787
+ }
3788
+ if (changed) pi.setActiveTools(next);
3673
3789
  if (!toolHealNotified) {
3674
3790
  toolHealNotified = true;
3675
3791
  const list = missing.join(", ");
@@ -15,7 +15,7 @@
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
17
 
18
- export type ReviewerMode = "off" | "default" | "auto" | "aggressive" | "report";
18
+ export type ReviewerMode = "off" | "on" | "auto" | "aggressive";
19
19
 
20
20
  export interface ReviewerConfig {
21
21
  enabled: boolean;
@@ -37,7 +37,7 @@ export interface ReviewerConfig {
37
37
 
38
38
  export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
39
39
  enabled: true,
40
- mode: "default",
40
+ mode: "on",
41
41
  fireOn: ["goal-complete", "list-complete"],
42
42
  doNotFireOn: ["goal-aborted", "goal-paused"],
43
43
  cascade: ["convert-findings-to-list", "queue-leftovers", "fire-audit-on-clean", "notify-and-idle"],
@@ -49,9 +49,15 @@ export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
49
49
  maxReviewsPerDay: 20,
50
50
  };
51
51
 
52
- /** Merge a partial project-settings block over the defaults. */
52
+ /** Merge a partial project-settings block over the defaults. v0.27.9:
53
+ * migrate legacy `"default"` / `"report"` mode values to `"on"` (the
54
+ * contract-compliant 4-mode set is now `off | on | auto | aggressive`). */
53
55
  export function resolveReviewerConfig(block?: Partial<ReviewerConfig>): ReviewerConfig {
54
- return { ...DEFAULT_REVIEWER_CONFIG, ...(block ?? {}) };
56
+ const merged = { ...DEFAULT_REVIEWER_CONFIG, ...(block ?? {}) };
57
+ if ((merged.mode as string) === "default" || (merged.mode as string) === "report") {
58
+ merged.mode = "on";
59
+ }
60
+ return merged;
55
61
  }
56
62
 
57
63
  export type FindingClass = "bug" | "refactor" | "architectural" | "strategic";
@@ -250,11 +256,10 @@ export function runReviewer(
250
256
  let cascadeStep = "notify-and-idle";
251
257
  const auto = config.mode === "auto" || config.mode === "aggressive";
252
258
  const aggressive = config.mode === "aggressive";
253
- const reportOnly = config.mode === "report";
254
259
 
255
260
  // Cascade: findings → list items (leverage: fix-without-confirm).
256
261
  const convertStep = source.kind === "goal" ? "convert-findings-to-list" : "queue-leftovers";
257
- if (bugs.length > 0 && config.cascade.includes(convertStep) && !reportOnly) {
262
+ if (bugs.length > 0 && config.cascade.includes(convertStep)) {
258
263
  deps.enqueueListItems(bugs.map((f) => f.text));
259
264
  enqueued = bugs.length;
260
265
  cascadeStep = convertStep;
@@ -263,7 +268,7 @@ export function runReviewer(
263
268
  // auto mode → /list items (the auto-loop rolls straight into them).
264
269
  // aggressive mode → enqueue AND relaunch as the next active goal
265
270
  // (skips both Confirm and the queue — the unattended rig never stops).
266
- if (architectural.length > 0 && !reportOnly) {
271
+ if (architectural.length > 0) {
267
272
  if (aggressive) {
268
273
  deps.enqueueListItems(architectural.map((f) => f.text));
269
274
  // v0.27.5 aggressive: also propose the FIRST architectural finding
@@ -293,7 +298,7 @@ export function runReviewer(
293
298
  // auto mode enqueues the audit as a /list item (no Confirm — the
294
299
  // cascade keeps rolling until the findings run dry).
295
300
  // aggressive mode → relaunch the audit goal directly (no Confirm).
296
- if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean") && !reportOnly) {
301
+ if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")) {
297
302
  const auditObjective = `Post-completion regression scan after ${source.goalId} (${config.auditScope})`;
298
303
  if (aggressive) {
299
304
  deps.proposeGoal(auditObjective, "aggressive postaudit: clean completion — relaunching the regression scan as /goal");
@@ -309,7 +314,6 @@ export function runReviewer(
309
314
  cascadeStep = "fire-audit-on-clean";
310
315
  }
311
316
  }
312
- if (reportOnly) cascadeStep = "report-only";
313
317
 
314
318
  const report: ReviewReport = {
315
319
  goalId: source.goalId,
@@ -345,7 +349,7 @@ export function runReviewer(
345
349
  export function reviewerMenuOptions(cfg: ReviewerConfig): string[] {
346
350
  return [
347
351
  `Enabled — ${cfg.enabled ? "ON" : "OFF"}`,
348
- `Mode — ${cfg.mode} (off = silenced · default = Confirm-gated · auto = auto-loop, no Confirms · aggressive = auto + relaunch · report = report only)`,
352
+ `Mode — ${cfg.mode} (off = silenced · on = Confirm-gated cascade · auto = auto-loop, no Confirms · aggressive = auto + relaunch)`,
349
353
  `Leverage mode — ${cfg.leverageMode} (bug/refactor findings)`,
350
354
  `Fire on goal-complete — ${cfg.fireOn.includes("goal-complete") ? "ON" : "OFF"}`,
351
355
  `Fire on list-complete — ${cfg.fireOn.includes("list-complete") ? "ON" : "OFF"}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.27.7",
3
+ "version": "0.27.9",
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",