pi-goal-x 0.31.4 → 0.31.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.
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,318 by downloads · Sep 17, 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" } })
@@ -163,3 +168,7 @@ Existing producers that directly send `triggerTurn`/`followUp` messages still ru
163
168
  ## License
164
169
 
165
170
  MIT
171
+
172
+ ### Prompt caching
173
+
174
+ Goal state is refreshed at the request tail while the system prompt and conversation prefix stay stable. Pi retains control of provider cache settings. See [prompt caching](docs/prompt-caching.md) for explicit-cache handling, validation, and cache invalidation boundaries.
@@ -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,318 by downloads · Sep 17, 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,318 by downloads · Sep 17, 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,318 by downloads · Sep 17, 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,318 by downloads · Sep 17, 2026</text>
17
17
  </a>
18
18
  </svg>
@@ -19,7 +19,7 @@ handlers from their dedicated modules:
19
19
  | `goal-task-tools.ts` | `set_goal_tasks` / `update_goal_task` executors plus flat parent-linked conversion, id-stable merge, `countTasks` |
20
20
  | `goal-task-confirmation.ts` | Task-only result boundary (`{decision}`, no auditor toggle) with neutral Confirm task list / Keep current tasks labels |
21
21
  | `goal-commands.ts` | The curated fourteen-command palette and its handlers |
22
- | `goal-events.ts` | The 14 lifecycle event handlers (`context`, `turn_start`, `tool_call`, `tool_execution_end`, `turn_end`, `message_end`, `session_start`, `session_before_compact`, `session_compact`, `session_tree`, `before_agent_start`, `agent_end`, `agent_settled`, `session_shutdown`) |
22
+ | `goal-events.ts` | Lifecycle and provider event handlers (`context`, `before_provider_request`, `turn_start`, `tool_call`, `tool_execution_end`, `turn_end`, `message_end`, `session_start`, `session_before_compact`, `session_compact`, `session_tree`, `before_agent_start`, `agent_end`, `agent_settled`, `session_shutdown`) |
23
23
  | `goal-widget.ts` | Terminal input keybindings (Esc pause / abort-audit, Ctrl+Shift+T overlay) and the hidden debug helpers |
24
24
  | `goal-format.ts` | Pure formatting/message-introspection helpers and renderers |
25
25
  | `goal-service.ts` | `GoalService` — the sole mutation boundary: ordered reconcile → id/focus-revision validation → clone-mutate → write/archive → ledger → memory commit → returned effects |
@@ -341,3 +341,5 @@ operable; `/goal-clear` confirms; task confirmation uses neutral labels;
341
341
  completion commits are failure-checked; and cross-process mutations are
342
342
  serialized with persisted revisions plus per-goal filesystem locks that
343
343
  return typed conflicts to stale writers instead of overwriting blindly.
344
+
345
+ Prompt layout and explicit provider cache handling are documented in [prompt caching](prompt-caching.md). Live goal state is constructed in the context hook; before_agent_start performs lifecycle preflight without modifying the system prompt.
@@ -0,0 +1,21 @@
1
+ # Prompt caching
2
+
3
+ pi-goal-x preserves the provider's reusable prompt prefix during normal user turns, autonomous checkpoints, and tool loops. No extra extension setting is required. Pi's provider configuration still determines whether caching is enabled, its retention period, and model support.
4
+
5
+ The request layout is:
6
+
7
+ 1. Host system prompt and tool schemas.
8
+ 2. Conversation history, with each goal checkpoint normalized to a tiny, stable trigger in its original position.
9
+ 3. One request-only goal-state message containing the current objective, tasks, lifecycle rules, usage, budget, scheduling state, and applicable recovery/auditor reminders.
10
+
11
+ Changing goal state never rewrites the system prompt or historical messages. The full goal-state message is not persisted into the session. Old checkpoints remain until Pi compacts history; retaining small markers avoids shifting the prefix on every continuation. Display-only auditor messages are consistently excluded, and tool calls stay adjacent to their results.
12
+
13
+ For Anthropic-style explicit caching, the extension moves Pi's existing cache_control breakpoint from the transient state message onto the preceding cacheable history block. The Bedrock equivalent moves the cachePoint. Both preserve the configured TTL and do not add breakpoints. Payloads without an existing explicit marker—including caching disabled and implicit-cache providers—are unchanged. The extension does not set cache keys, enable paid extended retention, or replace provider defaults.
14
+
15
+ Execution tools already use an idempotent profile: goal progress, focus, and lifecycle changes do not change schemas. Explicit switches into/out of drafting and changes to disableTasks can change tools and legitimately rebuild the cache. Drafting messages are appended to conversation history. Isolated auditor and Oracle sessions already use fixed instructions/tool profiles and place goal-specific evidence in their user messages; their tool loops retain history. They have separate sessions and use Pi's cache defaults. Opting these sessions into project resources also opts into any behavior from those resources.
16
+
17
+ Cache reuse can still be interrupted by Pi compaction, branch/session changes, model/provider switches, other extensions changing earlier content, changed tool schemas, provider expiry, minimum-length or breakpoint-lookback limits, or routing. The extension cannot guarantee a hit rate or a particular cost reduction. The reported issue used Pi 0.85.1; this change is validated against the repository's supported installed SDK, without expanding its peer dependency range.
18
+
19
+ Validation includes consecutive request-prefix regressions; actual Anthropic, Anthropic-compatible Chat Completions, OpenAI Chat Completions and Responses serialization with short/long/disabled retention; Bedrock payload-shape checks; lifecycle/tool-pair safety tests; and full SDK parent/auditor/Oracle payload captures. These tests stop before HTTP dispatch. They verify request construction, not live provider hit rates.
20
+
21
+ Provider references: [Anthropic prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) describes exact-prefix matching and explicit breakpoints; [DeepSeek context caching](https://api-docs.deepseek.com/guides/kv_cache/) describes its automatic prefix cache and usage counters.
@@ -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." })),
@@ -1,4 +1,4 @@
1
- import { schedulerSummary } from "./goal-scheduler-state.ts";
1
+ import { cacheGoalHistory } from "./goal-prompt-cache.ts";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  GOAL_EVENT_ENTRY,
@@ -34,45 +34,21 @@ import type { GoalCore } from "./goal-state.ts";
34
34
  import { filterGoalSessionContext } from "./goal-session-safety.ts";
35
35
  import type { GoalMutationOutcome } from "./goal-service.ts";
36
36
 
37
- /**
38
- * Issue #30: provider-context checkpoint compaction (pure helper).
39
- *
40
- * Every historical checkpoint message is redundant: its authoritative state is
41
- * reconstructed from goal storage and injected once per turn by
42
- * before_agent_start. Normal provider requests therefore retain at most ONE
43
- * checkpoint marker — the latest — rewritten to the tiny bounded v2 trigger
44
- * content. Keeping one user-role turn-start marker avoids provider edge cases
45
- * where removing it would leave the request ending on an assistant or tool
46
- * result. Audit events, user messages, assistant messages, and tool results
47
- * pass through untouched.
48
- */
37
+ /** Normalize checkpoints independently: deleting old markers shifts the cached prefix. */
49
38
  export function compactGoalCheckpointContext(
50
- messages: readonly unknown[],
51
- currentGoal: GoalRecord | null,
39
+ messages: readonly unknown[],
40
+ _currentGoal: GoalRecord | null,
52
41
  ): unknown[] | null {
53
- let lastCheckpointIndex = -1;
54
- let checkpointGoalId: string | null = null;
55
- const checkpoints: number[] = [];
56
- // Parse each message once; retain ordinary messages and rewrite only the last marker.
42
+ let output: unknown[] | null = null;
57
43
  for (let i = 0; i < messages.length; i++) {
58
- const id = goalEventMessageId(messages[i] as {customType?: string; details?: unknown; content?: unknown});
59
- if (id !== null) { checkpoints.push(i); lastCheckpointIndex = i; checkpointGoalId = id; }
44
+ const message = messages[i] as { customType?: string; details?: unknown; content?: unknown };
45
+ const id = goalEventMessageId(message);
46
+ if (id === null) { output?.push(message); continue; }
47
+ output ??= messages.slice(0, i);
48
+ output.push({ ...message, content: checkpointTriggerPrompt(id), display: false,
49
+ details: { version: 2, kind: "checkpoint", goalId: id } });
60
50
  }
61
- if (checkpointGoalId === null) return null;
62
- const output: unknown[] = [];
63
- let start = 0;
64
- for (const index of checkpoints) {
65
- for (let i = start; i < index; i++) output.push(messages[i]);
66
- start = index + 1;
67
- }
68
- const message = messages[lastCheckpointIndex] as Record<string, unknown>;
69
- output.push({...message, content: checkpointTriggerPrompt(checkpointGoalId), display: false, details: {
70
- version: 2,
71
- kind: currentGoal?.id === checkpointGoalId && currentGoal?.status === "active" ? "checkpoint" : "stale",
72
- goalId: checkpointGoalId, currentGoalId: currentGoal?.id ?? null, currentStatus: currentGoal?.status ?? null,
73
- }});
74
- for (let i = start; i < messages.length; i++) output.push(messages[i]);
75
- return output;
51
+ return output;
76
52
  }
77
53
 
78
54
  /**
@@ -84,22 +60,18 @@ export function compactGoalCheckpointContext(
84
60
  */
85
61
  export function registerGoalEvents(core: GoalCore): void {
86
62
  const { pi } = core;
63
+ let liveContent: string | undefined;
64
+ pi.on("before_provider_request", event => cacheGoalHistory(event.payload, liveContent));
87
65
  let continuationAfterSettleFor: string | null = null;
88
66
  let networkErrorRecoveryAfterSettleFor: string | null = null;
89
67
 
90
68
  pi.on("context", async (event, ctx) => {
91
69
  const filtered = filterGoalSessionContext(event.messages);
92
70
  const messages = compactGoalCheckpointContext(filtered ?? event.messages, core.state.goal) ?? filtered;
93
- if (core.scheduler.needsLiveContext() && core.state.goal) {
94
- const goal = core.state.goal;
95
- const settings = loadGoalSettings(ctx.cwd);
96
- // Custom-message runs bypass before_agent_start. Refresh mutable scheduling
97
- // state, but do not repeat policy/objective already in the inherited prompt.
98
- const content = ctx.getSystemPrompt?.().includes(`[PI GOAL ACTIVE goalId=${goal.id}]`)
99
- ? `[CURRENT EXECUTION STATE goalId=${goal.id}]\nStatus: ${goal.status}; autoContinue: ${goal.autoContinue}.\n${schedulerSummary(goal.scheduler, settings.maxAutonomousRuns)}`
100
- : goalPrompt(goal, settings);
101
- const live = { role: "custom" as const, customType: "pi-goal-live-context", content, display: false, timestamp: Date.now() };
102
- return { messages: [live, ...(messages ?? event.messages)] as typeof event.messages };
71
+ const content = liveContent = currentGoalContext(ctx);
72
+ if (content) {
73
+ const live = { role: "custom" as const, customType: "pi-goal-live-context", content, display: false, timestamp: 0 };
74
+ return { messages: [...(messages ?? event.messages), live] as typeof event.messages };
103
75
  }
104
76
  return messages === null ? undefined : { messages: messages as typeof event.messages };
105
77
  });
@@ -343,16 +315,9 @@ export function registerGoalEvents(core: GoalCore): void {
343
315
  });
344
316
 
345
317
  pi.on("before_agent_start", async (event, ctx) => {
346
- core.scheduler.prepare();
347
318
  core.advanceTurnSeq();
348
319
  if (!hasActiveDraft(core)) core.installGoalToolProfile(!loadGoalSettings(ctx.cwd).disableTasks);
349
- const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
350
320
  const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
351
- // Several prompt enrichments may need the same ledger snapshot. Keep one
352
- // local read for this hook instead of repeatedly traversing the cached
353
- // ledger when rejection and post-compaction steering overlap.
354
- let promptLedger: ReturnType<typeof readGoalLedger> | undefined;
355
- const getPromptLedger = () => promptLedger ??= { events: core.state.goal ? goalRuntimeEvents(ctx, core.state.goal.id) : [], malformed: 0 };
356
321
 
357
322
  // If this turn was triggered by a hidden goal checkpoint that no longer
358
323
  // matches the active goal, abort the whole turn instead of letting the
@@ -371,9 +336,7 @@ export function registerGoalEvents(core: GoalCore): void {
371
336
  ctx.abort?.();
372
337
  } catch {}
373
338
  core.updateUI(ctx);
374
- return {
375
- systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, core.state.goal)}`,
376
- };
339
+ return;
377
340
  }
378
341
  core.runtime.setCheckpoint(null);
379
342
  } else {
@@ -385,22 +348,29 @@ export function registerGoalEvents(core: GoalCore): void {
385
348
  networkErrorRecoveryAfterSettleFor = null;
386
349
  }
387
350
 
351
+ core.reconcileFocusedGoalFromDisk(ctx);
352
+ core.runningGoalId = core.state.goal?.status === "active" ? core.state.goal.id : null;
353
+ });
354
+
355
+ /** Request-only state: no live counters, focus, or reminders enter the system prefix. */
356
+ function currentGoalContext(ctx: ExtensionContext): string | undefined {
357
+ const checkpoint = core.runtime.getCheckpointGoalId();
358
+ if (checkpoint !== null && !core.isActionableContinuationGoal(checkpoint)) {
359
+ return staleContinuationPrompt(checkpoint, core.state.goal);
360
+ }
361
+ // Several prompt enrichments may need the same ledger snapshot. Keep one
362
+ // local read for this hook instead of repeatedly traversing the cached
363
+ // ledger when rejection and post-compaction steering overlap.
364
+ let promptLedger: ReturnType<typeof readGoalLedger> | undefined;
365
+ const getPromptLedger = () => promptLedger ??= { events: core.state.goal ? goalRuntimeEvents(ctx, core.state.goal.id) : [], malformed: 0 };
366
+
388
367
  if (!core.state.goal) {
389
- core.runningGoalId = null;
390
368
  const openCount = otherOpenGoalCount(core.goalsById, null);
391
369
  if (openCount > 0) {
392
- return { systemPrompt: `${currentSystemPrompt()}\n\n${unfocusedOpenGoalsPrompt(openCount)}` };
370
+ return unfocusedOpenGoalsPrompt(openCount);
393
371
  }
394
372
  return;
395
373
  }
396
- core.reconcileFocusedGoalFromDisk(ctx);
397
- if (!core.state.goal) {
398
- core.runningGoalId = null;
399
- const openCount = otherOpenGoalCount(core.goalsById, null);
400
- if (openCount > 0) return { systemPrompt: `${currentSystemPrompt()}\n\n${unfocusedOpenGoalsPrompt(openCount)}` };
401
- return;
402
- }
403
- core.runningGoalId = core.state.goal.status === "active" ? core.state.goal.id : null;
404
374
  if (core.state.goal.status === "complete") return;
405
375
  if (core.state.goal.status === "paused") {
406
376
  const current = core.state.goal;
@@ -421,9 +391,7 @@ export function registerGoalEvents(core: GoalCore): void {
421
391
  } catch {
422
392
  // Ledger read failure should not break the prompt
423
393
  }
424
- return {
425
- systemPrompt: `${currentSystemPrompt()}\n\n[PI GOAL PAUSED goalId=${current.id}]\n${untrustedObjectiveBlock(current)}${pauseExtras.join("\n")}${auditorExtra}\n\nThe goal is paused. Do not autonomously continue substantive work unless the user resumes it with /goal-resume. If the user explicitly asks to finish the paused goal and the objective is already satisfied based on available evidence, you may call update_goal({status: "complete"}). To abandon a goal, the user runs /goal-clear. Do not report the goal blocked in response to a pause.`,
426
- };
394
+ return `[PI GOAL PAUSED goalId=${current.id}]\n${untrustedObjectiveBlock(current)}${pauseExtras.join("\n")}${auditorExtra}\n\nThe goal is paused. Do not autonomously continue substantive work unless the user resumes it with /goal-resume. If the user explicitly asks to finish the paused goal and the objective is already satisfied based on available evidence, you may call update_goal({status: "complete"}). To abandon a goal, the user runs /goal-clear. Do not report the goal blocked in response to a pause.`;
427
395
  }
428
396
  // Token-budget-limited goals get one-time wrap-up steering: summarize,
429
397
  // do not start new substantive work, never claim completion unless real.
@@ -440,13 +408,11 @@ export function registerGoalEvents(core: GoalCore): void {
440
408
  const reminder = core.runtime.consumePostBudgetReminder()
441
409
  ? `\n\n[TOKEN BUDGET REACHED goalId=${limitedGoal.id}]\nThe goal's token budget has been reached${budgetText ? ` (${budgetText}${balanceText})` : ""}. Wrap up the current work in one final response: summarize what was accomplished and what remains, do not start new substantive work, and do not claim the goal is complete unless it actually is. To continue, the user must raise or remove the budget and resume the goal.`
442
410
  : "";
443
- return {
444
- systemPrompt: `${currentSystemPrompt()}\n\n[PI GOAL BUDGET LIMITED goalId=${limitedGoal.id}]\n${untrustedObjectiveBlock(limitedGoal)}${budgetText ? `\n${budgetText}` : ""}${reminder}`,
445
- };
411
+ return `[PI GOAL BUDGET LIMITED goalId=${limitedGoal.id}]\n${untrustedObjectiveBlock(limitedGoal)}${budgetText ? `\n${budgetText}` : ""}${reminder}`;
446
412
  }
447
413
  if (core.state.goal.status === "blocked") {
448
414
  const blocked = core.state.goal;
449
- return {systemPrompt: `${currentSystemPrompt()}\n\n[PI GOAL BLOCKED goalId=${blocked.id}]\n${untrustedObjectiveBlock(blocked)}\nBlocker: ${blocked.pauseReason ?? "unspecified"}\nThe goal is blocked; the user must run /goal-resume before goal work continues.`};
415
+ return `[PI GOAL BLOCKED goalId=${blocked.id}]\n${untrustedObjectiveBlock(blocked)}\nBlocker: ${blocked.pauseReason ?? "unspecified"}\nThe goal is blocked; the user must run /goal-resume before goal work continues.`;
450
416
  }
451
417
  const activeGoal = core.state.goal;
452
418
  const settings = loadGoalSettings(ctx.cwd);
@@ -466,7 +432,7 @@ export function registerGoalEvents(core: GoalCore): void {
466
432
  }
467
433
  if (core.runtime.isPostCompactReminderPending() && shouldInjectPostCompactReminder({ pending: true, goal: activeGoal })) {
468
434
  core.runtime.clearPostCompactReminder();
469
- // PR E §62: post-compaction DELTA — the active system goal block already
435
+ // PR E §62: post-compaction DELTA — the current goal block already
470
436
  // carries objective/policy/task gate/contract; inject only what
471
437
  // compaction may have lost. Falls back to a generic note on ledger
472
438
  // read failure.
@@ -479,8 +445,8 @@ export function registerGoalEvents(core: GoalCore): void {
479
445
  prompt = `${prompt}\n\n[POST-COMPACTION RESYNC goalId=${core.state.goal.id}]\nThe conversation was just compacted. Re-read the objective and continue from the actual artifacts/state; do not rely on memory of the prior chat.`;
480
446
  }
481
447
  }
482
- return { systemPrompt: `${currentSystemPrompt()}\n\n${prompt}` };
483
- });
448
+ return prompt;
449
+ }
484
450
 
485
451
  pi.on("agent_end", async (event, ctx) => {
486
452
  const endedGoalId = core.runningGoalId;
@@ -0,0 +1,49 @@
1
+ import { asRecord } from "./goal-record.ts";
2
+
3
+ /**
4
+ * Pi marks the final user block for explicit caching. Our request-only state
5
+ * never enters history, so that block cannot be reused on the next request.
6
+ * Move that existing breakpoint to the preceding cacheable history block.
7
+ * Implicit caches and cacheRetention=none have no marker and are untouched.
8
+ * Reuse the provider's TTL and marker count rather than enabling caching here.
9
+ */
10
+ export function cacheGoalHistory(payload: unknown, liveContent: string | undefined): unknown {
11
+ const root = asRecord(payload);
12
+ if (!liveContent || !Array.isArray(root?.messages)) return undefined;
13
+ const messages = root.messages;
14
+ const last = asRecord(messages.at(-1));
15
+ if (last?.role !== "user" || !Array.isArray(last.content)) return undefined;
16
+ // Bedrock represents its breakpoint as a separate content block.
17
+ const bedrockPoint = asRecord(last.content.at(-1));
18
+ if (asRecord(bedrockPoint?.cachePoint) && asRecord(last.content.at(-2))?.text === liveContent) {
19
+ for (let i = messages.length - 2; i >= 0; i--) {
20
+ const previous = asRecord(messages[i]);
21
+ if (!Array.isArray(previous?.content) || previous.content.length === 0) continue;
22
+ if (!previous.content.some(block => asRecord(block)?.cachePoint)) previous.content.push(bedrockPoint);
23
+ last.content.pop();
24
+ return payload;
25
+ }
26
+ return undefined;
27
+ }
28
+ const tail = asRecord(last.content.at(-1));
29
+ if (tail?.type !== "text" || tail.text !== liveContent || !asRecord(tail.cache_control)) return undefined;
30
+ for (let i = messages.length - 1; i >= 0; i--) {
31
+ const message = asRecord(messages[i]);
32
+ if (!message || !["user", "assistant", "tool"].includes(String(message.role))) continue;
33
+ if (typeof message.content === "string" && message.content.length > 0) {
34
+ message.content = [{ type: "text", text: message.content, cache_control: tail.cache_control }];
35
+ delete tail.cache_control;
36
+ return payload;
37
+ }
38
+ if (!Array.isArray(message.content)) continue;
39
+ for (let j = message.content.length - 1; j >= 0; j--) {
40
+ const block = asRecord(message.content[j]);
41
+ // Thinking blocks cannot carry cache_control. Never split/reorder tool pairs.
42
+ if (!block || block === tail || !["text", "image", "tool_use", "tool_result"].includes(String(block.type))) continue;
43
+ block.cache_control ??= tail.cache_control;
44
+ delete tail.cache_control;
45
+ return payload;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
@@ -11,8 +11,6 @@ export class GoalScheduler {
11
11
  private timer: ReturnType<typeof setTimeout> | undefined;
12
12
  private ctx: ExtensionContext | undefined;
13
13
  private inRun = false;
14
- private prepared = false;
15
- private liveContext = false;
16
14
  private armedGeneration: string | undefined;
17
15
  private declared = false;
18
16
  private runGoalId: string | undefined;
@@ -23,6 +21,13 @@ export class GoalScheduler {
23
21
 
24
22
  private owner(ctx: ExtensionContext): string { return ctx.sessionManager.getSessionId() || "unknown-session"; }
25
23
  private limit(ctx: ExtensionContext): number | undefined { return loadGoalSettings(ctx.cwd).maxAutonomousRuns; }
24
+ private strict(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
25
+ return loadGoalSettings(ctx.cwd).strictExecutionContract === true || !!s.wait;
26
+ }
27
+ private implicitReady(s: GoalSchedulerState): void {
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" };
30
+ }
26
31
  private available(ctx: ExtensionContext, s: GoalSchedulerState): boolean {
27
32
  const limit = this.limit(ctx);
28
33
  return limit === undefined || s.used < limit;
@@ -148,6 +153,7 @@ export class GoalScheduler {
148
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).");
149
154
  s.phase = "ready"; s.decision = { kind: "ready", nextAction: input.next_action.trim(), purpose: "ready" }; s.wait = undefined;
150
155
  } else if (input.kind === "wait") {
156
+ 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
157
  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
158
  const deadline = Date.parse(input.deadline);
153
159
  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.");
@@ -175,14 +181,10 @@ export class GoalScheduler {
175
181
  } catch (error) { return { content: [{ type: "text", text: `Scheduling decision NOT saved: ${error instanceof Error ? error.message : String(error)}` }], details: { error: true }, terminate: false }; }
176
182
  }
177
183
 
178
- prepare(): void { this.prepared = true; }
179
- needsLiveContext(): boolean { return this.liveContext; }
180
-
181
184
  /** One logical run can contain multiple agent_start events during native retry/compaction. */
182
185
  begin(ctx: ExtensionContext): void {
183
186
  this.ctx = ctx;
184
187
  if (this.inRun) { this.core.runningGoalId = this.core.state.goal?.id ?? null; return; }
185
- this.liveContext = !this.prepared; this.prepared = false;
186
188
  this.inRun = true; this.declared = false; this.denied = false;
187
189
  this.runGoalId = this.core.state.goal?.id;
188
190
  this.cancelTimer(); this.core.runtime.clearContinuationTimer();
@@ -230,11 +232,16 @@ export class GoalScheduler {
230
232
  settled(ctx: ExtensionContext, successful = true): void {
231
233
  this.inRun = false;
232
234
  if (this.denied || (this.runGoalId && this.core.state.goal?.id !== this.runGoalId)) return;
235
+ invalidateGoalSettingsCache();
233
236
  this.safe(ctx, () => {
234
237
  const goal = this.core.state.goal;
235
238
  if (!goal || goal.status !== "active" || !goal.autoContinue || !successful) return;
236
239
  if (this.declared && goal.scheduler?.decision) { this.schedule(ctx); return; }
237
240
  const s = this.state(ctx, goal);
241
+ if (!this.strict(ctx, s)) {
242
+ this.update(ctx, state => this.implicitReady(state));
243
+ this.schedule(ctx); return;
244
+ }
238
245
  if (s.repairUsed || !this.available(ctx, s)) {
239
246
  this.pause(ctx, !this.available(ctx, s) ? this.allowanceReason(ctx) : "No execution disposition after the contract-repair prompt. Use /goal-resume to continue.");
240
247
  return;
@@ -248,7 +255,7 @@ export class GoalScheduler {
248
255
  }
249
256
  recover(ctx: ExtensionContext): void {
250
257
  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" }; });
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" }; });
252
259
  this.schedule(ctx);
253
260
  });
254
261
  }
@@ -264,11 +271,15 @@ export class GoalScheduler {
264
271
  }
265
272
  schedule(ctx: ExtensionContext): void {
266
273
  if (this.inRun) return;
274
+ invalidateGoalSettingsCache();
267
275
  this.safe(ctx, () => {
268
276
  this.core.reconcileFocusedGoalFromDisk(ctx);
269
277
  const g = this.core.state.goal;
270
278
  if (!g || g.status !== "active" || !g.autoContinue || !g.scheduler) return;
271
- const s = this.state(ctx, g);
279
+ let s = this.state(ctx, g);
280
+ if (s.phase === "ready" && s.decision?.kind === "ready" && s.decision.purpose === "repair" && !this.strict(ctx, s)) {
281
+ s = this.update(ctx, state => this.implicitReady(state)).scheduler!;
282
+ }
272
283
  if (!["ready", "waiting"].includes(s.phase)) return;
273
284
  if (budgetReached(g)) { this.pause(ctx, "Goal token budget exhausted."); return; }
274
285
  if (!this.available(ctx, s)) { this.pause(ctx, this.allowanceReason(ctx)); return; }
@@ -299,6 +310,7 @@ export class GoalScheduler {
299
310
  if (s.generation !== generation) throw new Error("Scheduling generation changed.");
300
311
  if (g.status !== "active" || !g.autoContinue || budgetReached(g) || !this.available(ctx, s)) throw new Error(this.allowanceReason(ctx));
301
312
  if (s.wait && Date.now() >= s.wait.deadline) throw new Error("Wait deadline reached.");
313
+ if (s.phase === "ready" && s.decision?.kind === "ready" && s.decision.purpose === "repair" && !this.strict(ctx, s)) this.implicitReady(s);
302
314
  let kind = s.phase === "ready" && s.decision?.kind === "ready" ? s.decision.purpose : undefined;
303
315
  if (s.phase === "waiting" && s.wait) {
304
316
  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) },
@@ -11,8 +11,8 @@ export const MAX_PROMPT_FRAGMENT_CHARS = 10_000;
11
11
 
12
12
  /**
13
13
  * Issue #30: a persisted continuation checkpoint is a tiny trigger record, not
14
- * a full prompt. The authoritative goal state is injected once per turn by
15
- * before_agent_start; the persisted marker only needs to carry the goal id.
14
+ * a full prompt. The authoritative goal state is injected at the request tail by
15
+ * the context hook; the persisted marker only needs to carry the goal id.
16
16
  */
17
17
  export const CHECKPOINT_TRIGGER_MAX_CHARS = 160;
18
18
 
@@ -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");
@@ -264,8 +265,8 @@ export function objectiveEditedPrompt(goal: GoalRecord): string {
264
265
  * Deprecated compatibility wrapper (issue #30). The full continuation prompt
265
266
  * was the defect: every auto-continue turn persisted the whole objective/task/
266
267
  * contract/policy block as a custom session message, growing sessions by
267
- * ~6.4K chars per turn. The authoritative state is now injected once per turn
268
- * by before_agent_start; the persisted follow-up is only a tiny trigger.
268
+ * ~6.4K chars per turn. The authoritative state is now injected at the request tail
269
+ * by the context hook; the persisted follow-up is only a tiny trigger.
269
270
  *
270
271
  * Kept for one minor release so external call sites migrate explicitly.
271
272
  */
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.6",
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",
@@ -34,6 +34,7 @@
34
34
  "docs/agent-flow-design.md",
35
35
  "docs/agentic-runtime-prd.md",
36
36
  "docs/architecture.md",
37
+ "docs/prompt-caching.md",
37
38
  "docs/pi-autoresearch-survey.md",
38
39
  "docs/pi-goal-x-unified-dashboard-plan.md",
39
40
  "docs/unified-dashboard.md",