pi-goal-x 0.31.4 → 0.31.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
@@ -6,7 +6,7 @@
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,200 by downloads · Sep 14, 2026 (best recorded rank)" width="480">
9
+ <img src="assets/badge-light.svg" alt="TOP 0.3% of Pi coding agent extensions: #7 of 3,352 by downloads · Sep 16, 2026 (best recorded rank)" width="480">
10
10
  </picture>
11
11
  </a>
12
12
  </div>
@@ -115,7 +115,8 @@ Open `/goal-settings` to change these options. You can save defaults for all pro
115
115
 
116
116
  | Setting | What it controls |
117
117
  | --- | --- |
118
- | Autonomous run allowance (`maxAutonomousRuns`) | Positive whole number of extension-started runs per creation or `/goal-resume` period. **Unset disables automatic continuation.** Settings edits change the limit without resetting usage. |
118
+ | Explicit execution contracts (`strictExecutionContract`) | Opt-in ready/wait protocol with one missing-decision repair, then pause. Defaults to `false`: successful executions continue automatically. |
119
+ | 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. |
119
120
  | Task tracking (`disableTasks`) | Turn task lists on or off. Set to `true` to disable them. |
120
121
  | Subtask depth (`subtaskDepth`) | Limit how many levels of subtasks the agent can create. |
121
122
  | Completion requirements (`disableContracts`) | Turn explicit goal and task completion requirements on or off. Set to `true` to disable them. |
@@ -123,9 +124,11 @@ Open `/goal-settings` to change these options. You can save defaults for all pro
123
124
  | Auditor provider, model, and thinking level | Choose which model reviews completed work and its reasoning effort. |
124
125
 
125
126
 
126
- ### Explicit execution and waiting
127
+ ### Automatic continuation and optional execution contracts
127
128
 
128
- Goals no longer restart merely because they remain unfinished or a tool was used. Before yielding, the agent declares runnable work or an external wait using `update_goal`, or reports complete, paused, or blocked. A missing decision permits one repair prompt within the remaining allowance, then pauses.
129
+ Active goals continue automatically after successful executions, including reasoning-only responses and final-task verification. No tool call, task update, scheduling declaration, or cooldown is required. Unproductive loops remain possible; optional run limits and token budgets still apply.
130
+
131
+ Enable `strictExecutionContract: true` in `/goal-settings` or your global/project settings to require explicit ready/wait decisions. In that mode, a missing decision permits one repair prompt within the remaining allowance, then pauses. This is a user preference; agents should not enable it merely to continue.
129
132
 
130
133
  Set an appropriate allowance in `/goal-settings`, or in `.pi/pi-goal-x-settings.json`:
131
134
 
@@ -133,7 +136,9 @@ Set an appropriate allowance in `/goal-settings`, or in `.pi/pi-goal-x-settings.
133
136
  { "maxAutonomousRuns": 20 }
134
137
  ```
135
138
 
136
- 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. It requires a configured allowance. Tool calls within a run are not separate runs. Existing token budgets still apply.
139
+ 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.
140
+
141
+ 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.
137
142
 
138
143
  ```js
139
144
  update_goal({ continuation: { kind: "ready", next_action: "Verify the build artifacts" } })
@@ -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,200 by downloads · Sep 14, 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.3% of Pi coding agent extensions: #7 of 3,352 by downloads · Sep 16, 2026 (best recorded rank)">
2
2
  <!-- Dark mode -->
3
3
  <style>
4
4
  .frame { fill: #14181f; stroke: #2a3040; stroke-width: 1; }
@@ -13,6 +13,6 @@
13
13
  <text class="rank" x="18" y="34">TOP 0.3%</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,200 by downloads · Sep 14, 2026</text>
16
+ <text class="meta" x="143" y="41">#7 of 3,352 by downloads · Sep 16, 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,200 by downloads · Sep 14, 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.3% of Pi coding agent extensions: #7 of 3,352 by downloads · Sep 16, 2026 (best recorded rank)">
2
2
  <!-- Light mode -->
3
3
  <style>
4
4
  .frame { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1; }
@@ -13,6 +13,6 @@
13
13
  <text class="rank" x="18" y="34">TOP 0.3%</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,200 by downloads · Sep 14, 2026</text>
16
+ <text class="meta" x="143" y="41">#7 of 3,352 by downloads · Sep 16, 2026</text>
17
17
  </a>
18
18
  </svg>
@@ -370,6 +370,7 @@ export function registerGoalCommands(core: GoalCore): void {
370
370
  { key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
371
371
  { key: "hideUnfocusedBanner", label: "hideUnfocusedBanner", section: "Goal behavior", kind: "boolean" },
372
372
  { key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
373
+ { key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", section: "Goal behavior", kind: "boolean" },
373
374
  { key: "maxAutonomousRuns", label: "autonomous run allowance", section: "Goal behavior", kind: "positiveInteger" },
374
375
  { key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
375
376
  { key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
@@ -389,7 +390,7 @@ export function registerGoalCommands(core: GoalCore): void {
389
390
  ];
390
391
 
391
392
  function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
392
- if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
393
+ if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
393
394
  return config[key] === true ? "true" : "false";
394
395
  }
395
396
  if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
@@ -509,8 +509,8 @@ async function runGoalAgentPauseFlow(ctx: ExtensionContext, reason: string | und
509
509
  pi.registerTool(defineTool({
510
510
  name: "update_goal",
511
511
  label: "Update Goal",
512
- description: "End execution with status OR continuation. Wait: future ISO deadline; reuse wait_id/deadline without polling on recheck. Completion is audited.",
513
- promptSnippet: "Declare the next execution disposition or complete, block, or pause.",
512
+ description: "Update lifecycle status or optionally save continuation. New waits require strictExecutionContract opt-in. Wait: future ISO deadline; reuse wait_id/deadline without polling on recheck. Completion is audited.",
513
+ promptSnippet: "Complete, block, pause, or optionally save a scheduling decision.",
514
514
  promptGuidelines: [],
515
515
  parameters: Type.Object({
516
516
  status: Type.Optional(StringEnum(["complete", "blocked", "paused"] as const, { description: "Exclusive with continuation." })),
@@ -23,6 +23,13 @@ export class GoalScheduler {
23
23
 
24
24
  private owner(ctx: ExtensionContext): string { return ctx.sessionManager.getSessionId() || "unknown-session"; }
25
25
  private limit(ctx: ExtensionContext): number | undefined { return loadGoalSettings(ctx.cwd).maxAutonomousRuns; }
26
+ private strict(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
27
+ return loadGoalSettings(ctx.cwd).strictExecutionContract === true || !!s.wait;
28
+ }
29
+ private implicitReady(s: GoalSchedulerState): void {
30
+ s.phase = "ready"; s.dispatch = undefined; s.generation = randomUUID();
31
+ s.decision = { kind: "ready", nextAction: "Continue pursuing the goal, then verify and complete it when satisfied.", purpose: "ready" };
32
+ }
26
33
  private available(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
27
34
  const limit = this.limit(ctx);
28
35
  return limit === undefined || s.used < limit;
@@ -148,6 +155,7 @@ export class GoalScheduler {
148
155
  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).");
149
156
  s.phase = "ready"; s.decision = { kind: "ready", nextAction: input.next_action.trim(), purpose: "ready" }; s.wait = undefined;
150
157
  } else if (input.kind === "wait") {
158
+ 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.");
151
159
  if (typeof input.reason !== "string" || !input.reason.trim() || input.reason.length > 2000) throw new Error("wait requires a nonempty reason (at most 2000 characters).");
152
160
  const deadline = Date.parse(input.deadline);
153
161
  if (!/T.*(?:Z|[+-]\d\d:\d\d)$/.test(input.deadline) || !Number.isSafeInteger(deadline) || deadline <= Date.now()) throw new Error("wait requires a future ISO deadline with a timezone.");
@@ -230,11 +238,16 @@ export class GoalScheduler {
230
238
  settled(ctx: ExtensionContext, successful = true): void {
231
239
  this.inRun = false;
232
240
  if (this.denied || (this.runGoalId && this.core.state.goal?.id !== this.runGoalId)) return;
241
+ invalidateGoalSettingsCache();
233
242
  this.safe(ctx, () => {
234
243
  const goal = this.core.state.goal;
235
244
  if (!goal || goal.status !== "active" || !goal.autoContinue || !successful) return;
236
245
  if (this.declared && goal.scheduler?.decision) { this.schedule(ctx); return; }
237
246
  const s = this.state(ctx, goal);
247
+ if (!this.strict(ctx, s)) {
248
+ this.update(ctx, state => this.implicitReady(state));
249
+ this.schedule(ctx); return;
250
+ }
238
251
  if (s.repairUsed || !this.available(ctx, s)) {
239
252
  this.pause(ctx, !this.available(ctx, s) ? this.allowanceReason(ctx) : "No execution disposition after the contract-repair prompt. Use /goal-resume to continue.");
240
253
  return;
@@ -248,7 +261,7 @@ export class GoalScheduler {
248
261
  }
249
262
  recover(ctx: ExtensionContext): void {
250
263
  this.safe(ctx, () => {
251
- this.update(ctx, s => { s.phase = "ready"; s.dispatch = undefined; s.decision = { kind: "ready", nextAction: "Retry after the provider error, then declare an execution disposition.", purpose: "recovery" }; });
264
+ 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" }; });
252
265
  this.schedule(ctx);
253
266
  });
254
267
  }
@@ -264,11 +277,15 @@ export class GoalScheduler {
264
277
  }
265
278
  schedule(ctx: ExtensionContext): void {
266
279
  if (this.inRun) return;
280
+ invalidateGoalSettingsCache();
267
281
  this.safe(ctx, () => {
268
282
  this.core.reconcileFocusedGoalFromDisk(ctx);
269
283
  const g = this.core.state.goal;
270
284
  if (!g || g.status !== "active" || !g.autoContinue || !g.scheduler) return;
271
- const s = this.state(ctx, g);
285
+ let s = this.state(ctx, g);
286
+ if (s.phase === "ready" && s.decision?.kind === "ready" && s.decision.purpose === "repair" && !this.strict(ctx, s)) {
287
+ s = this.update(ctx, state => this.implicitReady(state)).scheduler!;
288
+ }
272
289
  if (!["ready", "waiting"].includes(s.phase)) return;
273
290
  if (budgetReached(g)) { this.pause(ctx, "Goal token budget exhausted."); return; }
274
291
  if (!this.available(ctx, s)) { this.pause(ctx, this.allowanceReason(ctx)); return; }
@@ -299,6 +316,7 @@ export class GoalScheduler {
299
316
  if (s.generation !== generation) throw new Error("Scheduling generation changed.");
300
317
  if (g.status !== "active" || !g.autoContinue || budgetReached(g) || !this.available(ctx, s)) throw new Error(this.allowanceReason(ctx));
301
318
  if (s.wait && Date.now() >= s.wait.deadline) throw new Error("Wait deadline reached.");
319
+ if (s.phase === "ready" && s.decision?.kind === "ready" && s.decision.purpose === "repair" && !this.strict(ctx, s)) this.implicitReady(s);
302
320
  let kind = s.phase === "ready" && s.decision?.kind === "ready" ? s.decision.purpose : undefined;
303
321
  if (s.phase === "waiting" && s.wait) {
304
322
  if (s.wait.signalled) kind = "wake";
@@ -104,6 +104,7 @@ 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
+ strictExecutionContract?: boolean;
107
108
  /**
108
109
  * Maximum objective length in characters (0/unset = no limit, the
109
110
  * default; >0 caps objectives in create_goal, propose_goal_draft, and
@@ -331,6 +332,7 @@ const ALLOWED_SETTINGS_KEYS = new Set([
331
332
  "auditorProjectResources",
332
333
  "stallTimeoutMinutes",
333
334
  "maxAutonomousRuns",
335
+ "strictExecutionContract",
334
336
  "objectiveMaxChars",
335
337
  "keybindings",
336
338
  "hideUnfocusedBanner",
@@ -380,6 +382,7 @@ export function parseSettingsLayer(
380
382
 
381
383
  for (const [key, value] of Object.entries(record)) {
382
384
  switch (key) {
385
+ case "strictExecutionContract":
383
386
  case "disableTasks":
384
387
  case "disableContracts":
385
388
  case "disabled":
@@ -790,6 +793,11 @@ function resolvedSettingsSnapshot(cwd: string, env: NodeJS.ProcessEnv): Settings
790
793
  globalValue: global.layer.oracle?.maxFailedAttemptsPerBlocker,
791
794
  defaultValue: 2,
792
795
  }));
796
+ const strictExecutionContract = track("strictExecutionContract", resolveLeaf<boolean>({
797
+ projectValue: project.layer.strictExecutionContract,
798
+ globalValue: global.layer.strictExecutionContract,
799
+ defaultValue: false,
800
+ }));
793
801
  const maxAutonomousRuns = track("maxAutonomousRuns", resolveLeaf<number | undefined>({
794
802
  projectValue: project.layer.maxAutonomousRuns,
795
803
  globalValue: global.layer.maxAutonomousRuns,
@@ -855,6 +863,7 @@ function resolvedSettingsSnapshot(cwd: string, env: NodeJS.ProcessEnv): Settings
855
863
  hideUnfocusedBanner,
856
864
  stallTimeoutMinutes,
857
865
  maxAutonomousRuns,
866
+ strictExecutionContract,
858
867
  objectiveMaxChars,
859
868
  keybindings,
860
869
  networkRecovery: {
@@ -1204,6 +1213,7 @@ function buildPersistedLayer(settings: GoalSettings): Record<string, unknown> {
1204
1213
  if (so.maxFailedAttemptsPerBlocker !== undefined) o.maxFailedAttemptsPerBlocker = so.maxFailedAttemptsPerBlocker;
1205
1214
  if (Object.keys(o).length > 0) persisted.oracle = o;
1206
1215
  }
1216
+ if (settings.strictExecutionContract !== undefined) persisted.strictExecutionContract = settings.strictExecutionContract;
1207
1217
  if (settings.maxAutonomousRuns !== undefined) persisted.maxAutonomousRuns = settings.maxAutonomousRuns;
1208
1218
  if (settings.stallTimeoutMinutes !== undefined) persisted.stallTimeoutMinutes = settings.stallTimeoutMinutes;
1209
1219
  if (settings.objectiveMaxChars !== undefined) persisted.objectiveMaxChars = settings.objectiveMaxChars;
@@ -1248,6 +1258,7 @@ export function effectiveSettingsReport(cwd: string, env: NodeJS.ProcessEnv = pr
1248
1258
  { key: "thinkingLevel", label: "thinking_level", format: () => snapshot.value.thinkingLevel ?? "(default)" },
1249
1259
  { key: "auditorProjectResources", label: "auditor project resources", format: () => String(snapshot.value.auditorProjectResources) },
1250
1260
  { key: "hideUnfocusedBanner", label: "hide unfocused banner", format: () => String(snapshot.value.hideUnfocusedBanner) },
1261
+ { key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", format: () => String(snapshot.value.strictExecutionContract) },
1251
1262
  { key: "maxAutonomousRuns", label: "autonomous run allowance", format: () => snapshot.value.maxAutonomousRuns === 0 ? "0 (disabled)" : String(snapshot.value.maxAutonomousRuns ?? "unlimited (default)") },
1252
1263
  { key: "stallTimeoutMinutes", label: "stall timeout (minutes)", format: () => String(snapshot.value.stallTimeoutMinutes) },
1253
1264
  { key: "objectiveMaxChars", label: "max objective length (0 = none)", format: () => String(snapshot.value.objectiveMaxChars) },
@@ -183,11 +183,12 @@ export function sisyphusDisciplineBlock(goal: GoalRecord): string {
183
183
  }
184
184
 
185
185
  /** Shared outcome/blocker policy for active goals (bounded). */
186
- function lifecyclePolicyBlock(autonomous: boolean): string {
186
+ function lifecyclePolicyBlock(autonomous: boolean, strict: boolean): string {
187
187
  return [
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
- ...(autonomous ? ['- 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.'] : []),
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
192
  '- update_goal({status: "complete"}) only when every requirement is satisfied; the independent completion auditor checks actual evidence. Approval archives; rejection requires rework.',
192
193
  '- update_goal({status: "blocked"}) only after the SAME blocker recurs on three consecutive goal turns; keep trying concrete steps before then.',
193
194
  '- update_goal({status: "paused", reason: "…"}) pauses immediately. User controls: /goal-pause, /goal-resume, /goal-clear.',
@@ -207,7 +208,7 @@ let promptCacheChars = 0;
207
208
  function cachedPrompt(goal: GoalRecord, settings: GoalSettings | undefined, kind: "goal" | "continuation", build: () => string): string {
208
209
  const key = [kind, goal.id, goal.status, goal.autoContinue, goal.sisyphus, goal.objective,
209
210
  goal.verificationContract, settings?.disableTasks ? undefined : taskIndex(goal.taskList?.tasks),
210
- goal.taskList?.blockCompletion, promptProfile(), goal.currentTaskId, settings?.disableTasks, settings?.disableContracts, settings?.maxAutonomousRuns !== 0];
211
+ goal.taskList?.blockCompletion, promptProfile(), goal.currentTaskId, settings?.disableTasks, settings?.disableContracts, settings?.maxAutonomousRuns !== 0, settings?.strictExecutionContract === true, !!goal.scheduler?.wait];
211
212
  for (let i = promptFragmentCache.length - 1; i >= 0; i--) {
212
213
  const entry = promptFragmentCache[i]!;
213
214
  if (key.every((part, j) => part === entry.key[j])) return entry.value;
@@ -238,7 +239,7 @@ function buildGoalPrompt(goal: GoalRecord, settings?: GoalSettings): string {
238
239
  // Bound individual data fields so essential rules can never be sliced off.
239
240
  return [
240
241
  `[PI GOAL ACTIVE goalId=${goal.id}]`,
241
- lifecyclePolicyBlock(settings?.maxAutonomousRuns !== 0), sisyphusDisciplineBlock(goal),
242
+ lifecyclePolicyBlock(settings?.maxAutonomousRuns !== 0, settings?.strictExecutionContract === true || !!goal.scheduler?.wait), sisyphusDisciplineBlock(goal),
242
243
  `Status: ${statusLabel(goal)}\nMode: ${goal.sisyphus ? "sisyphus" : "regular"}`,
243
244
  untrustedObjectiveBlock(goal), taskListBlock(goal, settings), verificationContractBlock(goal, settings),
244
245
  ].filter(Boolean).join("\n\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.31.4",
3
+ "version": "0.31.5",
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",