pi-goal 0.1.5 → 0.1.6

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.
@@ -0,0 +1,115 @@
1
+ export type GoalStatus = "active" | "paused" | "budget_limited" | "complete";
2
+
3
+ export type GoalState = {
4
+ version: 1;
5
+ id: string;
6
+ objective: string;
7
+ status: GoalStatus;
8
+ tokenBudget: number | null;
9
+ tokensUsed: number;
10
+ timeUsedSeconds: number;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+ };
14
+
15
+ export type GoalEventKind = "active" | "continuation" | "paused" | "resumed" | "cleared" | "budget_limited" | "complete";
16
+
17
+ export function parseTokenBudget(input: string): { objective: string; tokenBudget: number | null; error?: string } {
18
+ const match = input.match(/(?:^|\s)--tokens(?:=|\s+)(\S+\s*[kKmM]?)(?:\s|$)/);
19
+ if (!match) return { objective: input.trim(), tokenBudget: null };
20
+
21
+ const raw = match[1].replace(/\s+/g, "");
22
+ const suffix = raw.slice(-1).toLowerCase();
23
+ const numeric = suffix === "k" || suffix === "m" ? raw.slice(0, -1) : raw;
24
+ const value = Number(numeric);
25
+ if (!Number.isFinite(value) || value <= 0) {
26
+ return { objective: input.trim(), tokenBudget: null, error: "Token budget must be positive." };
27
+ }
28
+ const multiplier = suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
29
+ const tokenBudget = Math.round(value * multiplier);
30
+ const objective = (input.slice(0, match.index) + " " + input.slice((match.index ?? 0) + match[0].length)).trim();
31
+ return { objective, tokenBudget };
32
+ }
33
+
34
+ export function normalizeTokenBudget(value: unknown): { tokenBudget: number | null; error?: string } {
35
+ if (value == null) return { tokenBudget: null };
36
+ const tokenBudget = Math.round(Number(value));
37
+ if (!Number.isFinite(tokenBudget) || tokenBudget <= 0) {
38
+ return { tokenBudget: null, error: "tokenBudget must be a positive number when provided." };
39
+ }
40
+ return { tokenBudget };
41
+ }
42
+
43
+ export function formatTokens(value: number): string {
44
+ if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
45
+ if (value >= 1_000) return `${Math.round(value / 100) / 10}K`;
46
+ return String(value);
47
+ }
48
+
49
+ export function formatElapsed(seconds: number): string {
50
+ if (seconds < 60) return `${seconds}s`;
51
+ const minutes = Math.floor(seconds / 60);
52
+ if (minutes < 60) return `${minutes}m`;
53
+ const hours = Math.floor(minutes / 60);
54
+ const remMinutes = minutes % 60;
55
+ return remMinutes ? `${hours}h ${remMinutes}m` : `${hours}h`;
56
+ }
57
+
58
+ export function statusLine(state: GoalState | null): string | undefined {
59
+ if (!state) return undefined;
60
+ const budget = state.tokenBudget ? ` (${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)})` : ` (${formatElapsed(state.timeUsedSeconds)})`;
61
+ if (state.status === "active") return `Pursuing goal${budget}`;
62
+ if (state.status === "paused") return "Goal paused (/goal resume)";
63
+ if (state.status === "budget_limited") return state.tokenBudget ? `Goal unmet${budget}` : "Goal abandoned";
64
+ return `Goal achieved${budget}`;
65
+ }
66
+
67
+ export function goalUsage(state: GoalState): string {
68
+ if (state.tokenBudget != null) return `${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)} tokens`;
69
+ return formatElapsed(state.timeUsedSeconds);
70
+ }
71
+
72
+ export function truncateObjective(objective: string, max = 96): string {
73
+ const singleLine = objective.replace(/\s+/g, " ").trim();
74
+ return singleLine.length > max ? `${singleLine.slice(0, max - 1)}…` : singleLine;
75
+ }
76
+
77
+ export function goalEventStatus(kind: GoalEventKind): string {
78
+ const labels: Record<GoalEventKind, string> = {
79
+ active: "active",
80
+ continuation: "continuing",
81
+ paused: "paused",
82
+ resumed: "resumed",
83
+ cleared: "cleared",
84
+ budget_limited: "budget reached",
85
+ complete: "achieved",
86
+ };
87
+ return labels[kind];
88
+ }
89
+
90
+ export function createGoalState(objective: string, tokenBudget: number | null, now = Date.now(), random = Math.random()): GoalState {
91
+ return {
92
+ version: 1,
93
+ id: `${now}-${random.toString(16).slice(2)}`,
94
+ objective,
95
+ status: "active",
96
+ tokenBudget,
97
+ tokensUsed: 0,
98
+ timeUsedSeconds: 0,
99
+ createdAt: now,
100
+ updatedAt: now,
101
+ };
102
+ }
103
+
104
+ export function accountGoalTurn(state: GoalState, tokenDelta: number, elapsedSeconds: number, now = Date.now()): GoalState {
105
+ let next: GoalState = {
106
+ ...state,
107
+ tokensUsed: state.tokensUsed + Math.max(0, tokenDelta),
108
+ timeUsedSeconds: state.timeUsedSeconds + Math.max(0, elapsedSeconds),
109
+ updatedAt: now,
110
+ };
111
+ if (next.status === "active" && next.tokenBudget != null && next.tokensUsed >= next.tokenBudget) {
112
+ next = { ...next, status: "budget_limited" };
113
+ }
114
+ return next;
115
+ }
@@ -1,95 +1,29 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { Box, Spacer, Text } from "@mariozechner/pi-tui";
3
- import { tokenDeltaFromUsage } from "./usage";
3
+ import {
4
+ accountGoalTurn,
5
+ createGoalState,
6
+ goalEventStatus,
7
+ goalUsage,
8
+ parseTokenBudget,
9
+ statusLine,
10
+ truncateObjective,
11
+ type GoalEventKind,
12
+ type GoalState,
13
+ type GoalStatus,
14
+ normalizeTokenBudget,
15
+ } from "./goal-state";
16
+ import { tokenDeltaFromUsage, type UsageSnapshot } from "./usage";
4
17
 
5
18
  const CUSTOM_TYPE = "pi-goal";
6
19
  const EVENT_TYPE = "pi-goal-event";
7
20
 
8
- type GoalStatus = "active" | "paused" | "budget_limited" | "complete";
9
-
10
- type GoalState = {
11
- version: 1;
12
- id: string;
13
- objective: string;
14
- status: GoalStatus;
15
- tokenBudget: number | null;
16
- tokensUsed: number;
17
- timeUsedSeconds: number;
18
- createdAt: number;
19
- updatedAt: number;
20
- };
21
-
22
- type GoalEventKind = "active" | "continuation" | "paused" | "resumed" | "cleared" | "budget_limited" | "complete";
23
-
24
21
  let goal: GoalState | null = null;
25
22
  let statusBarEnabled = true;
26
23
  let activeTurnStartedAt: number | null = null;
24
+ let activeGoalThisTurnId: string | null = null;
27
25
  let continuationQueued = false;
28
26
 
29
- function parseTokenBudget(input: string): { objective: string; tokenBudget: number | null; error?: string } {
30
- const match = input.match(/(?:^|\s)--tokens(?:=|\s+)([0-9]+(?:\.[0-9]+)?\s*[kKmM]?)(?:\s|$)/);
31
- if (!match) return { objective: input.trim(), tokenBudget: null };
32
-
33
- const raw = match[1].replace(/\s+/g, "");
34
- const suffix = raw.slice(-1).toLowerCase();
35
- const numeric = suffix === "k" || suffix === "m" ? raw.slice(0, -1) : raw;
36
- const value = Number(numeric);
37
- if (!Number.isFinite(value) || value <= 0) {
38
- return { objective: input.trim(), tokenBudget: null, error: "Token budget must be positive." };
39
- }
40
- const multiplier = suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
41
- const tokenBudget = Math.round(value * multiplier);
42
- const objective = (input.slice(0, match.index) + " " + input.slice((match.index ?? 0) + match[0].length)).trim();
43
- return { objective, tokenBudget };
44
- }
45
-
46
- function formatTokens(value: number): string {
47
- if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
48
- if (value >= 1_000) return `${Math.round(value / 100) / 10}K`;
49
- return String(value);
50
- }
51
-
52
- function formatElapsed(seconds: number): string {
53
- if (seconds < 60) return `${seconds}s`;
54
- const minutes = Math.floor(seconds / 60);
55
- if (minutes < 60) return `${minutes}m`;
56
- const hours = Math.floor(minutes / 60);
57
- const remMinutes = minutes % 60;
58
- return remMinutes ? `${hours}h ${remMinutes}m` : `${hours}h`;
59
- }
60
-
61
- function statusLine(state: GoalState | null): string | undefined {
62
- if (!state) return undefined;
63
- const budget = state.tokenBudget ? ` (${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)})` : ` (${formatElapsed(state.timeUsedSeconds)})`;
64
- if (state.status === "active") return `Pursuing goal${budget}`;
65
- if (state.status === "paused") return "Goal paused (/goal resume)";
66
- if (state.status === "budget_limited") return state.tokenBudget ? `Goal unmet${budget}` : "Goal abandoned";
67
- return `Goal achieved${budget}`;
68
- }
69
-
70
- function goalUsage(state: GoalState): string {
71
- if (state.tokenBudget != null) return `${formatTokens(state.tokensUsed)} / ${formatTokens(state.tokenBudget)} tokens`;
72
- return formatElapsed(state.timeUsedSeconds);
73
- }
74
-
75
- function truncateObjective(objective: string, max = 96): string {
76
- const singleLine = objective.replace(/\s+/g, " ").trim();
77
- return singleLine.length > max ? `${singleLine.slice(0, max - 1)}…` : singleLine;
78
- }
79
-
80
- function goalEventStatus(kind: GoalEventKind): string {
81
- const labels: Record<GoalEventKind, string> = {
82
- active: "active",
83
- continuation: "continuing",
84
- paused: "paused",
85
- resumed: "resumed",
86
- cleared: "cleared",
87
- budget_limited: "budget reached",
88
- complete: "achieved",
89
- };
90
- return labels[kind];
91
- }
92
-
93
27
  // The `content` field is what the LLM sees in the conversation history.
94
28
  // Every goal event MUST carry actionable text — never a cryptic marker.
95
29
  // The TUI renderer collapses long bodies down to a compact badge for humans.
@@ -153,20 +87,24 @@ function updateStatusBar(ctx: ExtensionContext) {
153
87
  ctx.ui.setStatus(CUSTOM_TYPE, statusBarEnabled ? statusLine(goal) ?? "" : "");
154
88
  }
155
89
 
156
- const GOAL_TOOL_NAMES = ["get_goal", "update_goal"];
90
+ const ACTIVE_GOAL_TOOL_NAMES = ["get_goal", "update_goal"];
157
91
 
158
- // Expose goal tools to the LLM only while a goal is actively being pursued.
159
- // When no goal exists (or it is paused / complete / budget-limited), keep them
160
- // hidden so unrelated sessions are not tempted to call them every turn.
92
+ // Expose read/update tools to the LLM only while a goal is actively being pursued.
93
+ // Keep create_goal available so the model can start a goal when explicitly asked,
94
+ // but rely on its tool contract to reject inferred goals and existing goals.
161
95
  function syncGoalTools(pi: ExtensionAPI) {
162
- const want = goal?.status === "active";
96
+ const wantActiveTools = goal?.status === "active";
163
97
  const active = new Set(pi.getActiveTools());
164
- for (const name of GOAL_TOOL_NAMES) (want ? active.add(name) : active.delete(name));
98
+ active.add("create_goal");
99
+ for (const name of ACTIVE_GOAL_TOOL_NAMES) (wantActiveTools ? active.add(name) : active.delete(name));
165
100
  pi.setActiveTools(Array.from(active));
166
101
  }
167
102
 
168
103
  function persist(pi: ExtensionAPI, ctx: ExtensionContext, next: GoalState | null) {
169
104
  goal = next;
105
+ if (next?.status !== "active") {
106
+ continuationQueued = false;
107
+ }
170
108
  pi.appendEntry(CUSTOM_TYPE, { goal: next, statusBarEnabled });
171
109
  updateStatusBar(ctx);
172
110
  syncGoalTools(pi);
@@ -205,7 +143,7 @@ Before deciding that the goal is achieved, perform a completion audit against th
205
143
  - Identify any missing, incomplete, weakly verified, or uncovered requirement.
206
144
  - Treat uncertainty as not achieved; do more verification or continue the work.
207
145
 
208
- Do not rely on intent, partial progress, elapsed effort, memory of earlier work, or a plausible final answer as proof of completion. Only mark the goal achieved when the audit shows that the objective has actually been achieved and no required work remains. If any requirement is missing, incomplete, or unverified, keep working instead of marking the goal complete. If the objective is achieved, call update_goal with status \"complete\".
146
+ Do not rely on intent, partial progress, elapsed effort, memory of earlier work, or a plausible final answer as proof of completion. Only mark the goal achieved when the audit shows that the objective has actually been achieved and no required work remains. If any requirement is missing, incomplete, or unverified, keep working instead of marking the goal complete. If the objective is achieved, call update_goal with status \"complete\" so usage accounting is preserved.
209
147
 
210
148
  Do not call update_goal unless the goal is complete. Do not mark a goal complete merely because the budget is nearly exhausted or because you are stopping work.`;
211
149
  }
@@ -280,10 +218,64 @@ export default function piGoal(pi: ExtensionAPI) {
280
218
  },
281
219
  });
282
220
 
221
+ pi.registerTool({
222
+ name: "create_goal",
223
+ label: "Create Goal",
224
+ description: "Create a new active thread goal only when explicitly requested. A goal must be a durable, evidence-checkable work contract: outcome, verification surface, constraints, boundaries, iteration policy, and blocked stop condition. Fails if a goal already exists.",
225
+ promptSnippet: "Create a pi-goal objective only when the user explicitly requests goal mode",
226
+ promptGuidelines: [
227
+ "Use create_goal only when the user explicitly asks to set/start/follow a goal, or system/developer instructions require a goal.",
228
+ "Do not infer goals from ordinary coding tasks or one-off prompts.",
229
+ "Before creating a goal, turn the request into a concrete objective with: outcome, verification surface, constraints, boundaries, iteration policy, and blocked stop condition.",
230
+ "Use this objective shape when possible: <desired end state>, verified by <specific evidence>, while preserving <constraints>. Use <allowed scope/tools> and avoid <forbidden scope>. Between iterations, <how to choose the next action and what to re-check>. If blocked or no defensible path remains, stop with <evidence gathered, attempted paths, blocker, and next input needed>.",
231
+ "Prefer a self-contained objective that survives continuation turns and context compaction.",
232
+ "Do not create vague goals like 'improve this' or 'finish the feature'; ask a clarifying question if missing success criteria or boundaries materially affect the contract.",
233
+ "Set tokenBudget only when the user explicitly requested a token budget.",
234
+ ],
235
+ parameters: {
236
+ type: "object",
237
+ properties: {
238
+ objective: {
239
+ type: "string",
240
+ description: "The concrete objective to pursue as an active thread goal.",
241
+ },
242
+ tokenBudget: {
243
+ type: "number",
244
+ description: "Optional positive token budget for the goal, only when explicitly requested.",
245
+ },
246
+ },
247
+ required: ["objective"],
248
+ additionalProperties: false,
249
+ } as any,
250
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
251
+ if (goal) {
252
+ return {
253
+ content: [{ type: "text", text: "Cannot create a new goal because this thread already has a goal. Use update_goal only when the existing goal is complete, or ask the user to clear/replace it." }],
254
+ isError: true,
255
+ };
256
+ }
257
+ const objective = typeof params.objective === "string" ? params.objective.trim() : "";
258
+ if (!objective) {
259
+ return { content: [{ type: "text", text: "objective is required." }], isError: true };
260
+ }
261
+ const parsedBudget = normalizeTokenBudget(params.tokenBudget);
262
+ if (parsedBudget.error) {
263
+ return { content: [{ type: "text", text: parsedBudget.error }], isError: true };
264
+ }
265
+ const next = createGoalState(objective, parsedBudget.tokenBudget);
266
+ persist(pi, ctx, next);
267
+ emitGoalEvent(pi, "active", next, { triggerTurn: ctx.isIdle() });
268
+ return {
269
+ content: [{ type: "text", text: JSON.stringify({ goal: next, remainingTokens: next.tokenBudget }, null, 2) }],
270
+ details: { goal: next },
271
+ };
272
+ },
273
+ });
274
+
283
275
  pi.registerTool({
284
276
  name: "update_goal",
285
277
  label: "Update Goal",
286
- description: "Mark the current thread goal complete. This tool only accepts status=complete.",
278
+ description: "Mark the current thread goal complete. This tool only accepts status=complete and final turn usage is accounted by the runtime.",
287
279
  promptSnippet: "Mark the current goal complete after a strict completion audit",
288
280
  promptGuidelines: [
289
281
  "Use update_goal only when the current pi-goal objective is fully achieved and verified against concrete evidence.",
@@ -381,17 +373,7 @@ export default function piGoal(pi: ExtensionAPI) {
381
373
  const ok = await ctx.ui.confirm("Replace goal?", `Current: ${goal.objective}\n\nNew: ${parsed.objective}`);
382
374
  if (!ok) return;
383
375
  }
384
- const next: GoalState = {
385
- version: 1,
386
- id: `${now}-${Math.random().toString(16).slice(2)}`,
387
- objective: parsed.objective,
388
- status: "active",
389
- tokenBudget: parsed.tokenBudget,
390
- tokensUsed: 0,
391
- timeUsedSeconds: 0,
392
- createdAt: now,
393
- updatedAt: now,
394
- };
376
+ const next = createGoalState(parsed.objective, parsed.tokenBudget, now);
395
377
  persist(pi, ctx, next);
396
378
  emitGoalEvent(pi, "active", next, { triggerTurn: ctx.isIdle() });
397
379
  },
@@ -403,7 +385,8 @@ export default function piGoal(pi: ExtensionAPI) {
403
385
  statusBarEnabled = restored.statusBarEnabled;
404
386
  continuationQueued = false;
405
387
  activeTurnStartedAt = null;
406
- // Hide goal tools from the LLM unless we have an active goal to pursue.
388
+ activeGoalThisTurnId = null;
389
+ // Keep create_goal available, and hide read/update tools unless there is an active goal to pursue.
407
390
  syncGoalTools(pi);
408
391
  if (goal?.status === "active" && event.reason === "reload") {
409
392
  // Reload pauses an active goal so it does not silently resume.
@@ -431,22 +414,20 @@ export default function piGoal(pi: ExtensionAPI) {
431
414
 
432
415
  pi.on("turn_start", (_event, _ctx) => {
433
416
  activeTurnStartedAt = Date.now();
417
+ activeGoalThisTurnId = goal?.status === "active" ? goal.id : null;
434
418
  });
435
419
 
436
420
  pi.on("turn_end", (event, ctx) => {
437
- if (!goal || goal.status !== "active") return;
421
+ if (!goal || activeGoalThisTurnId !== goal.id) {
422
+ activeTurnStartedAt = null;
423
+ activeGoalThisTurnId = null;
424
+ return;
425
+ }
438
426
  const elapsed = activeTurnStartedAt ? Math.max(0, Math.round((Date.now() - activeTurnStartedAt) / 1000)) : 0;
439
427
  activeTurnStartedAt = null;
428
+ activeGoalThisTurnId = null;
440
429
  const tokenDelta = tokenDeltaFromUsage((event.message as { usage?: UsageSnapshot } | undefined)?.usage);
441
- let next: GoalState = {
442
- ...goal,
443
- tokensUsed: goal.tokensUsed + tokenDelta,
444
- timeUsedSeconds: goal.timeUsedSeconds + elapsed,
445
- updatedAt: Date.now(),
446
- };
447
- if (next.tokenBudget != null && next.tokensUsed >= next.tokenBudget) {
448
- next = { ...next, status: "budget_limited" };
449
- }
430
+ const next = accountGoalTurn(goal, tokenDelta, elapsed);
450
431
  persist(pi, ctx, next);
451
432
  if (next.status === "budget_limited") {
452
433
  emitGoalEvent(pi, "budget_limited", next, { triggerTurn: true, deliverAs: "followUp" });
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Persistent autonomous goals for [pi](https://github.com/badlogic/pi-mono).
6
6
 
7
- `pi-goal` adds a `/goal` command and goal tools so Pi can keep working toward a long-running objective until the goal is complete, paused, cleared, or token-budget-limited.
7
+ `pi-goal` adds a `/goal` command and goal tools so Pi can keep working toward a long-running, thread-scoped objective until the goal is complete, paused, cleared, or token-budget-limited.
8
8
 
9
9
  ## Install
10
10
 
@@ -23,6 +23,7 @@ pi install git:github.com/Michaelliv/pi-goal
23
23
  ```text
24
24
  /goal improve benchmark coverage until the suite has strong evidence
25
25
  /goal --tokens 50k finish the migration and verify tests
26
+ /goal
26
27
  /goal status
27
28
  /goal pause
28
29
  /goal resume
@@ -36,12 +37,14 @@ The same Pi agent keeps running normal turns in the same session context until i
36
37
 
37
38
  ## What it adds
38
39
 
40
+ - `pi-goal-writer` skill: draft and review strong `/goal` objectives with evidence-based success criteria
39
41
  - `/goal [--tokens 50k] <objective>`: set or replace a goal
40
- - `/goal status`: show the current goal
42
+ - `/goal` or `/goal status`: show the current goal
41
43
  - `/goal pause`: stop autonomous continuation without deleting the goal
42
44
  - `/goal resume`: reactivate a paused goal
43
45
  - `/goal clear`: remove the goal
44
46
  - `/goal statusbar on|off`: show or hide the footer status line
47
+ - `create_goal` tool: model can create a goal only when explicitly requested and only if no goal exists
45
48
  - `get_goal` tool: read current goal state
46
49
  - `update_goal` tool: model can only mark the goal `complete`
47
50
  - `get_goal` and `update_goal` are only exposed to the model while a goal is `active`; paused, cleared, complete, and budget-limited goals hide them so unrelated sessions are not tempted to call them
@@ -62,7 +65,7 @@ The same Pi agent keeps running normal turns in the same session context until i
62
65
 
63
66
  ## Completion behavior
64
67
 
65
- The model is instructed to audit completion against real evidence before calling `update_goal`. The `update_goal` tool deliberately accepts only `status: "complete"`; pausing, resuming, clearing, and budget limiting are controlled by the user or extension runtime.
68
+ The model is instructed to audit completion against real evidence before calling `update_goal`. The `update_goal` tool deliberately accepts only `status: "complete"`; pausing, resuming, clearing, and budget limiting are controlled by the user or extension runtime. The final turn is still accounted even when the model completes the goal mid-turn.
66
69
 
67
70
  ## State
68
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Persistent autonomous goals for pi — /goal loops until complete, paused, or budget-limited",
5
5
  "type": "commonjs",
6
6
  "keywords": [
@@ -12,12 +12,16 @@
12
12
  ],
13
13
  "files": [
14
14
  ".pi/",
15
+ "skills/",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
18
19
  "pi": {
19
20
  "extensions": [
20
21
  ".pi/extensions/pi-goal"
22
+ ],
23
+ "skills": [
24
+ "skills"
21
25
  ]
22
26
  },
23
27
  "peerDependencies": {
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: pi-goal-writer
3
+ description: Drafts and reviews strong /goal objectives for Pi pi-goal and compatible goal-mode agents. Use when the user asks to write, improve, audit, or meta-prompt a long-running agent goal with clear success criteria, verification, constraints, iteration policy, and blocked stop conditions.
4
+ ---
5
+
6
+ # Pi Goal Writer
7
+
8
+ ## Purpose
9
+
10
+ Write `/goal` prompts that are fit for persistent autonomous work. A goal is not a bigger ordinary prompt; it is a completion contract. The agent will keep using it to decide what to do next and whether it can honestly stop, so the goal must define the desired end state, the evidence that proves it, the constraints that must remain true, and when to stop as blocked instead of drifting.
11
+
12
+ Use this skill for Pi `pi-goal` first. The same goal-writing principles also apply to Codex Goal mode and compatible `/goal` workflows.
13
+
14
+ ## Core rule
15
+
16
+ Never produce a vague goal such as “make this better,” “finish the feature,” or “improve the codebase.” Turn the user’s rough intent into a goal with auditable completion criteria.
17
+
18
+ A strong goal includes six parts:
19
+
20
+ 1. **Outcome** — what must be true when the work is done.
21
+ 2. **Verification surface** — tests, commands, benchmark output, report, artifact, diff audit, PR state, screenshots, logs, or other concrete evidence.
22
+ 3. **Constraints** — what must not regress or be changed.
23
+ 4. **Boundaries** — files, directories, tools, systems, data sources, or permissions the agent may or may not use.
24
+ 5. **Iteration policy** — how the agent should choose the next action after each attempt.
25
+ 6. **Blocked stop condition** — when the agent should stop honestly, with evidence and the next needed input, instead of continuing blindly.
26
+
27
+ ## Workflow
28
+
29
+ 1. Default to Pi `pi-goal`. Write a Pi-compatible `/goal` command unless the user explicitly asks for another harness. The goal body can usually be reused in Codex Goal mode; Pi also supports optional token budgets such as `/goal --tokens 50k ...`.
30
+ 2. Gather context before drafting when the task depends on a repository, issue, test suite, benchmark, PR, design, or external documentation. Read the relevant files or sources instead of inventing the verification surface.
31
+ 3. Ask at most three clarifying questions only when missing information changes the goal contract. Prefer making safe assumptions explicit when the user is trying to move quickly.
32
+ 4. Draft the goal as a single pasteable command, then include a short rationale or checklist showing how the six parts are covered.
33
+ 5. For high-stakes or ambiguous work, provide two options: a narrower goal that is safer to execute and a broader goal that delegates more discovery to the agent. Recommend one.
34
+
35
+ ## Goal template
36
+
37
+ Use this shape unless the user asks for a different format:
38
+
39
+ ```text
40
+ /goal <desired end state>, verified by <specific evidence>, while preserving <constraints>. Use <allowed inputs/tools/scope> and avoid <forbidden scope>. Between iterations, <how to choose the next best action and what to re-check>. If blocked or no defensible path remains, stop with <evidence gathered, attempted paths, blocker, and next input needed>.
41
+ ```
42
+
43
+ For Pi token budgets:
44
+
45
+ ```text
46
+ /goal --tokens 50k <same goal contract>
47
+ ```
48
+
49
+ ## Writing standards
50
+
51
+ Make the goal self-contained. It should survive context compaction and continuation turns. Include exact command names when known, but do not invent commands. Say “run the relevant project checks identified in AGENTS.md/package scripts” only when exact commands are unknown and the agent can inspect them.
52
+
53
+ Make completion evidence-based. The goal should require the agent to inspect real artifacts before declaring success: files changed, tests passed, benchmark numbers, rendered screenshots, logs, PR checks, or a written audit. Do not let “tests pass” be the only evidence unless the tests actually cover every requirement.
54
+
55
+ Bound the scope. Name excluded directories or behaviors when important, such as “do not rewrite CLI user-facing output,” “do not change public API behavior,” or “do not touch generated files except via the generator.”
56
+
57
+ Preserve honesty under uncertainty. If evidence may be unavailable, require a final report that separates confirmed findings, approximate/proxy evidence, blocked claims, and remaining uncertainty.
58
+
59
+ Prefer concrete stop language: “If blocked, stop with the exact blocker and what would unlock progress.” Avoid weak endings like “do your best.”
60
+
61
+ ## Review checklist
62
+
63
+ Before returning a goal, verify it answers:
64
+
65
+ - Can the agent tell when it is done?
66
+ - Can the user independently audit that completion claim?
67
+ - Are regressions and forbidden approaches named?
68
+ - Does the goal allow iteration without inviting unlimited drift?
69
+ - Does it define what to do when tests, credentials, network, data, or product decisions block progress?
70
+ - Is it pasteable as one `/goal` command?
71
+
72
+ ## Examples
73
+
74
+ Weak:
75
+
76
+ ```text
77
+ /goal improve logging
78
+ ```
79
+
80
+ Strong:
81
+
82
+ ```text
83
+ /goal Implement structured runtime logging, verified by targeted logger tests, the full project check/type/test suite, and final audits showing no production console.* calls outside approved CLI/UI/logger-sink exceptions. Preserve existing operator-visible console behavior, avoid logging secrets or credentials, and keep the logger generic rather than error-only. Between iterations, inspect the diff and audit remaining catch paths before deciding the next change. If blocked, stop with the unverified requirement, evidence gathered, and the next input needed.
84
+ ```
85
+
86
+ Weak:
87
+
88
+ ```text
89
+ /goal fix flaky checkout test
90
+ ```
91
+
92
+ Strong:
93
+
94
+ ```text
95
+ /goal Diagnose and either fix or conclusively characterize the flaky checkout test, verified by a reliable local reproduction or an evidence-backed failure analysis plus the relevant test command passing when a fix is made. Preserve public checkout behavior and avoid broad timing hacks unless the evidence shows timing is the root cause. Between iterations, record the hypothesis tested, command output, and next most likely cause. If the flake cannot be reproduced or no safe fix remains, stop with attempted reproductions, logs, suspected causes, and the missing evidence needed.
96
+ ```
97
+
98
+ Weak:
99
+
100
+ ```text
101
+ /goal reproduce this paper
102
+ ```
103
+
104
+ Strong:
105
+
106
+ ```text
107
+ /goal Produce the strongest evidence-backed reproduction of the paper using available local resources, verified by a final claim-by-claim report and any generated artifacts or runnable checks. Attempt the headline results where feasible, label approximate reconstructions separately from exact reproductions, and do not overclaim missing seeds, checkpoints, datasets, or implementation details. Between iterations, map claims to available evidence and prioritize the highest-value verifiable claim. If exact reproduction is blocked, stop with confirmed claims, proxy evidence, blocked claims, and the specific missing materials.
108
+ ```