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 +5 -2
- package/assets/badge-dark.svg +3 -3
- package/assets/badge-light.svg +3 -3
- package/docs/advanced-usage.md +8 -2
- package/docs/agent-flow-design.md +1 -1
- package/extensions/goal-commands.ts +2 -1
- package/extensions/goal-core-tools.ts +3 -3
- package/extensions/goal-drafting.ts +2 -1
- package/extensions/goal-events.ts +21 -0
- package/extensions/goal-scheduler-state.ts +18 -10
- package/extensions/goal-scheduler.ts +7 -8
- package/extensions/goal-settings.ts +16 -0
- package/extensions/prompts/goal-prompts.ts +2 -2
- package/extensions/widgets/goal-dashboard-model.ts +3 -1
- package/package.json +7 -7
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.
|
|
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://
|
|
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. |
|
package/assets/badge-dark.svg
CHANGED
|
@@ -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.
|
|
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.
|
|
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">#
|
|
16
|
+
<text class="meta" x="143" y="41">#6 of 3,258 by downloads · Oct 1, 2026</text>
|
|
17
17
|
</a>
|
|
18
18
|
</svg>
|
package/assets/badge-light.svg
CHANGED
|
@@ -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.
|
|
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.
|
|
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">#
|
|
16
|
+
<text class="meta" x="143" y="41">#6 of 3,258 by downloads · Oct 1, 2026</text>
|
|
17
17
|
</a>
|
|
18
18
|
</svg>
|
package/docs/advanced-usage.md
CHANGED
|
@@ -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.
|
|
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"
|
|
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
|
|
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")
|
|
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"
|
|
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";
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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.
|
|
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.
|
|
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.
|
|
78
|
-
"@earendil-works/pi-coding-agent": ">=0.83.0 <0.
|
|
79
|
-
"@earendil-works/pi-tui": ">=0.83.0 <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.
|
|
84
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
85
|
-
"@earendil-works/pi-tui": "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",
|