pi-goal-x 0.30.4 → 0.31.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.
Files changed (35) hide show
  1. package/README.md +12 -0
  2. package/docs/architecture.md +37 -10
  3. package/extensions/goal-activity.ts +38 -0
  4. package/extensions/goal-auditor.ts +40 -34
  5. package/extensions/goal-commands.ts +5 -5
  6. package/extensions/goal-compaction.ts +5 -11
  7. package/extensions/goal-completion.ts +4 -2
  8. package/extensions/goal-core-tools.ts +45 -51
  9. package/extensions/goal-detail.ts +65 -0
  10. package/extensions/goal-events.ts +48 -42
  11. package/extensions/goal-format.ts +24 -5
  12. package/extensions/goal-ledger-index.ts +57 -0
  13. package/extensions/goal-ledger.ts +117 -90
  14. package/extensions/goal-oracle.ts +23 -9
  15. package/extensions/goal-policy.ts +11 -6
  16. package/extensions/goal-pool.ts +3 -1
  17. package/extensions/goal-questionnaire.ts +2 -1
  18. package/extensions/goal-service.ts +120 -12
  19. package/extensions/goal-session-health.ts +25 -27
  20. package/extensions/goal-settings.ts +28 -3
  21. package/extensions/goal-state.ts +14 -9
  22. package/extensions/goal-task-confirmation.ts +2 -1
  23. package/extensions/goal-task-count.ts +2 -2
  24. package/extensions/goal-task-index.ts +57 -0
  25. package/extensions/goal-task-tools.ts +83 -34
  26. package/extensions/goal-tool-names.ts +11 -0
  27. package/extensions/prompts/goal-prompts.ts +71 -97
  28. package/extensions/storage/goal-files.ts +33 -29
  29. package/extensions/widgets/goal-dashboard-model.ts +27 -29
  30. package/extensions/widgets/goal-dashboard-renderer.ts +2 -1
  31. package/extensions/widgets/goal-escape-dialog.ts +2 -1
  32. package/extensions/widgets/goal-widget.ts +2 -1
  33. package/extensions/widgets/task-list-overlay.ts +2 -1
  34. package/extensions/widgets/text-cache.ts +30 -0
  35. package/package.json +7 -2
package/README.md CHANGED
@@ -251,6 +251,18 @@ Pressing `Esc` during active work pauses the goal.
251
251
 
252
252
  The agent can divide a goal into tasks and subtasks and update them as work progresses. The current task is tracked explicitly (persisted as the goal's execution focus) and highlighted in the dashboard; starting a task with `update_goal_task(status="start")` sets it, and completing or skipping it clears it.
253
253
 
254
+ The model receives the objective excerpt, current task, next three pending tasks, progress, budget, and applicable rules. Full requirements, contracts, and evidence remain saved. `get_goal()` returns a compact summary; use `get_goal({section:"objective"})`, `section:"tasks"` (optionally `task_id`), or `section:"history"` for lossless pages of at most 4,000 content characters. Repeat the section and task with the returned `cursor` to continue. A changed source rejects stale cursors and asks for a restart. Existing `verbose` and `include_history` arguments remain supported.
255
+
256
+ Task progress can be recorded atomically in order:
257
+
258
+ ```json
259
+ {"updates":[{"task_id":"child","status":"complete","evidence":"Tests passed"},{"task_id":"parent","status":"complete"},{"task_id":"next","status":"start"}]}
260
+ ```
261
+
262
+ Pass this to `update_goal_task`. A batch accepts 1–100 updates and validates every transition before applying any. Complete children before their parent; contracted tasks still require evidence, skips require reasons, and completed tasks cannot reopen. Single-task calls remain supported; do not mix the two forms. Each update retains its own ledger event. Turn-level writes check the original persisted revision, and completion audits wait for a successful flush.
263
+
264
+ Goal tools appear when applicable. Outside drafting, creation and inspection remain available; outcomes appear for active, paused, or budget-limited goals, task planning for active or paused goals, and task progress for active goals with a task tree. Disabling tasks hides both task tools. Drafting keeps its question/proposal tools, and ordinary work tools remain available.
265
+
254
266
  Verification contracts describe the evidence required for completion. They can apply to the entire goal or to an individual task.
255
267
 
256
268
  Examples include:
@@ -35,7 +35,10 @@ handlers from their dedicated modules:
35
35
  | `goal-draft.ts` | Drafting prompt/confirmation text helpers (goalDraftingPrompt, buildDraftConfirmationText, renderConfirmationTasks, GoalDraftingFocus) |
36
36
  | `goal-drafting.ts` | Guided drafting orchestration: durable `pi-goal-draft` session entries (survive compaction/tree navigation), resume/replace/cancel protection, transient drafting profile, `goal_question`/`goal_questionnaire`/`propose_goal_draft` tools, per-draft auditor selection |
37
37
  | `goal-questionnaire.ts` | Structured question/answer UI (`runGoalQuestionnaire`, `showProposalDialog`) used by the drafting tools and confirmations |
38
- | `goal-tool-names.ts` | The five published tool-name constants, fixed three/five profiles, work/progress classification, post-stop allowlist |
38
+ | `goal-tool-names.ts` | The five published tool-name constants, lifecycle-dependent profiles, work/progress classification, post-stop allowlist |
39
+ | `goal-detail.ts` | Lossless objective/task/history paging with content-bound cursors |
40
+ | `goal-ledger-index.ts` | Incremental per-goal activity, audit, lifecycle, and Oracle projections |
41
+ | `goal-task-index.ts` | Content-keyed task snapshots shared by prompts, tools, and dashboard models |
39
42
  | `prompts/goal-prompts.ts` | Bounded five-tool steering prompts (active-goal, continuation, stale-checkpoint, unfocused, budget-limited) |
40
43
  | `storage/goal-files.ts` | Goal path safety, serialization/parsing, active-file scanning, active-file writes, archive writes, prompt-body merge from disk |
41
44
  | `widgets/goal-widget.ts` | Above-editor Goal Beacon component |
@@ -197,19 +200,43 @@ The extension registers five normal-execution tools and three drafting-only tool
197
200
  | Tool | Purpose |
198
201
  |---|---|
199
202
  | `create_goal` | Create and focus a new goal after an explicit user request (objective 1–4000 chars, optional `mode` regular/sisyphus and `token_budget`). |
200
- | `get_goal` | Read-only complete focused goal snapshot. |
203
+ | `get_goal` | Compact summary by default; lossless objective/tasks/history pages (up to 4,000 content characters), optional task selection, legacy verbose/history forms. |
201
204
  | `update_goal` | Run outcomes: `complete` (audited from actual evidence; optional `completion_summary` is an untrusted claim), `blocked` (after three consecutive identical blockers), or `paused` (immediate agent pause with required `reason`). |
202
205
  | `set_goal_tasks` | Create or structurally replace the task tree (flat parent-linked input, confirmation dialog, id-stable merge). |
203
- | `update_goal_task` | Update one task without stopping the turn: complete (evidence for contracted tasks), skipped (reason), pending (reopens skipped). |
206
+ | `update_goal_task` | Update one task or an ordered atomic batch: start, complete (evidence for contracted tasks), skipped (reason), pending (reopens skipped). |
204
207
  | `goal_question` | Drafting-only structured clarification question. |
205
208
  | `goal_questionnaire` | Drafting-only multi-question clarification UI. |
206
209
  | `propose_goal_draft` | Drafting-only objective/task proposal with Confirm or Continue Chatting. |
207
210
 
208
- The normal execution profile is fixed: exactly five goal tools when tasks are
209
- enabled, exactly three when disabled. A user-started guided draft is the sole
210
- exception: it replaces those goal tools with question/questionnaire/proposal
211
- tools until confirmation or cancellation. Ordinary pi work tools are never
212
- touched. Invalid lifecycle calls return concise state-aware tool results.
211
+ Outside drafting, creation and inspection are always advertised. Outcomes are
212
+ advertised for active/paused/budget-limited goals; task planning for active or
213
+ paused goals; task progress for active goals with tasks. Disabled tasks hide both
214
+ task tools. A guided draft replaces goal tools with question/questionnaire/proposal
215
+ tools until confirmation or cancellation. Ordinary pi work tools remain available.
216
+ The SDK active profile changes only when membership changes. Executors still
217
+ validate lifecycle state, including stale calls made after a transition.
218
+
219
+ Normal prompt/dashboard reads use per-goal ledger indexes: 12 recent events,
220
+ 64 activity candidates in stable timestamp order, pinned audit/lifecycle state,
221
+ and blocker-fingerprint Oracle state. Appends extend these indexes without
222
+ copying history. Version 3 derived checkpoints use UTF-8 byte offsets and retain
223
+ reconstructed Oracle advice; old or
224
+ corrupt checkpoints rebuild from the authoritative JSONL ledger. Full history
225
+ is loaded for explicit history/diagnostic requests. Saved goal/session formats
226
+ remain compatible. Task presentation caches use content snapshots; usage changes
227
+ do not rebuild task trees. Stable prompt rules precede changing goal data and
228
+ usage counters, with retrieval instructions beside bounded excerpts.
229
+
230
+ Warm settings resolution is cached by layer identity and applicable environment
231
+ values; public resolved values remain independently editable. Task and prompt
232
+ caches compare content fields without repeatedly serializing long contracts.
233
+ Compiled detail pages reuse their source and cursor hash; history sources are
234
+ invalidated by an opaque ledger generation on append or refresh. Cache size
235
+ limits bound retained data; larger requirements remain available losslessly.
236
+ Pure ANSI text wrapping/truncation is cached by text and layout arguments across
237
+ widgets and dialogs. Live auditor previews scan only the report tail, while the
238
+ complete final report remains available. Measurements and the module audit are
239
+ in `specs/2026-09-07-comprehensive-optimization/`.
213
240
 
214
241
  The `tool_call` interceptor blocks work tools after a stop tool has fired in
215
242
  the same turn, and blocks work tools when the checkpoint that triggered the
@@ -283,7 +310,7 @@ entries and run them in one Node process with small test-only adapters for the
283
310
  SDK values used by handlers. This avoids loading unrelated model-provider and
284
311
  TUI media modules. The fast path requires Node 22.15+; `test:serial` remains
285
312
  the slow, real-SDK, process-isolated
286
- compatibility path. The suites cover: surface baselines (exactly the fixed five/three tool
313
+ compatibility path. The suites cover: surface baselines (lifecycle-dependent goal tool
287
314
  profile and fourteen commands), golden file/ledger fixtures, stale-continuation behavior,
288
315
  GoalService mutation boundary, runtime/accounting, token-budget transitions,
289
316
  task-tool consolidation, verification contracts, the independent auditor,
@@ -298,7 +325,7 @@ The 2026-08-04 hardening plan
298
325
  is implemented: paused-status normalization (status authoritative, legacy
299
326
  `autoContinue: true` records stay paused), disk-fresh task transactions with
300
327
  structural-field clearing, token-budget integer validation, `task_reopened`
301
- ledger semantics with observable diagnostics, the three/five fixed tool
328
+ ledger semantics with observable diagnostics, the lifecycle-dependent tool
302
329
  profile, and the supported integration/experiment coverage described above.
303
330
  (the interim drafting-surface removal was later reversed by the product
304
331
  correction in the runtime follow-up, which restores guided drafting as a
@@ -136,6 +136,33 @@ function mapEvent(event: GoalLedgerEvent, taskTitles: ReadonlyMap<string, string
136
136
  }
137
137
  }
138
138
 
139
+ const indexedActivity = new WeakSet<readonly GoalLedgerEvent[]>();
140
+ export function indexedActivityEvents(events: GoalLedgerEvent[]): GoalLedgerEvent[] {
141
+ indexedActivity.add(events);
142
+ return events;
143
+ }
144
+
145
+ /** Stable event identity for append-order deduplication, independent of title edits. */
146
+ export function activityEventKey(event: GoalLedgerEvent): string | undefined {
147
+ const item = mapEvent(event, undefined);
148
+ return item ? `${item.kind}:${item.text}` : undefined;
149
+ }
150
+
151
+ const activityTypes = new Set([
152
+ "goal_created", "goal_tweaked", "auditor_toggled", "goal_paused", "goal_resumed", "goal_blocked",
153
+ "goal_budget_limited", "goal_completed", "goal_aborted", "task_started", "task_complete", "task_skipped",
154
+ "task_reopened", "completion_requested", "audit_started", "audit_result", "audit_skipped", "goal_archived",
155
+ ]);
156
+ export function isActivityEvent(event: GoalLedgerEvent): boolean { return activityTypes.has(event.type); }
157
+
158
+ /** Avoid formatting distinct task events during a cold ledger rebuild. */
159
+ export function sameActivityEvent(left: GoalLedgerEvent, right: GoalLedgerEvent): boolean {
160
+ if (left.type !== right.type) return false;
161
+ if ("taskId" in left && "taskId" in right && left.taskId !== right.taskId) return false;
162
+ // Reasons/evidence use the exact display normalization for true duplicates.
163
+ return activityEventKey(left) === activityEventKey(right);
164
+ }
165
+
139
166
  /**
140
167
  * Derive the readable activity feed for one goal from its durable ledger
141
168
  * events. Returns items in chronological order (oldest first), capped to the
@@ -148,6 +175,17 @@ export function deriveGoalActivity(
148
175
  ): GoalActivityItem[] {
149
176
  const { taskTitles, limit = 5 } = options;
150
177
  const items: GoalActivityItem[] = [];
178
+ if (indexedActivity.has(events)) {
179
+ // Runtime indexes already deduplicate and sort. Format only the requested tail.
180
+ for (let i = events.length - 1; i >= 0; i--) {
181
+ const event = events[i]!;
182
+ if (!("goalId" in event) || event.goalId !== goalId) continue;
183
+ const item = mapEvent(event, taskTitles);
184
+ if (item) items.push(item);
185
+ if (limit > 0 && items.length >= limit) break;
186
+ }
187
+ return items.reverse().slice(-limit);
188
+ }
151
189
  for (const event of events) {
152
190
  if (!("goalId" in event) || event.goalId !== goalId) continue;
153
191
  const item = mapEvent(event, taskTitles);
@@ -109,6 +109,20 @@ function escapePromptPayload(value: string): string {
109
109
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
110
110
  }
111
111
 
112
+ /** Stream previews need only the tail, not a split/copy of the full growing report. */
113
+ export function recentNonEmptyLines(text: string, limit: number): string[] {
114
+ const lines: string[] = [];
115
+ let end = text.length;
116
+ while (end >= 0 && lines.length < limit) {
117
+ const newline = end > 0 ? text.lastIndexOf("\n", end - 1) : -1;
118
+ const line = text.slice(newline + 1, end);
119
+ if (line.trim()) lines.push(line);
120
+ if (newline < 0) break;
121
+ end = newline;
122
+ }
123
+ return lines.reverse();
124
+ }
125
+
112
126
  /** §60: human-readable labels for the auditor's read-only tool set. */
113
127
  export function labelForReadOnlyTool(toolName: string): string {
114
128
  switch (toolName) {
@@ -160,15 +174,14 @@ export function buildGoalAuditorPrompt(args: {
160
174
  warmContext?: string | null;
161
175
  }): string {
162
176
  return [
163
- "You are the independent completion auditor for pi-goal.",
164
- "The executor claims the goal is complete. Your job is to decide whether the user's objective is actually satisfied.",
165
- "Be skeptical and semantic. Do not approve from paperwork, intent, file count, word count, build success, or a plausible summary alone.",
166
- "Use read/grep/find/ls/bash as needed to inspect real artifacts. Do not mutate files or run destructive commands.",
167
- "If the work is only an alpha scaffold, generated template, shallow draft, proxy milestone, or lacks the user-facing value requested, disapprove.",
168
- "If any explicit requirement is missing, weakly verified, contradicted, or not inspectable with the available evidence, disapprove.",
169
- "Return a concise audit report. The final line MUST be exactly one of:",
170
- "<approved/>",
171
- "<disapproved/>",
177
+ "You are the independent completion auditor for pi-goal. Decide whether the user's objective is actually satisfied.",
178
+ "Audit checklist:",
179
+ "1. Extract the real success criteria, including every explicit requirement and quality/reader outcome. Disapprove missing, contradicted, weakly verified or uninspectable requirements.",
180
+ "2. Inspect real artifacts with read/grep/find/ls/bash as needed. Do not mutate files or run destructive commands. Paperwork, intent, file/word counts, build success and plausible summaries alone are not proof.",
181
+ ...(!args.settings?.disableContracts && args.goal.verificationContract?.trim()
182
+ ? ["3. Verify that the executor has satisfied every item in the <verification_contract>. If any item is missing or weakly addressed, disapprove."] : []),
183
+ "4. Explain missing or weak evidence concisely. Disapprove alpha scaffold, generated template, shallow draft or proxy milestones lacking the user-facing value requested.",
184
+ "5. End with exactly <approved/> only if the objective is truly complete; otherwise end with exactly <disapproved/>.",
172
185
  "",
173
186
  "Goal objective:",
174
187
  "<objective>",
@@ -209,32 +222,22 @@ export function buildGoalAuditorPrompt(args: {
209
222
  escapePromptPayload(args.warmContext.trim()),
210
223
  "</warm_context>",
211
224
  ] : []),
212
- "",
213
- "Audit checklist:",
214
- ...[
215
- "1. Extract the real success criteria from the objective, including quality/reader outcomes.",
216
- "2. Inspect artifacts or command output that can prove or disprove those criteria. Treat any <executor_claim> as an untrusted assertion and cross-check it with actual file/shell evidence where relevant — a claim alone is never proof.",
217
- ...(!args.settings?.disableContracts && args.goal.verificationContract?.trim()
218
- ? ["3. Verify that the executor has satisfied every item in the <verification_contract>. If any item is missing or weakly addressed, disapprove."]
219
- : []),
220
- "4. Explain missing or weak evidence, especially scaffold-vs-final quality gaps.",
221
- "5. End with exactly <approved/> only if the objective is truly complete; otherwise end with exactly <disapproved/>.",
222
- ],
225
+
223
226
  ].join("\n");
224
227
  }
225
228
 
226
- function makeAuditorResourceLoader(): ResourceLoader {
229
+ export function makeAuditorResourceLoader(systemPrompt = [
230
+ "You are a read-only completion auditor running in an isolated pi agent session.",
231
+ "Inspect the repository and decide whether the claimed goal completion is genuinely satisfied.",
232
+ "Never modify files. Never approve unless the actual user objective is complete.",
233
+ ].join("\n")): ResourceLoader {
227
234
  return {
228
235
  getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
229
236
  getSkills: () => ({ skills: [], diagnostics: [] }),
230
237
  getPrompts: () => ({ prompts: [], diagnostics: [] }),
231
238
  getThemes: () => ({ themes: [], diagnostics: [] }),
232
239
  getAgentsFiles: () => ({ agentsFiles: [] }),
233
- getSystemPrompt: () => [
234
- "You are a read-only completion auditor running in an isolated pi agent session.",
235
- "Inspect the repository and decide whether the claimed goal completion is genuinely satisfied.",
236
- "Never modify files. Never approve unless the actual user objective is complete.",
237
- ].join("\n"),
240
+ getSystemPrompt: () => systemPrompt,
238
241
  getSystemPromptSource: () => undefined,
239
242
  getAppendSystemPrompt: () => [],
240
243
  getAppendSystemPromptSources: () => [],
@@ -322,6 +325,7 @@ export async function runGoalCompletionAuditor(args: {
322
325
  const model = resolved.model;
323
326
  const thinkingLevel = config.thinkingLevel;
324
327
  const outputParts: string[] = [];
328
+ let outputTail: string[] = [];
325
329
  if (resolved.error) {
326
330
  return { approved: false, disapproved: true, output: "", model: modelLabel(model), thinkingLevel, error: resolved.error };
327
331
  }
@@ -404,10 +408,10 @@ export async function runGoalCompletionAuditor(args: {
404
408
  const message = event.message as { role?: string; content?: Array<{ type?: string; text?: string }> };
405
409
  if (message?.role === "assistant") {
406
410
  for (const part of message.content ?? []) {
407
- if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
411
+ if (part.type === "text" && typeof part.text === "string") {
408
412
  // Keep the last 5 non-empty text lines for live display
409
- const lines = part.text.split("\n").filter((l: string) => l.trim());
410
- progress.recentOutput = [...lines.slice(-5)];
413
+ const lines = recentNonEmptyLines(part.text, 5);
414
+ if (lines.length) progress.recentOutput = lines;
411
415
  }
412
416
  }
413
417
  }
@@ -418,18 +422,19 @@ export async function runGoalCompletionAuditor(args: {
418
422
  const message = event.message as { role?: string; content?: Array<{ type?: string; text?: string }> };
419
423
  if (message.role !== "assistant") return;
420
424
  for (const part of message.content ?? []) {
421
- if (part.type === "text" && typeof part.text === "string") outputParts.push(part.text);
425
+ if (part.type === "text" && typeof part.text === "string") {
426
+ outputParts.push(part.text);
427
+ outputTail = [...outputTail, ...recentNonEmptyLines(part.text, 8)].slice(-8);
428
+ }
422
429
  }
423
430
  // Final report production: derived label for the widget.
424
- if (outputParts.some((t) => t.trim())) {
431
+ if (outputTail.length > 0) {
425
432
  progress.label = "Producing report...";
426
433
  progress.percentage = Math.max(progress.percentage ?? 0, 90);
427
434
  progress.phase = "producing_report";
428
435
  }
429
436
  // Show the accumulated output in progress
430
- const fullText = outputParts.join("\n\n");
431
- const lines = fullText.split("\n").filter((l: string) => l.trim());
432
- progress.recentOutput = lines.slice(-8);
437
+ progress.recentOutput = outputTail;
433
438
  emitProgress();
434
439
  });
435
440
  // Wire the external AbortSignal to abort the running session when fired
@@ -451,6 +456,7 @@ export async function runGoalCompletionAuditor(args: {
451
456
  progress.percentage = 100;
452
457
  emitProgress();
453
458
  unsubscribe();
459
+ session.dispose?.();
454
460
  }
455
461
  // session.abort() does NOT throw — the agent loop returns normally with
456
462
  // whatever output was captured before the abort. Check the signal after
@@ -170,7 +170,7 @@ export function registerGoalCommands(core: GoalCore): void {
170
170
  } catch {}
171
171
  }
172
172
  if (!current) {
173
- const openCount = core.openGoals().length;
173
+ const openCount = otherOpenGoalCount(core.goalsById, null);
174
174
  ctx.ui.notify(openCount > 0 ? buildUnfocusedOpenGoalsSummary(openCount) : detailedSummary(null), "info");
175
175
  return;
176
176
  }
@@ -306,7 +306,7 @@ export function registerGoalCommands(core: GoalCore): void {
306
306
  async function handleGoalPause(ctx: ExtensionContext): Promise<void> {
307
307
  core.reconcileFocusedGoalFromDisk(ctx);
308
308
  if (!core.state.goal) {
309
- if (core.openGoals().length > 0) {
309
+ if (otherOpenGoalCount(core.goalsById, null) > 0) {
310
310
  const selected = await chooseOpenGoal(ctx, "Pause which open goal?");
311
311
  if (!selected) return;
312
312
  } else {
@@ -329,7 +329,7 @@ export function registerGoalCommands(core: GoalCore): void {
329
329
 
330
330
  async function handleGoalResume(ctx: ExtensionContext): Promise<void> {
331
331
  core.reconcileFocusedGoalFromDisk(ctx);
332
- if (!core.state.goal && core.openGoals().length > 0) {
332
+ if (!core.state.goal && otherOpenGoalCount(core.goalsById, null) > 0) {
333
333
  const selected = await chooseOpenGoal(ctx, "Resume or focus open goal");
334
334
  if (!selected) return;
335
335
  if (selected.status === "active") {
@@ -660,7 +660,7 @@ export function registerGoalCommands(core: GoalCore): void {
660
660
  }
661
661
  async function handleGoalClear(ctx: ExtensionContext): Promise<void> {
662
662
  core.reconcileFocusedGoalFromDisk(ctx);
663
- if (!core.state.goal && core.openGoals().length > 0) {
663
+ if (!core.state.goal && otherOpenGoalCount(core.goalsById, null) > 0) {
664
664
  const selected = await chooseOpenGoal(ctx, "Clear which open goal?");
665
665
  if (!selected) return;
666
666
  }
@@ -700,7 +700,7 @@ export function registerGoalCommands(core: GoalCore): void {
700
700
  async function runGoalTweak(replacement: string, ctx: ExtensionContext): Promise<void> {
701
701
  core.reconcileFocusedGoalFromDisk(ctx);
702
702
  if (!core.state.goal) {
703
- if (core.openGoals().length > 0) {
703
+ if (otherOpenGoalCount(core.goalsById, null) > 0) {
704
704
  const selected = await chooseOpenGoal(ctx, "Tweak which open goal?");
705
705
  if (!selected) return;
706
706
  } else {
@@ -11,7 +11,8 @@ import {
11
11
  type GoalLedgerEvent,
12
12
  type LedgerStateReadResult,
13
13
  } from "./goal-ledger.ts";
14
- import { type GoalRecord, type GoalTask } from "./goal-record.ts";
14
+ import { type GoalRecord } from "./goal-record.ts";
15
+ import { taskIndex } from "./goal-task-index.ts";
15
16
 
16
17
  export function buildGoalCompactSummary(
17
18
  goal: GoalRecord,
@@ -169,17 +170,10 @@ export function buildPostCompactionGoalDelta(args: {
169
170
  lines.push(`[POST-COMPACTION RESYNC goalId=${goal.id}]`);
170
171
  // Execution focus (with contract) — the one thing memory must not lose.
171
172
  if (goal.currentTaskId && goal.taskList) {
172
- const find = (tasks: GoalTask[]): GoalTask | undefined => {
173
- for (const t of tasks) {
174
- if (t.id === goal.currentTaskId) return t;
175
- const nested = t.subtasks ? find(t.subtasks) : undefined;
176
- if (nested) return nested;
177
- }
178
- return undefined;
179
- };
180
- const current = find(goal.taskList.tasks);
173
+ const current = taskIndex(goal.taskList.tasks).byId.get(goal.currentTaskId);
181
174
  if (current) {
182
- lines.push(`Current task: ${current.id} — ${current.title}${current.verificationContract ? ` (contract: ${current.verificationContract})` : ""}`);
175
+ lines.push(`Current task: ${truncateText(current.id, 80)} — ${truncateText(current.title, 180)}${current.verificationContract ? ` (contract: ${truncateText(current.verificationContract, 600)})` : ""}`);
176
+ if (current.id.length > 80 || current.title.length > 180 || (current.verificationContract?.length ?? 0) > 600) lines.push('Retrieve omitted task requirements with get_goal(section="tasks") before continuing.');
183
177
  }
184
178
  }
185
179
  // Bounded recent-event tail.
@@ -9,7 +9,7 @@ import {
9
9
  import { loadGoalSettings, loadGoalSettingsFileConfig } from "./goal-settings.ts";
10
10
  import { runGoalCompletionAuditor } from "./goal-auditor.ts";
11
11
  import { nowIso, type GoalRecord } from "./goal-record.ts";
12
- import { latestEventsForGoal, readGoalLedger } from "./goal-ledger.ts";
12
+ import { latestEventsForGoal, goalRuntimeEvents } from "./goal-ledger.ts";
13
13
  import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
14
14
  import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
15
15
  import type { GoalCore } from "./goal-state.ts";
@@ -23,6 +23,8 @@ import type { GoalMutationOutcome } from "./goal-service.ts";
23
23
  // UNTRUSTED executor claim — never evidence and never an approval bypass.
24
24
  export async function runGoalCompletionFlow(core: GoalCore, ctx: ExtensionContext, completionSummary?: string): Promise<AgentToolResult<unknown>> {
25
25
  const { pi } = core;
26
+ const flushError = core.goalService.flushForAudit(ctx);
27
+ if (flushError) return {content: [{type: "text", text: flushError}], details: goalDetails(core.state.goal)};
26
28
  core.reconcileFocusedGoalFromDisk(ctx);
27
29
 
28
30
  // -- Completion --
@@ -239,7 +241,7 @@ if (settings.disabled === true) {
239
241
 
240
242
  // P1-6: warm start — seed the auditor with the parent-rendered ledger tail
241
243
  // (recent lifecycle + task evidence) so it does not re-derive session facts.
242
- const ledger = readGoalLedger(ctx).events;
244
+ const ledger = goalRuntimeEvents(ctx, auditTarget.id);
243
245
  const warmTail = latestEventsForGoal(ledger, auditTarget.id, 8);
244
246
  const warmContext = warmTail.length > 0
245
247
  ? `Recent goal events (from the shared ledger):\n${warmTail.map((e) => `- ${e.at} ${e.type}${"taskId" in e ? ` (task ${e.taskId})` : ""}${"evidence" in e && e.evidence ? ` evidence: ${e.evidence}` : ""}`).join("\n")}`
@@ -1,3 +1,5 @@
1
+ import { goalDetailPage, type GoalDetailSection } from "./goal-detail.ts";
2
+ import { taskIndex } from "./goal-task-index.ts";
1
3
  import { StringEnum, Type } from "@earendil-works/pi-ai";
2
4
  import { defineTool, type AgentToolResult, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
3
5
  import { Text } from "@earendil-works/pi-tui";
@@ -7,7 +9,7 @@ import { detailedSummary, goalDetails, renderGoalResult } from "./goal-format.ts
7
9
  import { budgetLine } from "./goal-accounting.ts";
8
10
  import { buildGoalCreatedReport, buildTaskSummary, findTaskInTree, validateGoalAgentPause, validateGoalBlock } from "./goal-policy.ts";
9
11
  import { buildUnfocusedOpenGoalsSummary, otherOpenGoalCount } from "./goal-pool.ts";
10
- import { readGoalLedger } from "./goal-ledger.ts";
12
+ import { readGoalLedger, goalOracleState } from "./goal-ledger.ts";
11
13
  import { loadGoalSettings } from "./goal-settings.ts";
12
14
  import { buildGoalHistoryBlock, buildGoalTaskDetailBlock } from "./goal-format.ts";
13
15
  import { sisyphusStepProgress } from "./goal-policy.ts";
@@ -20,7 +22,6 @@ import {
20
22
  buildBlockerFingerprint,
21
23
  consumeOracleFollowupMarker,
22
24
  hasPendingOracleAdviceForFocusedGoal,
23
- oracleStateForFingerprint,
24
25
  renderActionableOracleAdvice,
25
26
  renderOracleAdviceReminder,
26
27
  runBlockerOracle,
@@ -30,15 +31,9 @@ import { loadSettingsSnapshot, type ResolvedGoalOracleSettings } from "./goal-se
30
31
  /** Current + first-pending (excluding current) task pointers for concise get_goal. */
31
32
  function conciseTaskPointers(goal: GoalRecord): { findCurrentTask?: GoalTask; firstPendingTask?: GoalTask } {
32
33
  if (!goal.taskList) return {};
33
- let firstPendingTask: GoalTask | undefined;
34
- const walk = (tasks: GoalTask[]): void => {
35
- for (const t of tasks) {
36
- if (!firstPendingTask && t.status === "pending" && t.id !== goal.currentTaskId) firstPendingTask = t;
37
- if (t.subtasks) walk(t.subtasks);
38
- }
39
- };
40
- walk(goal.taskList.tasks);
41
- const findCurrentTask = goal.currentTaskId ? findTaskInTree(goal.taskList.tasks, goal.currentTaskId) : undefined;
34
+ const index = taskIndex(goal.taskList.tasks);
35
+ const firstPendingTask = index.pending.find(task => task.id !== goal.currentTaskId);
36
+ const findCurrentTask = goal.currentTaskId ? index.byId.get(goal.currentTaskId) : undefined;
42
37
  return { findCurrentTask, firstPendingTask };
43
38
  }
44
39
 
@@ -53,12 +48,13 @@ export function registerCoreTools(
53
48
  pi.registerTool(defineTool({
54
49
  name: "get_goal",
55
50
  label: "Get Goal",
56
- description: "Get the current pi goal for this session: objective, status, auto-continue, usage, and local file paths.",
57
- promptSnippet: "Read the active pi goal state for the current session.",
58
- promptGuidelines: [
59
- "Use get_goal when you need the current goal before deciding whether to continue or mark it complete.",
60
- ],
51
+ description: "Read focused goal state. Default: compact summary. Retrieve full objective/contracts, tasks, or history in 4000-character pages with section and cursor. verbose preserves full legacy output.",
52
+ promptSnippet: "Inspect goal state or retrieve omitted requirements.",
53
+ promptGuidelines: [],
61
54
  parameters: Type.Object({
55
+ section: Type.Optional(StringEnum(["summary", "objective", "tasks", "history"] as const)),
56
+ task_id: Type.Optional(Type.String({description: "With section=tasks, retrieve one task."})),
57
+ cursor: Type.Optional(Type.String({maxLength: 256, description: "Next page; repeat section/task."})),
62
58
  verbose: Type.Optional(Type.Boolean({ description: "Full detail mode." })),
63
59
  include_history: Type.Optional(Type.Boolean()),
64
60
  }, { additionalProperties: false }),
@@ -66,7 +62,7 @@ pi.registerTool(defineTool({
66
62
  core.reconcileFocusedGoalFromDisk(ctx);
67
63
  if (core.state.goal) core.syncGoalPromptFromDisk(ctx);
68
64
  const view = core.goalForDisplay() ?? core.state.goal;
69
- const params = (_params ?? {}) as { verbose?: boolean; include_history?: boolean };
65
+ const params = (_params ?? {}) as { verbose?: boolean; include_history?: boolean; section?: "summary" | GoalDetailSection; task_id?: string; cursor?: string };
70
66
  // PR E profile: legacy-v1 keeps pre-optimization verbose-by-default output;
71
67
  // compact-v2 (default) returns a concise state line set because the full
72
68
  // policy already lives in the injected active-goal system block.
@@ -74,15 +70,22 @@ pi.registerTool(defineTool({
74
70
  const includeHistory = params.include_history === true || verbose;
75
71
  const otherCount = otherOpenGoalCount(core.goalsById, core.focusedGoalId);
76
72
  if (!view) {
77
- const text = core.openGoals().length > 0
78
- ? `${buildUnfocusedOpenGoalsSummary(core.openGoals().length)}\n\nCall create_goal with the objective to create and focus a new goal, or ask the user to run /goal-focus to choose an open goal.`
73
+ const text = otherOpenGoalCount(core.goalsById, null) > 0
74
+ ? `${buildUnfocusedOpenGoalsSummary(otherOpenGoalCount(core.goalsById, null))}\n\nCall create_goal with the objective to create and focus a new goal, or ask the user to run /goal-focus to choose an open goal.`
79
75
  : "No goal is set in this session. Call create_goal with the objective when the user explicitly asks to start a persistent goal.";
80
76
  return {
81
77
  content: [{ type: "text", text }],
82
78
  details: goalDetails(view),
83
79
  };
84
80
  }
85
- if (verbose) {
81
+ if (params.section && params.section !== "summary") {
82
+ if (!["objective", "tasks", "history"].includes(params.section)) return {content: [{type: "text", text: "Unknown goal section."}], details: goalDetails(view)};
83
+ const history = params.section === "history" ? readGoalLedger(ctx) : undefined;
84
+ const page = goalDetailPage(view, {section: params.section, task_id: params.task_id, cursor: params.cursor}, history?.events, history?.revision);
85
+ return {content: [{type: "text", text: page.text}], details: {...goalDetails(view), ...(page.ok ? {page: {content: page.content, nextCursor: page.nextCursor, totalChars: page.totalChars}} : {})}};
86
+ }
87
+ 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)};
88
+ if (verbose && !params.section) {
86
89
  const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`];
87
90
  lines.push(`Objective: ${view.objective}`, "");
88
91
  lines.push(`Status: ${statusLabel(view)}`);
@@ -125,20 +128,17 @@ pi.registerTool(defineTool({
125
128
  };
126
129
  }
127
130
 
128
- // compact-v2 default (PR E §53): concise state read. The full objective
129
- // is kept — get_goal is an explicit state-read tool — but duplicated task
130
- // headings and the lifecycle prose (already in the injected policy) are
131
- // omitted.
131
+ // Compact state read; full requirements remain available through detail pages.
132
132
  const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`];
133
- lines.push(`Objective: ${view.objective}`);
133
+ lines.push(`Objective: ${truncateText(view.objective, 180)}${view.objective.length > 180 ? " (full: get_goal section=objective)" : ""}`);
134
134
  if (view.taskList) {
135
135
  const { findCurrentTask, firstPendingTask } = conciseTaskPointers(view);
136
136
  if (findCurrentTask) {
137
- const contract = findCurrentTask.verificationContract ? ` — contract: ${findCurrentTask.verificationContract}` : "";
138
- lines.push(`Current task: ${findCurrentTask.id} — ${findCurrentTask.title}${contract}`);
137
+ const contract = findCurrentTask.verificationContract ? ` — contract: ${truncateText(findCurrentTask.verificationContract, 240)}` : "";
138
+ lines.push(`Current task: ${findCurrentTask.id} — ${truncateText(findCurrentTask.title, 180)}${contract}`);
139
139
  }
140
140
  if (firstPendingTask) {
141
- lines.push(`Next pending: ${firstPendingTask.id} — ${firstPendingTask.title}`);
141
+ lines.push(`Next pending: ${firstPendingTask.id} — ${truncateText(firstPendingTask.title, 180)}`);
142
142
  }
143
143
  lines.push(`Tasks: ${buildTaskSummary(view.taskList)}`);
144
144
  } else if (view.currentTaskId) {
@@ -149,6 +149,10 @@ pi.registerTool(defineTool({
149
149
  if ((view.status === "paused" || view.status === "blocked") && view.pauseReason) {
150
150
  lines.push(`Blocker: ${view.pauseReason}`);
151
151
  }
152
+ if (params.include_history) {
153
+ const history = buildGoalHistoryBlock(view, readGoalLedger(ctx).events);
154
+ if (history) lines.push(history);
155
+ }
152
156
  return {
153
157
  content: [{ type: "text", text: lines.join("\n") }],
154
158
  details: goalDetails(view),
@@ -165,17 +169,13 @@ pi.registerTool(defineTool({
165
169
  pi.registerTool(defineTool({
166
170
  name: "create_goal",
167
171
  label: "Create Goal",
168
- description: "Create and focus a new pi goal after an explicit user request. Only call this when the user has explicitly asked to make something a persistent goal (directly, or via /goal or /sisyphus); do NOT infer a goal from an ordinary task. Creating a goal focuses it and leaves other open goals untouched.",
169
- promptSnippet: "Create a persistent pi goal only when the user explicitly asks for one.",
170
- promptGuidelines: [
171
- "Call create_goal only when the user explicitly asks to start a long-running goal or hands you a concrete objective to pursue. Never infer a goal from an ordinary one-off task.",
172
- "Creating a new goal focuses it and leaves other open goals untouched. Do not archive or replace existing goals unless the user explicitly asks through a user command.",
173
- "Pass mode=\"sisyphus\" only when the user explicitly invoked Sisyphus mode.",
174
- ],
172
+ description: "Create and focus a persistent goal only on explicit user request; other open goals remain unchanged.",
173
+ promptSnippet: "Create a goal only when explicitly requested.",
174
+ promptGuidelines: ["Never infer persistent goals, Sisyphus mode, or token budgets from an ordinary task. The objective must faithfully preserve all user requirements and ordered steps."],
175
175
  parameters: Type.Object({
176
- objective: Type.String({ description: "Full goal text. For Sisyphus goals this MUST include the user's numbered steps + per-step done criteria, taken faithfully from the user's input. Length is capped by the `max objective length` goal setting (0/unset = no limit)." }),
177
- mode: Type.Optional(StringEnum(["regular", "sisyphus"] as const, { description: "Goal mode. Defaults to regular. Use sisyphus only when the user explicitly invoked Sisyphus mode." })),
178
- token_budget: Type.Optional(Type.Integer({ minimum: 1, description: "Optional token budget in whole tokens. Accept it only when the user explicitly supplied a budget; never invent one." })),
176
+ objective: Type.String({ description: "Full objective; preserve ordered steps and done criteria." }),
177
+ mode: Type.Optional(StringEnum(["regular", "sisyphus"] as const, { description: "Default regular; sisyphus only if requested." })),
178
+ token_budget: Type.Optional(Type.Integer({ minimum: 1, description: "Whole-token budget, only if supplied by the user." })),
179
179
  }, { additionalProperties: false }),
180
180
  executionMode: "sequential",
181
181
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -311,7 +311,7 @@ pi.registerTool(defineTool({
311
311
  const goalAtBlock = core.state.goal;
312
312
  const focusToken = core.focusedOperationToken(goalAtBlock.id);
313
313
  const fingerprint = buildBlockerFingerprint(goalAtBlock, reason);
314
- const consult = oracleStateForFingerprint(readGoalLedger(ctx).events, goalAtBlock.id, fingerprint);
314
+ const consult = goalOracleState(ctx, goalAtBlock.id, fingerprint);
315
315
 
316
316
  // One actionable result already exists.
317
317
  if (consult.result?.disposition === "actionable") {
@@ -508,21 +508,15 @@ async function runGoalAgentPauseFlow(ctx: ExtensionContext, reason: string | und
508
508
  pi.registerTool(defineTool({
509
509
  name: "update_goal",
510
510
  label: "Update Goal",
511
- description: "Report a terminal or pausing outcome for the current run: \"complete\" runs the independent completion auditor (completion_summary is an untrusted claim only); \"blocked\" records a distinct agent-blocked state and stops continuation per the active-goal policy; \"paused\" pauses immediately with a required reason. Never archive or abandon a goal yourself — ask the user to run /goal-clear.",
512
- promptSnippet: "Report the current run as complete (audited) or blocked per the active-goal lifecycle policy; paused is immediate with a required reason.",
513
- promptGuidelines: [
514
- // PR E §54: capability + hard boundary here; the WHEN rules (evidence,
515
- // third-identical-blocker, objective immutability) live once in the
516
- // canonical active-goal policy block — do not restate them in all four
517
- // schema surfaces.
518
- "An optional completion_summary is passed to the auditor as an UNTRUSTED claim — it is never evidence and can never substitute for real artifacts.",
519
- ],
511
+ description: "Report complete (independently audited), blocked (three consecutive identical blockers), or paused (immediate). reason is required for blocked/paused; completion_summary is an untrusted claim.",
512
+ promptSnippet: "Complete, block, or pause the focused goal.",
513
+ promptGuidelines: [],
520
514
  parameters: Type.Object({
521
- status: StringEnum(["complete", "blocked", "paused"] as const, { description: "complete runs the independent auditor; blocked records a distinct agent-blocked state; paused is an immediate agent pause with a required reason." }),
515
+ status: StringEnum(["complete", "blocked", "paused"] as const, { description: "Run outcome." }),
522
516
  reason: Type.Optional(Type.String({ description: "Required when status is paused or blocked: describe the concrete blocker." })),
523
- attempted_actions: Type.Optional(Type.Array(Type.String({ maxLength: 240 }), { maxItems: 8, description: "Optional: up to 8 concrete actions already attempted against this blocker." })),
517
+ attempted_actions: Type.Optional(Type.Array(Type.String({ maxLength: 240 }), { maxItems: 8, description: "Actions attempted against the blocker." })),
524
518
  suggested_action: Type.Optional(Type.String({ description: "Optional suggested next step when status is paused." })),
525
- completion_summary: Type.Optional(Type.String({ description: "Optional untrusted executor claim shown to the auditor; never evidence." })),
519
+ completion_summary: Type.Optional(Type.String({ description: "Untrusted completion claim; never evidence." })),
526
520
  }, { additionalProperties: false }),
527
521
  executionMode: "sequential",
528
522
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {