pi-goal-x 0.31.8 → 0.32.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.
package/README.md CHANGED
@@ -6,19 +6,21 @@
6
6
  <a href="https://pi.dev/packages?type=extension" target="_blank" rel="noopener noreferrer">
7
7
  <picture>
8
8
  <source media="(prefers-color-scheme: dark)" srcset="assets/badge-dark.svg">
9
- <img src="assets/badge-light.svg" alt="TOP 0.3% of Pi coding agent extensions: #7 of 3,275 by downloads · Sep 22, 2026 (best recorded rank)" width="480">
9
+ <img src="assets/badge-light.svg" alt="TOP 0.2% of Pi coding agent extensions: #6 of 3,258 by downloads · Oct 1, 2026 (best recorded rank)" width="480">
10
10
  </picture>
11
11
  </a>
12
12
  </div>
13
13
 
14
14
  # pi-goal-x
15
15
 
16
- Adds `/goal` functionality to [pi](https://github.com/earendil-works/pi-coding-agent). The agent helps you define a goal and plan, continues working on it automatically, and submits the result to an optional independent completion auditor.
16
+ Adds `/goal` functionality to [pi](https://pi.dev). The agent helps you define a goal and plan, continues working on it automatically, and submits the result to an optional independent completion auditor.
17
17
 
18
18
  The extension saves goal objectives, tasks, and progress across sessions. You can pause, resume, revise, or switch goals as your work changes.
19
19
 
20
20
  ## Install
21
21
 
22
+ Supports pi 0.83 through 1.x (`@earendil-works/pi-ai`, `pi-coding-agent`, and `pi-tui` `>=0.83.0 <2.0.0`), developed and verified against 1.0.0.
23
+
22
24
  ```bash
23
25
  pi install npm:pi-goal-x
24
26
  ```
@@ -117,6 +119,7 @@ Open `/goal-settings` to change these options. You can save defaults for all pro
117
119
  | --- | --- |
118
120
  | Explicit execution contracts (`strictExecutionContract`) | Opt-in ready/wait protocol with one missing-decision repair, then pause. Defaults to `false`: successful executions continue automatically. |
119
121
  | Autonomous run allowance (`maxAutonomousRuns`) | Positive whole number of extension-started runs per creation or `/goal-resume` period. **Unset means unlimited; zero disables automatic continuation.** Settings edits change the limit without resetting usage. |
122
+ | Autonomous runs line (`showAutonomousRuns`) | Controls the `Autonomous runs: N/limit` line that appears for a finite allowance. Defaults to `true`. |
120
123
  | Task tracking (`disableTasks`) | Turn task lists on or off. Set to `true` to disable them. |
121
124
  | Subtask depth (`subtaskDepth`) | Limit how many levels of subtasks the agent can create. |
122
125
  | Completion requirements (`disableContracts`) | Turn explicit goal and task completion requirements on or off. Set to `true` to disable them. |
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,275 by downloads · Sep 22, 2026 (best recorded rank)">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.2% of Pi coding agent extensions: #6 of 3,258 by downloads · Oct 1, 2026 (best recorded rank)">
2
2
  <!-- Dark mode -->
3
3
  <style>
4
4
  .frame { fill: #14181f; stroke: #2a3040; stroke-width: 1; }
@@ -10,9 +10,9 @@
10
10
  </style>
11
11
  <a class="link" href="https://pi.dev/packages?type=extension" target="_blank" rel="noopener noreferrer">
12
12
  <rect class="frame" x="0.5" y="0.5" width="479" height="53" rx="6"/>
13
- <text class="rank" x="18" y="34">TOP 0.3%</text>
13
+ <text class="rank" x="18" y="34">TOP 0.2%</text>
14
14
  <line class="divider" x1="128" y1="13" x2="128" y2="41"/>
15
15
  <text class="label" x="143" y="25">of Pi coding agent extensions</text>
16
- <text class="meta" x="143" y="41">#7 of 3,275 by downloads · Sep 22, 2026</text>
16
+ <text class="meta" x="143" y="41">#6 of 3,258 by downloads · Oct 1, 2026</text>
17
17
  </a>
18
18
  </svg>
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,275 by downloads · Sep 22, 2026 (best recorded rank)">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.2% of Pi coding agent extensions: #6 of 3,258 by downloads · Oct 1, 2026 (best recorded rank)">
2
2
  <!-- Light mode -->
3
3
  <style>
4
4
  .frame { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1; }
@@ -10,9 +10,9 @@
10
10
  </style>
11
11
  <a class="link" href="https://pi.dev/packages?type=extension" target="_blank" rel="noopener noreferrer">
12
12
  <rect class="frame" x="0.5" y="0.5" width="479" height="53" rx="6"/>
13
- <text class="rank" x="18" y="34">TOP 0.3%</text>
13
+ <text class="rank" x="18" y="34">TOP 0.2%</text>
14
14
  <line class="divider" x1="128" y1="13" x2="128" y2="41"/>
15
15
  <text class="label" x="143" y="25">of Pi coding agent extensions</text>
16
- <text class="meta" x="143" y="41">#7 of 3,275 by downloads · Sep 22, 2026</text>
16
+ <text class="meta" x="143" y="41">#6 of 3,258 by downloads · Oct 1, 2026</text>
17
17
  </a>
18
18
  </svg>
@@ -14,12 +14,18 @@ Set an appropriate allowance in `/goal-settings`, or in `.pi/pi-goal-x-settings.
14
14
  { "maxAutonomousRuns": 20 }
15
15
  ```
16
16
 
17
+ The `Autonomous runs: used/limit` line reports a finite allowance and is left out when
18
+ allowance is unlimited. Set `showAutonomousRuns` to `false` to drop it for a finite
19
+ allowance as well; it defaults to `true`.
20
+
17
21
  Agents may edit this setting. Changing it does not replenish consumed runs; explicit `/goal-resume` renews the period and continues now, including from a waiting goal. No configured allowance is required unless the effective limit is zero, which disables resume. Tool calls within a run are not separate runs. Existing token budgets still apply.
18
22
 
19
- Explicit `ready` is optional in default mode. New `wait` declarations require strict mode; otherwise they return a non-terminating error. Previously saved waits retain their deadline, checks, and repair rules when upgrading or disabling strict mode, and may be re-declared with the same identity. Mode changes never resume a paused goal or renew consumed runs.
23
+ Explicit `ready` is optional in default mode. A goal saved before next actions were
24
+ removed still loads, with its `nextAction` field stripped during normalization.
25
+ New `wait` declarations require strict mode; otherwise they return a non-terminating error. Previously saved waits retain their deadline, checks, and repair rules when upgrading or disabling strict mode, and may be re-declared with the same identity. Mode changes never resume a paused goal or renew consumed runs.
20
26
 
21
27
  ```js
22
- update_goal({ continuation: { kind: "ready", next_action: "Verify the build artifacts" } })
28
+ update_goal({ continuation: { kind: "ready" } })
23
29
  update_goal({ continuation: {
24
30
  kind: "wait", reason: "Await the remote build",
25
31
  deadline: "2026-09-15T12:00:00Z",
@@ -302,4 +302,4 @@ per-draft auditor selection), a fully operable settings menu, `/goal-clear`
302
302
  confirmation, neutral task-confirmation labels, failure-checked completion
303
303
  commits, per-goal revision/lock serialization with typed conflicts, the
304
304
  agent-pause outcome, untrusted `completion_summary` claims, the enforced
305
- experiment matrix, the runner self-check, and the Pi SDK 0.83 family upgrade.
305
+ experiment matrix, the runner self-check, and the Pi SDK 1.0 family upgrade.
@@ -374,6 +374,7 @@ export function registerGoalCommands(core: GoalCore): void {
374
374
  { key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
375
375
  { key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", section: "Goal behavior", kind: "boolean" },
376
376
  { key: "maxAutonomousRuns", label: "autonomous run allowance", section: "Goal behavior", kind: "positiveInteger" },
377
+ { key: "showAutonomousRuns", label: "show autonomous runs line", section: "Goal behavior", kind: "boolean" },
377
378
  { key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
378
379
  { key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
379
380
  { key: "disableTasks", label: "disableTasks", section: "Task tracking", kind: "boolean" },
@@ -392,7 +393,7 @@ export function registerGoalCommands(core: GoalCore): void {
392
393
  ];
393
394
 
394
395
  function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
395
- if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner" || key === "hideUnfocusedPrompt") {
396
+ if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner" || key === "hideUnfocusedPrompt" || key === "showAutonomousRuns") {
396
397
  return config[key] === true ? "true" : "false";
397
398
  }
398
399
  if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
@@ -87,7 +87,7 @@ pi.registerTool(defineTool({
87
87
  }
88
88
  if (params.cursor || params.task_id) return {content: [{type: "text", text: "Use section=objective, tasks, or history for detail retrieval; task_id requires tasks."}], details: goalDetails(view)};
89
89
  if (verbose && !params.section) {
90
- const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`, schedulerSummary(view.scheduler, loadGoalSettings(ctx.cwd).maxAutonomousRuns)];
90
+ const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`, schedulerSummary(view.scheduler, loadGoalSettings(ctx.cwd).maxAutonomousRuns, loadGoalSettings(ctx.cwd).showAutonomousRuns)];
91
91
  lines.push(`Objective: ${view.objective}`, "");
92
92
  lines.push(`Status: ${statusLabel(view)}`);
93
93
  lines.push(`Mode: ${view.sisyphus ? "sisyphus" : "regular"}`);
@@ -130,7 +130,7 @@ pi.registerTool(defineTool({
130
130
  }
131
131
 
132
132
  // Compact state read; full requirements remain available through detail pages.
133
- const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`, schedulerSummary(view.scheduler, loadGoalSettings(ctx.cwd).maxAutonomousRuns)];
133
+ const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`, schedulerSummary(view.scheduler, loadGoalSettings(ctx.cwd).maxAutonomousRuns, loadGoalSettings(ctx.cwd).showAutonomousRuns)];
134
134
  lines.push(`Objective: ${truncateText(view.objective, 180)}${view.objective.length > 180 ? " (full: get_goal section=objective)" : ""}`);
135
135
  if (view.taskList) {
136
136
  const { findCurrentTask, firstPendingTask } = conciseTaskPointers(view);
@@ -515,7 +515,7 @@ pi.registerTool(defineTool({
515
515
  parameters: Type.Object({
516
516
  status: Type.Optional(StringEnum(["complete", "blocked", "paused"] as const, { description: "Exclusive with continuation." })),
517
517
  continuation: Type.Optional(Type.Union([
518
- Type.Object({ kind: Type.Literal("ready"), next_action: Type.String({ minLength: 1, maxLength: 2000 }) }, { additionalProperties: false }),
518
+ Type.Object({ kind: Type.Literal("ready") }, { additionalProperties: false }),
519
519
  Type.Object({ kind: Type.Literal("wait"), reason: Type.String({ minLength: 1, maxLength: 2000 }), deadline: Type.String(), wait_id: Type.Optional(Type.String()), polling: Type.Optional(Type.Object({ interval_seconds: Type.Integer({ minimum: 1, maximum: 2147483 }), max_checks: Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }) }, { additionalProperties: false })) }, { additionalProperties: false }),
520
520
  ])),
521
521
  reason: Type.Optional(Type.String({ description: "Required when status is paused or blocked: describe the concrete blocker." })),
@@ -311,6 +311,7 @@ export function registerDraftingTools(core: GoalCore): void {
311
311
  promptGuidelines: [
312
312
  "Use only during a /goal, /sisyphus, or /goal-tweak guided draft.",
313
313
  "Clarify ambiguity before proposing. Include tasks when the work naturally decomposes into trackable milestones; omit them for genuinely simple work.",
314
+ "Set token_budget only when the user explicitly asked for a token budget or a spend limit. Never invent, estimate, or infer one from the objective's size; omitting it creates a goal with no budget.",
314
315
  "Confirmation creates or revises the goal atomically. Continue Chatting leaves drafting active for refinement.",
315
316
  ],
316
317
  parameters: Type.Object({
@@ -490,7 +491,7 @@ export function registerDraftingTools(core: GoalCore): void {
490
491
  // Resume glue mirrors replaceGoal: restart accounting and queue
491
492
  // the auto-continuation so the revived goal keeps going.
492
493
  core.beginAccounting();
493
- const scheduling = core.scheduler.declare(ctx, {kind: "ready", next_action: "Continue the goal after the confirmed tweak."});
494
+ const scheduling = core.scheduler.declare(ctx, {kind: "ready"});
494
495
  if (!scheduling.terminate) schedulingNote = "\n" + scheduling.content.filter(item => item.type === "text").map(item => item.text).join("\n");
495
496
  core.queueContinuation(ctx, true);
496
497
  }
@@ -317,6 +317,27 @@ export function registerGoalEvents(core: GoalCore): void {
317
317
  core.queueContinuation(ctx, true);
318
318
  });
319
319
 
320
+ type SessionCompactFailed = { reason: "manual" | "threshold" | "overflow"; errorMessage?: string; aborted: boolean; willRetry: boolean; fromExtension: boolean };
321
+ // session_compact_failed is new in pi 1.0.0. The supported peer range also
322
+ // includes 0.83 through 0.87, where this event does not exist and is never
323
+ // emitted, so the registration is typed loosely enough to compile against
324
+ // every supported host. On those hosts the handler is simply never called.
325
+ type CompactFailedOn = (event: "session_compact_failed", handler: (event: SessionCompactFailed, ctx: ExtensionContext) => unknown) => unknown;
326
+ (pi.on as unknown as CompactFailedOn)("session_compact_failed", async (event, ctx) => {
327
+ // `session_before_compact` already charged progress to the goal, so
328
+ // flush the buffered transaction and persist first. Otherwise the goal
329
+ // record and the ledger can disagree about a compaction that never
330
+ // happened.
331
+ core.goalService.flushTurn(ctx);
332
+ if (core.state.goal) core.persist(ctx);
333
+ if (event.aborted || !core.state.goal) return;
334
+ const detail = event.errorMessage ? `: ${event.errorMessage}` : "";
335
+ ctx.ui.notify(
336
+ `Goal context compaction failed (${event.reason})${detail}. The goal stays active; retry with /compact or keep working.`,
337
+ "warning",
338
+ );
339
+ });
340
+
320
341
  pi.on("session_tree", async (_event, ctx) => {
321
342
  core.auditMessages.clear();
322
343
  core.goalService.flushTurn(ctx); // P1-3: persist any buffered transaction before reload
@@ -17,13 +17,13 @@ export interface GoalSchedulerState {
17
17
  generation: string;
18
18
  used: number;
19
19
  phase: "idle" | "ready" | "waiting" | "claimed" | "running" | "interrupted";
20
- decision?: { kind: "ready"; nextAction: string; purpose: GoalDispatchKind } | { kind: "wait" };
20
+ decision?: { kind: "ready"; purpose: GoalDispatchKind } | { kind: "wait" };
21
21
  wait?: GoalWait;
22
22
  dispatch?: { id: string; kind: GoalDispatchKind; claimedAt: number };
23
23
  repairUsed: boolean;
24
24
  }
25
25
  export type GoalContinuation =
26
- | { kind: "ready"; next_action: string }
26
+ | { kind: "ready" }
27
27
  | { kind: "wait"; reason: string; deadline: string; wait_id?: string; polling?: { interval_seconds: number; max_checks: number } };
28
28
 
29
29
  export function newGoalScheduler(owner: string): GoalSchedulerState {
@@ -39,7 +39,10 @@ export function normalizeGoalScheduler(raw: unknown): GoalSchedulerState | undef
39
39
  const integer = (n: unknown) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
40
40
  const text = (v: unknown) => typeof v === "string" && v.length > 0 && v.length <= 2000;
41
41
  if (s.version !== 1 || !text(s.owner) || !text(s.generation) || !integer(s.used) || typeof s.repairUsed !== "boolean" || !["idle", "ready", "waiting", "claimed", "running", "interrupted"].includes(s.phase)) return invalid();
42
- if (s.decision && (s.decision.kind !== "wait" && (s.decision.kind !== "ready" || !text(s.decision.nextAction) || !["ready", "repair", "kickoff", "recovery"].includes(s.decision.purpose)))) return invalid();
42
+ // Goals saved before next actions were removed still carry decision.nextAction.
43
+ // The field is stripped after validation and never makes a goal invalid:
44
+ // rejecting one would strand it as interrupted with spent allowance.
45
+ if (s.decision && (s.decision.kind !== "wait" && (s.decision.kind !== "ready" || !["ready", "repair", "kickoff", "recovery"].includes(s.decision.purpose)))) return invalid();
43
46
  if (s.wait) {
44
47
  const w = s.wait;
45
48
  if (!text(w.id) || !text(w.token) || !text(w.reason) || (!integer(w.deadline) || w.deadline > 8_640_000_000_000_000)) return invalid();
@@ -51,7 +54,9 @@ export function normalizeGoalScheduler(raw: unknown): GoalSchedulerState | undef
51
54
  if ((s.phase === "claimed" || s.phase === "running") && !s.dispatch) return invalid();
52
55
  if (s.phase === "ready" && s.decision?.kind !== "ready") return invalid();
53
56
  if (s.phase === "waiting" && (!s.wait || s.decision?.kind !== "wait")) return invalid();
54
- return structuredClone(s);
57
+ const normalized = structuredClone(s) as GoalSchedulerState;
58
+ if (normalized.decision && "nextAction" in normalized.decision) delete (normalized.decision as Record<string, unknown>).nextAction;
59
+ return normalized;
55
60
  }
56
61
 
57
62
  /**
@@ -60,11 +65,14 @@ export function normalizeGoalScheduler(raw: unknown): GoalSchedulerState | undef
60
65
  * omission, so a retained stale copy would keep issuing a cancelled order:
61
66
  * they belong with the reset-on-change state, never with retained counters.
62
67
  */
63
- export function schedulerSummaryParts(s: GoalSchedulerState | undefined, limit?: number): { runs: string; instructions: string } {
64
- const runs = `Autonomous runs: ${s?.used ?? 0}/${limit ?? "unlimited"}${limit === 0 ? " (automatic continuation disabled)" : ""}.`;
68
+ export function schedulerSummaryParts(s: GoalSchedulerState | undefined, limit?: number, showRuns = true): { runs: string; instructions: string } {
69
+ // An unlimited allowance has no limit to report, so the runs line is left
70
+ // empty rather than rendered as "used/unlimited". A finite allowance is
71
+ // reported unless showAutonomousRuns is off. Enforcement reads the limit
72
+ // elsewhere; this is display only.
73
+ const runs = limit !== undefined && showRuns ? `Autonomous runs: ${s?.used ?? 0}/${limit}${limit === 0 ? " (automatic continuation disabled)" : ""}.` : "";
65
74
  const lines: string[] = [];
66
75
  if (s) {
67
- if (s.decision?.kind === "ready") lines.push(`Next action: ${s.decision.nextAction}`);
68
76
  if (s.wait) {
69
77
  lines.push(`Waiting: ${s.wait.reason}; wait_id=${s.wait.id}; deadline=${new Date(s.wait.deadline).toISOString()}.`);
70
78
  if (s.wait.nextCheckAt !== undefined) lines.push(`Next check: ${new Date(s.wait.nextCheckAt).toISOString()}; ${s.wait.remainingChecks} checks remaining.`);
@@ -74,7 +82,7 @@ export function schedulerSummaryParts(s: GoalSchedulerState | undefined, limit?:
74
82
  return { runs, instructions: lines.join("\n") };
75
83
  }
76
84
 
77
- export function schedulerSummary(s: GoalSchedulerState | undefined, limit?: number): string {
78
- const { runs, instructions } = schedulerSummaryParts(s, limit);
79
- return instructions ? `${runs}\n${instructions}` : runs;
85
+ export function schedulerSummary(s: GoalSchedulerState | undefined, limit?: number, showRuns = true): string {
86
+ const { runs, instructions } = schedulerSummaryParts(s, limit, showRuns);
87
+ return [runs, instructions].filter(Boolean).join("\n");
80
88
  }
@@ -26,7 +26,7 @@ export class GoalScheduler {
26
26
  }
27
27
  private implicitReady(s: GoalSchedulerState): void {
28
28
  s.phase = "ready"; s.dispatch = undefined; s.generation = randomUUID();
29
- s.decision = { kind: "ready", nextAction: "Continue pursuing the goal, then verify and complete it when satisfied.", purpose: "ready" };
29
+ s.decision = { kind: "ready", purpose: "ready" };
30
30
  }
31
31
  private available(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
32
32
  const limit = this.limit(ctx);
@@ -123,7 +123,7 @@ export class GoalScheduler {
123
123
  this.write(ctx, g => {
124
124
  if (g.status === "complete" || budgetReached(g)) throw new Error("A completed or token-budget-limited goal cannot resume.");
125
125
  return { ...g, status: "active", autoContinue: true, stopReason: undefined, pauseReason: undefined, pauseSuggestedAction: undefined,
126
- scheduler: { ...newGoalScheduler(this.owner(ctx)), phase: "ready", decision: { kind: "ready", nextAction: "Continue the goal at the user's request.", purpose: "kickoff" } } };
126
+ scheduler: { ...newGoalScheduler(this.owner(ctx)), phase: "ready", decision: { kind: "ready", purpose: "kickoff" } } };
127
127
  });
128
128
  // A resume during host work waits for settlement; another model turn
129
129
  // still invalidates it through turn(), just like a declaration.
@@ -138,7 +138,7 @@ export class GoalScheduler {
138
138
  const goal = this.core.state.goal;
139
139
  if (!goal || goal.status !== "active" || !goal.autoContinue) return;
140
140
  if (this.limit(ctx) === 0) { this.update(ctx, () => {}); ctx.ui.notify(this.allowanceReason(ctx), "info"); return; }
141
- this.update(ctx, s => { s.phase = "ready"; s.decision = { kind: "ready", nextAction: "Begin the requested goal.", purpose: "kickoff" }; });
141
+ this.update(ctx, s => { s.phase = "ready"; s.decision = { kind: "ready", purpose: "kickoff" }; });
142
142
  this.schedule(ctx);
143
143
  });
144
144
  }
@@ -150,8 +150,7 @@ export class GoalScheduler {
150
150
  const goal = this.update(ctx, s => {
151
151
  if (!this.available(ctx, s)) throw new Error(this.allowanceReason(ctx));
152
152
  if (input.kind === "ready") {
153
- if (typeof input.next_action !== "string" || !input.next_action.trim() || input.next_action.length > 2000) throw new Error("ready requires a nonempty next_action (at most 2000 characters).");
154
- s.phase = "ready"; s.decision = { kind: "ready", nextAction: input.next_action.trim(), purpose: "ready" }; s.wait = undefined;
153
+ s.phase = "ready"; s.decision = { kind: "ready", purpose: "ready" }; s.wait = undefined;
155
154
  } else if (input.kind === "wait") {
156
155
  if (!this.strict(ctx, s)) throw new Error("New waits require strictExecutionContract=true in /goal-settings. This is a user preference; continue pursuing the goal without a wait unless the user opts in.");
157
156
  if (typeof input.reason !== "string" || !input.reason.trim() || input.reason.length > 2000) throw new Error("wait requires a nonempty reason (at most 2000 characters).");
@@ -177,7 +176,7 @@ export class GoalScheduler {
177
176
  this.declared = true;
178
177
  this.core.runtime.markTurnStopped(goal.id);
179
178
  const wait = goal.scheduler?.wait;
180
- return { content: [{ type: "text", text: `Scheduling decision saved. Stop this execution.\n${schedulerSummary(goal.scheduler, this.limit(ctx))}${wait ? `\nWake token: ${wait.token}. Register this token with the producer before completion; adapters must retain early results.` : ""}` }], details: { goal, ...(wait ? { wait_id: wait.id, waitToken: wait.token } : {}) }, terminate: true };
179
+ return { content: [{ type: "text", text: `Scheduling decision saved. Stop this execution.\n${schedulerSummary(goal.scheduler, this.limit(ctx), loadGoalSettings(ctx.cwd).showAutonomousRuns)}${wait ? `\nWake token: ${wait.token}. Register this token with the producer before completion; adapters must retain early results.` : ""}` }], details: { goal, ...(wait ? { wait_id: wait.id, waitToken: wait.token } : {}) }, terminate: true };
181
180
  } catch (error) { return { content: [{ type: "text", text: `Scheduling decision NOT saved: ${error instanceof Error ? error.message : String(error)}` }], details: { error: true }, terminate: false }; }
182
181
  }
183
182
 
@@ -248,14 +247,14 @@ export class GoalScheduler {
248
247
  }
249
248
  this.update(ctx, state => {
250
249
  state.phase = "ready"; state.dispatch = undefined;
251
- state.decision = { kind: "ready", nextAction: "The prior execution did not declare a disposition. Declare ready with a concrete next action, wait with a deadline, or complete/pause/block. This is the only repair prompt.", purpose: "repair" };
250
+ state.decision = { kind: "ready", purpose: "repair" };
252
251
  });
253
252
  this.schedule(ctx);
254
253
  });
255
254
  }
256
255
  recover(ctx: ExtensionContext): void {
257
256
  this.safe(ctx, () => {
258
- this.update(ctx, s => { s.phase = "ready"; s.dispatch = undefined; s.decision = { kind: "ready", nextAction: this.strict(ctx, s) ? "Retry after the provider error, then declare an execution disposition." : "Retry after the provider error and continue pursuing the goal.", purpose: "recovery" }; });
257
+ this.update(ctx, s => { s.phase = "ready"; s.dispatch = undefined; s.decision = { kind: "ready", purpose: "recovery" }; });
259
258
  this.schedule(ctx);
260
259
  });
261
260
  }
@@ -104,6 +104,12 @@ export interface GoalSettingsResolvedShape {
104
104
  stallTimeoutMinutes?: number;
105
105
  /** Optional extension-run limit per creation/resume; zero disables, absent inherits (default unlimited). */
106
106
  maxAutonomousRuns?: number;
107
+ /**
108
+ * Report the "Autonomous runs: used/limit" line for a finite run allowance
109
+ * (default true). The line reports a limit, so it appears exactly when a
110
+ * finite allowance exists. Display only; enforcement is unaffected.
111
+ */
112
+ showAutonomousRuns?: boolean;
107
113
  strictExecutionContract?: boolean;
108
114
  /**
109
115
  * Maximum objective length in characters (0/unset = no limit, the
@@ -339,6 +345,7 @@ const ALLOWED_SETTINGS_KEYS = new Set([
339
345
  "objectiveMaxChars",
340
346
  "keybindings",
341
347
  "hideUnfocusedBanner",
348
+ "showAutonomousRuns",
342
349
  "hideUnfocusedPrompt",
343
350
  "goalsRoot",
344
351
  "oracle",
@@ -394,6 +401,7 @@ export function parseSettingsLayer(
394
401
  case "autoSelectSingleGoal":
395
402
  case "auditorProjectResources":
396
403
  case "hideUnfocusedBanner":
404
+ case "showAutonomousRuns":
397
405
  case "hideUnfocusedPrompt": {
398
406
  const parsed = asBool(value);
399
407
  if (parsed === undefined) {
@@ -769,6 +777,11 @@ function resolvedSettingsSnapshot(cwd: string, env: NodeJS.ProcessEnv): Settings
769
777
  globalValue: global.layer.hideUnfocusedBanner,
770
778
  defaultValue: false,
771
779
  }));
780
+ const showAutonomousRuns = track("showAutonomousRuns", resolveLeaf<boolean>({
781
+ projectValue: project.layer.showAutonomousRuns,
782
+ globalValue: global.layer.showAutonomousRuns,
783
+ defaultValue: true,
784
+ }));
772
785
  const hideUnfocusedPrompt = track("hideUnfocusedPrompt", resolveLeaf<boolean>({
773
786
  projectValue: project.layer.hideUnfocusedPrompt,
774
787
  globalValue: global.layer.hideUnfocusedPrompt,
@@ -877,6 +890,7 @@ function resolvedSettingsSnapshot(cwd: string, env: NodeJS.ProcessEnv): Settings
877
890
  autoSelectSingleGoal,
878
891
  auditorProjectResources,
879
892
  hideUnfocusedBanner,
893
+ showAutonomousRuns,
880
894
  hideUnfocusedPrompt,
881
895
  goalsRoot,
882
896
  stallTimeoutMinutes,
@@ -1213,6 +1227,7 @@ function buildPersistedLayer(settings: GoalSettings): Record<string, unknown> {
1213
1227
  if (settings.autoSelectSingleGoal !== undefined) persisted.autoSelectSingleGoal = settings.autoSelectSingleGoal;
1214
1228
  if (settings.auditorProjectResources !== undefined) persisted.auditorProjectResources = settings.auditorProjectResources;
1215
1229
  if (settings.hideUnfocusedBanner !== undefined) persisted.hideUnfocusedBanner = settings.hideUnfocusedBanner;
1230
+ if (settings.showAutonomousRuns !== undefined) persisted.showAutonomousRuns = settings.showAutonomousRuns;
1216
1231
  if (settings.goalsRoot !== undefined) persisted.goalsRoot = settings.goalsRoot;
1217
1232
  if (settings.hideUnfocusedPrompt !== undefined) persisted.hideUnfocusedPrompt = settings.hideUnfocusedPrompt;
1218
1233
  if ((settings as { networkRecovery?: ResolvedGoalNetworkRecoverySettings }).networkRecovery) {
@@ -1279,6 +1294,7 @@ export function effectiveSettingsReport(cwd: string, env: NodeJS.ProcessEnv = pr
1279
1294
  { key: "thinkingLevel", label: "thinking_level", format: () => snapshot.value.thinkingLevel ?? "(default)" },
1280
1295
  { key: "auditorProjectResources", label: "auditor project resources", format: () => String(snapshot.value.auditorProjectResources) },
1281
1296
  { key: "hideUnfocusedBanner", label: "hide unfocused banner", format: () => String(snapshot.value.hideUnfocusedBanner) },
1297
+ { key: "showAutonomousRuns", label: "show autonomous runs line", format: () => String(snapshot.value.showAutonomousRuns) },
1282
1298
  { key: "goalsRoot", label: "goals root (reload to change)", format: () => snapshot.value.goalsRoot ?? ".pi/goals" },
1283
1299
  { key: "hideUnfocusedPrompt", label: "hide unfocused prompt", format: () => String(snapshot.value.hideUnfocusedPrompt) },
1284
1300
  { key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", format: () => String(snapshot.value.strictExecutionContract) },
@@ -188,7 +188,7 @@ function lifecyclePolicyBlock(autonomous: boolean, strict: boolean): string {
188
188
  "[OUTCOMES]",
189
189
  '- Automatic runs default to unlimited. maxAutonomousRuns in .pi/pi-goal-x-settings.json caps runs; 0 disables (agents may set it). Only creation or user /goal-resume renews usage.',
190
190
  ...(autonomous && strict ? ['- End execution with update_goal: ready for runnable work, wait for an external condition, or a status below. Saved decisions terminate; further work invalidates them. Missing decisions allow one repair. Never busy-poll.'] : []),
191
- ...(autonomous && !strict ? ['- Continue pursuing the goal automatically after each execution; no scheduling declaration is required. Optional ready saves a next action. New waits require user opt-in to strictExecutionContract; do not enable it merely to continue.'] : []),
191
+ ...(autonomous && !strict ? ['- Continue pursuing the goal automatically after each execution; no scheduling declaration is required. New waits require user opt-in to strictExecutionContract; do not enable it merely to continue.'] : []),
192
192
  '- update_goal({status: "complete"}) only when every requirement is satisfied; the independent completion auditor checks actual evidence. Approval archives; rejection requires rework.',
193
193
  '- update_goal({status: "blocked"}) only after the SAME blocker recurs on three consecutive goal turns; keep trying concrete steps before then.',
194
194
  '- update_goal({status: "paused", reason: "…"}) pauses immediately. User controls: /goal-pause, /goal-resume, /goal-clear.',
@@ -235,7 +235,7 @@ export function goalPromptParts(goal: GoalRecord, settings?: GoalSettings, conte
235
235
  // Scheduling instructions ride with the policy block: they are cleared by
236
236
  // omission, so a retained copy would keep issuing a cancelled order.
237
237
  const fixed = cachedPrompt(goal, settings, "goal", () => buildGoalPrompt(goal, settings));
238
- const { runs, instructions } = schedulerSummaryParts(goal.scheduler, settings?.maxAutonomousRuns);
238
+ const { runs, instructions } = schedulerSummaryParts(goal.scheduler, settings?.maxAutonomousRuns, settings?.showAutonomousRuns);
239
239
  const limits = `Limits: lifetime tokens=${goal.tokenBudget ?? "none"}; runs=${settings?.maxAutonomousRuns ?? "unlimited"}.`;
240
240
  const state = [fixed, instructions, limits, "Usage spans goal turns, not context. Latest snapshot supersedes earlier snapshots."].filter(Boolean).join("\n");
241
241
  const counters = `Goal snapshot: ${formatUsage(goal)}\n${contextUsageLine(contextUsage)}\n${runs}`;
@@ -103,6 +103,8 @@ export interface DashboardTaskNode {
103
103
 
104
104
  export interface GoalDashboardModelOptions {
105
105
  maxAutonomousRuns?: number;
106
+ /** Report the runs line for a finite allowance (default on). */
107
+ showAutonomousRuns?: boolean;
106
108
  focused: boolean;
107
109
  otherOpenGoals: number;
108
110
  ledgerEvents?: readonly GoalLedgerEvent[];
@@ -498,7 +500,7 @@ export function deriveGoalDashboardModel(
498
500
  if (goal.usage.tokensUsed > 0) footerUsageBits.push(formatCompactTokens(goal.usage.tokensUsed));
499
501
 
500
502
  return {
501
- scheduling: schedulerSummary(goal.scheduler, options.maxAutonomousRuns).split("\n"),
503
+ scheduling: schedulerSummary(goal.scheduler, options.maxAutonomousRuns, options.showAutonomousRuns !== false).split("\n").filter(Boolean),
502
504
  goalId: goal.id,
503
505
  title: displayObjectiveTitle(goal.objective),
504
506
  status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.31.8",
3
+ "version": "0.32.0",
4
4
  "description": "Adds /goal to pi: conversational goal planning, flexible or ordered goals, persistent progress, and an independent completion auditor.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",
@@ -74,15 +74,15 @@
74
74
  "bench:gate:comprehensive": "node experiments/bench/comprehensive-gate.mjs"
75
75
  },
76
76
  "peerDependencies": {
77
- "@earendil-works/pi-ai": ">=0.83.0 <0.88.0",
78
- "@earendil-works/pi-coding-agent": ">=0.83.0 <0.88.0",
79
- "@earendil-works/pi-tui": ">=0.83.0 <0.88.0",
77
+ "@earendil-works/pi-ai": ">=0.83.0 <2.0.0",
78
+ "@earendil-works/pi-coding-agent": ">=0.83.0 <2.0.0",
79
+ "@earendil-works/pi-tui": ">=0.83.0 <2.0.0",
80
80
  "typebox": "^1.3.11"
81
81
  },
82
82
  "devDependencies": {
83
- "@earendil-works/pi-ai": "0.87.0",
84
- "@earendil-works/pi-coding-agent": "0.87.0",
85
- "@earendil-works/pi-tui": "0.87.0",
83
+ "@earendil-works/pi-ai": "1.0.0",
84
+ "@earendil-works/pi-coding-agent": "1.0.0",
85
+ "@earendil-works/pi-tui": "1.0.0",
86
86
  "@eslint/js": "^9.39.5",
87
87
  "@xterm/headless": "^6.0.0",
88
88
  "eslint": "^9.39.5",