pi-goal-x 0.30.5 → 0.31.1

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 (37) hide show
  1. package/README.md +18 -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 +12 -11
  8. package/extensions/goal-core-tools.ts +45 -51
  9. package/extensions/goal-detail.ts +65 -0
  10. package/extensions/goal-drafting.ts +16 -8
  11. package/extensions/goal-events.ts +41 -40
  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 +76 -4
  18. package/extensions/goal-service.ts +120 -12
  19. package/extensions/goal-session-health.ts +25 -27
  20. package/extensions/goal-session-safety.ts +48 -0
  21. package/extensions/goal-settings.ts +28 -3
  22. package/extensions/goal-state.ts +17 -9
  23. package/extensions/goal-task-confirmation.ts +2 -1
  24. package/extensions/goal-task-count.ts +2 -2
  25. package/extensions/goal-task-index.ts +57 -0
  26. package/extensions/goal-task-tools.ts +83 -34
  27. package/extensions/goal-tool-names.ts +11 -0
  28. package/extensions/goal.ts +9 -0
  29. package/extensions/prompts/goal-prompts.ts +71 -97
  30. package/extensions/storage/goal-files.ts +33 -29
  31. package/extensions/widgets/goal-dashboard-model.ts +27 -29
  32. package/extensions/widgets/goal-dashboard-renderer.ts +2 -1
  33. package/extensions/widgets/goal-escape-dialog.ts +2 -1
  34. package/extensions/widgets/goal-widget.ts +2 -1
  35. package/extensions/widgets/task-list-overlay.ts +2 -1
  36. package/extensions/widgets/text-cache.ts +30 -0
  37. package/package.json +7 -2
package/README.md CHANGED
@@ -35,6 +35,12 @@ Sisyphus goals work well for migrations, staged refactors, release procedures, d
35
35
 
36
36
  The `/goal` and `/sisyphus` commands start a guided drafting process. The agent can ask focused questions, clarify the objective, and propose a task plan for confirmation.
37
37
 
38
+ RPC and browser hosts use native select/input dialogs when the terminal overlay is unavailable. Questions retain their context, and proposal confirmation includes a choice of completion auditor. Cancelling or losing a dialog never confirms an unseen proposal; the draft stays available.
39
+
40
+ When used with `pi-subagents`, goal mode stays in the parent session. Fresh, forked, resumed, and nested children inherit their assignment context without goal tools, goal state writes, or automatic goal continuations. Ordinary interactive forks retain goal mode.
41
+
42
+ Audit progress remains visible in the dashboard. Audit transcript messages appear after the agent settles and are excluded from model input; existing sessions containing interleaved audit events can resume after loading the updated extension, without rewriting their history.
43
+
38
44
  The proposal is written to the conversation as a durable summary (objective, plan, verification, automatic continuation, auditor state); confirming it creates and focuses the goal and starts working automatically.
39
45
 
40
46
  ### Direct goal creation
@@ -251,6 +257,18 @@ Pressing `Esc` during active work pauses the goal.
251
257
 
252
258
  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
259
 
260
+ 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.
261
+
262
+ Task progress can be recorded atomically in order:
263
+
264
+ ```json
265
+ {"updates":[{"task_id":"child","status":"complete","evidence":"Tests passed"},{"task_id":"parent","status":"complete"},{"task_id":"next","status":"start"}]}
266
+ ```
267
+
268
+ 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.
269
+
270
+ 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.
271
+
254
272
  Verification contracts describe the evidence required for completion. They can apply to the entire goal or to an individual task.
255
273
 
256
274
  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.
@@ -1,5 +1,5 @@
1
1
  import { type AgentToolResult, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { GOAL_AUDIT_ENTRY, detailedSummary, goalDetails, type GoalAuditEventDetails } from "./goal-format.ts";
2
+ import { GOAL_AUDIT_ENTRY, detailedSummary, goalDetails } from "./goal-format.ts";
3
3
  import {
4
4
  buildCompletionReport,
5
5
  buildTaskSummary,
@@ -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";
@@ -22,7 +22,8 @@ import type { GoalMutationOutcome } from "./goal-service.ts";
22
22
  // actual workspace evidence. An optional completion_summary is forwarded as an
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
- const { pi } = core;
25
+ const flushError = core.goalService.flushForAudit(ctx);
26
+ if (flushError) return {content: [{type: "text", text: flushError}], details: goalDetails(core.state.goal)};
26
27
  core.reconcileFocusedGoalFromDisk(ctx);
27
28
 
28
29
  // -- Completion --
@@ -129,7 +130,7 @@ function commitGoalCompletion(core: GoalCore, ctx: ExtensionContext, opts: {
129
130
  // records remain readable and honored for compatibility; no model tool or
130
131
  // task dialog creates new per-goal bypass state).
131
132
  if (auditTarget.skipAuditor) {
132
- pi.sendMessage<GoalAuditEventDetails>({
133
+ core.auditMessages.enqueue(ctx, {
133
134
  customType: GOAL_AUDIT_ENTRY,
134
135
  content: `Goal completed — per-goal auditor disabled.`,
135
136
  display: true,
@@ -159,7 +160,7 @@ if (auditTarget.skipAuditor) {
159
160
  // the auditor, records audit_skipped, and proceeds through the normal
160
161
  // deferred-completion path. No model-side bypass flag is required.
161
162
  if (settings.disabled === true) {
162
- pi.sendMessage<GoalAuditEventDetails>({
163
+ core.auditMessages.enqueue(ctx, {
163
164
  customType: GOAL_AUDIT_ENTRY,
164
165
  content: `Goal completed — auditor disabled in settings.`,
165
166
  display: true,
@@ -186,7 +187,7 @@ if (settings.disabled === true) {
186
187
  }
187
188
 
188
189
  // Auditor is enabled — run the normal audit flow
189
- await pi.sendMessage<GoalAuditEventDetails>({
190
+ core.auditMessages.enqueue(ctx, {
190
191
  customType: GOAL_AUDIT_ENTRY,
191
192
  content: [
192
193
  "Auditor: I am starting the independent completion audit.",
@@ -195,7 +196,7 @@ if (settings.disabled === true) {
195
196
  ].filter((line): line is string => line !== undefined).join("\n"),
196
197
  display: true,
197
198
  details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel },
198
- }, { triggerTurn: false });
199
+ });
199
200
  if (!core.isFocusedOperationCurrent(completionFocus)) {
200
201
  return core.focusedOperationCancelledResult("Goal completion", completionFocus);
201
202
  }
@@ -239,7 +240,7 @@ if (settings.disabled === true) {
239
240
 
240
241
  // P1-6: warm start — seed the auditor with the parent-rendered ledger tail
241
242
  // (recent lifecycle + task evidence) so it does not re-derive session facts.
242
- const ledger = readGoalLedger(ctx).events;
243
+ const ledger = goalRuntimeEvents(ctx, auditTarget.id);
243
244
  const warmTail = latestEventsForGoal(ledger, auditTarget.id, 8);
244
245
  const warmContext = warmTail.length > 0
245
246
  ? `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")}`
@@ -296,7 +297,7 @@ if (settings.disabled === true) {
296
297
 
297
298
  if (userChoice === "complete_without_audit") {
298
299
  // ── Mark complete without audit ────────────────────────────
299
- pi.sendMessage<GoalAuditEventDetails>({
300
+ core.auditMessages.enqueue(ctx, {
300
301
  customType: GOAL_AUDIT_ENTRY,
301
302
  content: `Goal completed — user bypassed audit via Escape.`,
302
303
  display: true,
@@ -377,7 +378,7 @@ if (settings.disabled === true) {
377
378
  "",
378
379
  auditor.output || "Auditor produced no approval marker.",
379
380
  ].filter((line): line is string => line !== undefined).join("\n");
380
- pi.sendMessage<GoalAuditEventDetails>({
381
+ core.auditMessages.enqueue(ctx, {
381
382
  customType: GOAL_AUDIT_ENTRY,
382
383
  content: rejectionText,
383
384
  display: true,
@@ -394,7 +395,7 @@ if (settings.disabled === true) {
394
395
  "",
395
396
  auditor.output || "Auditor approved completion.",
396
397
  ].filter((line): line is string => line !== undefined).join("\n");
397
- pi.sendMessage<GoalAuditEventDetails>({
398
+ core.auditMessages.enqueue(ctx, {
398
399
  customType: GOAL_AUDIT_ENTRY,
399
400
  content: approvalText,
400
401
  display: true,