pi-goal-x 0.22.0 → 0.23.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.
@@ -13,6 +13,7 @@ import {
13
13
  type ResourceLoader,
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import type { GoalRecord, GoalTask, GoalTaskList } from "./goal-record.ts";
16
+ import { countTaskSubtree } from "./goal-task-count.ts";
16
17
  import { loadGoalSettings, type GoalSettings, type ThinkingLevel } from "./goal-settings.ts";
17
18
 
18
19
  export interface AuditorProgress {
@@ -84,27 +85,9 @@ function renderAuditorTaskTree(tasks: GoalTask[], indent: number): string[] {
84
85
  return lines;
85
86
  }
86
87
 
87
- function countAuditorTasks(tasks: GoalTask[]): { total: number; complete: number; skipped: number; pending: number } {
88
- let total = 0;
89
- let complete = 0;
90
- let skipped = 0;
91
- for (const t of tasks) {
92
- total++;
93
- if (t.status === "complete") complete++;
94
- else if (t.status === "skipped") skipped++;
95
- if (t.subtasks && t.subtasks.length > 0) {
96
- const child = countAuditorTasks(t.subtasks);
97
- total += child.total;
98
- complete += child.complete;
99
- skipped += child.skipped;
100
- }
101
- }
102
- return { total, complete, skipped, pending: total - complete - skipped };
103
- }
104
-
105
88
  function taskSummaryBlock(taskList?: GoalTaskList | null): string {
106
89
  if (!taskList || taskList.tasks.length === 0) return "";
107
- const { total, complete, skipped, pending } = countAuditorTasks(taskList.tasks);
90
+ const { total, complete, skipped, pending } = countTaskSubtree(taskList.tasks);
108
91
  const lines: string[] = [`Tasks: ${complete}/${total} complete${skipped > 0 ? `, ${skipped} skipped` : ""}`];
109
92
  lines.push(...renderAuditorTaskTree(taskList.tasks, 0));
110
93
  const gate = taskList.blockCompletion && pending > 0 ? " | TASK GATE: pending tasks block completion" : "";
@@ -117,6 +100,9 @@ export function buildGoalAuditorPrompt(args: {
117
100
  detailedSummary: string;
118
101
  completionSummary?: string | null;
119
102
  settings?: GoalSettings;
103
+ /** P1-6: parent-rendered evidence (ledger tail + turn trail) so the audit
104
+ * starts warm instead of re-deriving what the parent session already holds. */
105
+ warmContext?: string | null;
120
106
  }): string {
121
107
  return [
122
108
  "You are the independent completion auditor for pi-goal.",
@@ -153,6 +139,13 @@ export function buildGoalAuditorPrompt(args: {
153
139
  args.goal.verificationContract.trim(),
154
140
  "</verification_contract>",
155
141
  ] : []),
142
+ ...(args.warmContext?.trim() ? [
143
+ "",
144
+ "Warm parent context (already-rendered evidence from the executor session — inspect, do not re-derive):",
145
+ "<warm_context>",
146
+ args.warmContext.trim(),
147
+ "</warm_context>",
148
+ ] : []),
156
149
  "",
157
150
  "Audit checklist:",
158
151
  ...[
@@ -272,6 +265,8 @@ export async function runGoalCompletionAuditor(args: {
272
265
  detailedSummary: string;
273
266
  completionSummary?: string | null;
274
267
  settings?: GoalSettings;
268
+ /** P1-6: parent-rendered evidence (ledger tail + turn trail). */
269
+ warmContext?: string | null;
275
270
  signal?: AbortSignal;
276
271
  onProgress?: AuditorProgressCallback;
277
272
  /**
@@ -333,12 +328,18 @@ export async function runGoalCompletionAuditor(args: {
333
328
  },
334
329
  });
335
330
 
331
+ const projectResources = args.settings?.auditorProjectResources === true;
336
332
  const { session } = await createSession({
337
333
  cwd: args.ctx.cwd,
338
334
  model,
339
335
  thinkingLevel,
340
336
  ...resolveAuditorSessionModelOptions(args.ctx),
341
- resourceLoader: makeAuditorResourceLoader(),
337
+ // E3: default = the empty isolated loader (deliberate isolation);
338
+ // when auditorProjectResources is on, let the runtime build its own
339
+ // loader so the project's skills/extensions reach the auditor.
340
+ ...(projectResources
341
+ ? { resourceLoaderOptions: { noExtensions: false, noSkills: false, noPromptTemplates: false, noThemes: false, noContextFiles: false } }
342
+ : { resourceLoader: makeAuditorResourceLoader() }),
342
343
  sessionManager: SessionManager.inMemory(args.ctx.cwd),
343
344
  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
344
345
  tools: ["read", "grep", "find", "ls", "bash", REPORT_AUDITOR_PROGRESS_TOOL_NAME],
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { extractVerificationContract, sisyphusObjectiveSufficient } from "./goal-contract.ts";
3
- import { detailedSummary, oneLineSummary } from "./goal-format.ts";
3
+ import { buildGoalHistoryBlock, detailedSummary, oneLineSummary } from "./goal-format.ts";
4
4
  import {
5
5
  goalSettingsPath,
6
6
  loadGoalSettings,
@@ -15,6 +15,8 @@ import {
15
15
  otherOpenGoalCount,
16
16
  } from "./goal-pool.ts";
17
17
  import { clearGoalCommandMessage, validateResumeGoal } from "./goal-policy.ts";
18
+ import { readGoalLedger } from "./goal-ledger.ts";
19
+ import { effectiveSettingsReport, envOverrideFor } from "./goal-settings.ts";
18
20
  import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
19
21
  import { nowIso, type GoalMode, type GoalRecord } from "./goal-record.ts";
20
22
  import { clearGoalDrafting, hasActiveDraft, startGoalDrafting } from "./goal-drafting.ts";
@@ -158,7 +160,14 @@ export function registerGoalCommands(core: GoalCore): void {
158
160
  const view = core.goalForDisplay() ?? core.state.goal;
159
161
  const otherCount = otherOpenGoalCount(core.goalsById, core.focusedGoalId);
160
162
  const extra = view && otherCount > 0 ? `\nOther open goals: ${otherCount} (run /goal-list or /goal-focus)` : "";
161
- const text = view ? `${detailedSummary(view)}${extra}` : core.openGoals().length > 0 ? buildUnfocusedOpenGoalsSummary(core.openGoals().length) : detailedSummary(null);
163
+ let text = view ? `${detailedSummary(view)}${extra}` : core.openGoals().length > 0 ? buildUnfocusedOpenGoalsSummary(core.openGoals().length) : detailedSummary(null);
164
+ if (view) {
165
+ // E1: goal history (last audit verdict + recent lifecycle events).
166
+ const history = buildGoalHistoryBlock(view, readGoalLedger(ctx).events);
167
+ if (history) text += `\n\n${history}`;
168
+ // E2: effective settings with provenance.
169
+ text += `\n\n${effectiveSettingsReport(ctx.cwd).join("\n")}`;
170
+ }
162
171
  ctx.ui.notify(text, "info");
163
172
  core.updateUI(ctx);
164
173
  }
@@ -249,6 +258,7 @@ export function registerGoalCommands(core: GoalCore): void {
249
258
  const SETTING_ROWS: readonly SettingRow[] = [
250
259
  { key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
251
260
  { key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
261
+ { key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
252
262
  { key: "disableTasks", label: "disableTasks", section: "Task tracking", kind: "boolean" },
253
263
  { key: "subtaskDepth", label: "subtaskDepth", section: "Task tracking", kind: "positiveInteger" },
254
264
  { key: "disabled", label: "auditor disabled", section: "Completion auditor", kind: "boolean" },
@@ -258,10 +268,11 @@ export function registerGoalCommands(core: GoalCore): void {
258
268
  ];
259
269
 
260
270
  function settingsValue(config: GoalSettings, key: keyof GoalSettings): string {
261
- if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal") {
271
+ if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources") {
262
272
  return config[key] === true ? "true" : "false";
263
273
  }
264
274
  if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
275
+ if (key === "stallTimeoutMinutes") return config.stallTimeoutMinutes !== undefined ? String(config.stallTimeoutMinutes) : "0";
265
276
  return config[key] ?? "(default)";
266
277
  }
267
278
 
@@ -300,7 +311,8 @@ export function registerGoalCommands(core: GoalCore): void {
300
311
  options.push(`─── ${row.section} ───`);
301
312
  lastSection = row.section;
302
313
  }
303
- options.push(` ${row.label}: ${settingsValue(config, row.key)}`);
314
+ const envVar = envOverrideFor(row.key);
315
+ options.push(envVar ? ` ${row.label}: ${settingsValue(config, row.key)} (env: ${envVar} — read-only)` : ` ${row.label}: ${settingsValue(config, row.key)}`);
304
316
  }
305
317
  options.push("Done");
306
318
  const selected = await ctx.ui.select("Goal settings", options);
@@ -314,6 +326,10 @@ export function registerGoalCommands(core: GoalCore): void {
314
326
  const row = SETTING_ROWS.find((r) => r.label === label);
315
327
  if (!row) continue;
316
328
  const key = row.key;
329
+ if (envOverrideFor(key)) {
330
+ ctx.ui.notify(`${row.label} is read-only: overridden by the ${envOverrideFor(key)} env var.`, "warning");
331
+ continue;
332
+ }
317
333
  if (row.kind === "boolean") {
318
334
  const next = { ...config, [key]: config[key] !== true };
319
335
  saveSettings(next);
@@ -9,6 +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
13
  import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
13
14
  import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
14
15
  import type { GoalCore } from "./goal-state.ts";
@@ -235,12 +236,21 @@ if (settings.disabled === true) {
235
236
  const completionAuditController = new AbortController();
236
237
  core.auditAbortController = completionAuditController;
237
238
 
239
+ // P1-6: warm start — seed the auditor with the parent-rendered ledger tail
240
+ // (recent lifecycle + task evidence) so it does not re-derive session facts.
241
+ const ledger = readGoalLedger(ctx).events;
242
+ const warmTail = latestEventsForGoal(ledger, auditTarget.id, 8);
243
+ const warmContext = warmTail.length > 0
244
+ ? `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")}`
245
+ : null;
246
+
238
247
  const auditor = await (core.dependencies.runCompletionAuditor ?? runGoalCompletionAuditor)({
239
248
  ctx,
240
249
  goal: auditTarget,
241
250
  detailedSummary: detailedSummary(auditTarget),
242
251
  completionSummary: completionSummary?.trim() || undefined,
243
252
  settings: loadGoalSettings(ctx.cwd),
253
+ warmContext,
244
254
  signal: completionAuditController.signal,
245
255
  onProgress: (progress) => {
246
256
  core.auditProgress = {
@@ -7,6 +7,10 @@ import { detailedSummary, goalDetails, renderGoalResult } from "./goal-format.ts
7
7
  import { budgetLine } from "./goal-accounting.ts";
8
8
  import { buildGoalCreatedReport, buildTaskSummary, validateGoalAgentPause, validateGoalBlock } from "./goal-policy.ts";
9
9
  import { buildUnfocusedOpenGoalsSummary, otherOpenGoalCount } from "./goal-pool.ts";
10
+ import { readGoalLedger } from "./goal-ledger.ts";
11
+ import { buildGoalHistoryBlock, buildGoalTaskDetailBlock } from "./goal-format.ts";
12
+ import { sisyphusStepProgress } from "./goal-policy.ts";
13
+ import { deriveTasksFromObjective } from "./goal-task-derive.ts";
10
14
  import { nowIso, validateTokenBudgetInput } from "./goal-record.ts";
11
15
  import type { GoalCore } from "./goal-state.ts";
12
16
 
@@ -47,13 +51,23 @@ pi.registerTool(defineTool({
47
51
  const lines: string[] = [view.objective, ""];
48
52
  lines.push(`Status: ${statusLabel(view)}`);
49
53
  lines.push(`Mode: ${view.sisyphus ? "sisyphus" : "regular"}`);
54
+ if (view.sisyphus) {
55
+ // E6: sisyphus ordered-step progress.
56
+ const steps = sisyphusStepProgress(view);
57
+ if (steps) lines.push(`At step: ${steps.current} of ${steps.total}`);
58
+ }
50
59
  const usageBits: string[] = [];
51
60
  if (view.usage.activeSeconds > 0) usageBits.push(formatDuration(view.usage.activeSeconds));
52
61
  if (view.usage.tokensUsed > 0) usageBits.push(formatTokenValue(view.usage.tokensUsed));
53
62
  lines.push(`Usage: ${usageBits.length > 0 ? usageBits.join(" · ") : "none"}`);
54
63
  const budget = budgetLine(view);
55
64
  if (budget) lines.push(`Budget: ${budget}`);
56
- if (view.taskList) lines.push(`Tasks: ${buildTaskSummary(view.taskList)}`);
65
+ if (view.taskList) {
66
+ lines.push(`Tasks: ${buildTaskSummary(view.taskList)}`);
67
+ // F1: task-detail block mirroring the widget.
68
+ const detail = buildGoalTaskDetailBlock(view);
69
+ if (detail) lines.push("", detail);
70
+ }
57
71
  if (view.verificationContract?.trim()) lines.push(`Verification contract: ${view.verificationContract.trim()}`);
58
72
  if (view.status === "paused" || view.status === "blocked") {
59
73
  if (view.pauseReason) lines.push(`Blocker: ${view.pauseReason}`);
@@ -64,6 +78,9 @@ pi.registerTool(defineTool({
64
78
  if (otherCount > 0) lines.push(`Other open goals: ${otherCount} (user can run /goal-list or /goal-focus)`);
65
79
  lines.push("");
66
80
  lines.push("Lifecycle: call update_goal({status: \"complete\"}) only when every requirement is satisfied — the independent auditor verifies from actual evidence. Call update_goal({status: \"blocked\"}) only after the same blocker recurs on three consecutive goal turns. User commands handle pause/resume/clear/focus.");
81
+ // E1: goal history (last audit verdict + recent lifecycle events).
82
+ const history = buildGoalHistoryBlock(view, readGoalLedger(ctx).events);
83
+ if (history) lines.push("", history);
67
84
  return {
68
85
  content: [{ type: "text", text: lines.join("\n") }],
69
86
  details: goalDetails(view),
@@ -73,7 +90,7 @@ pi.registerTool(defineTool({
73
90
  return new Text(theme.fg("toolTitle", "get_goal"), 0, 0);
74
91
  },
75
92
  renderResult(result, _options, theme) {
76
- return renderGoalResult(result, theme);
93
+ return renderGoalResult(result, _options, theme);
77
94
  },
78
95
  }));
79
96
 
@@ -129,8 +146,14 @@ pi.registerTool(defineTool({
129
146
  const otherLine = otherCount > 0
130
147
  ? `\n${otherCount} other open goal${otherCount === 1 ? "" : "s"} remain in .pi/goals — this goal is now the session focus.`
131
148
  : "";
149
+ // F2: the agent path stays tool-driven — surface the derivable task plan
150
+ // as guidance so the agent proposes it via set_goal_tasks.
151
+ const derived = !created?.taskList ? deriveTasksFromObjective(cleanedObjective) : null;
152
+ const bootstrapLine = derived && derived.length > 0
153
+ ? `\n\nThe objective contains ${derived.length} ordered step${derived.length === 1 ? "" : "s"}; propose them as the task tree with set_goal_tasks if the user wants tracked milestones.`
154
+ : "";
132
155
  return {
133
- content: [{ type: "text", text: `${buildGoalCreatedReport({ objective: created?.objective ?? objective, detailedSummary: detailedSummary(created) })}${otherLine}` }],
156
+ content: [{ type: "text", text: `${buildGoalCreatedReport({ objective: created?.objective ?? objective, detailedSummary: detailedSummary(created) })}${bootstrapLine}${otherLine}` }],
134
157
  details: goalDetails(created),
135
158
  terminate: true,
136
159
  };
@@ -140,7 +163,7 @@ pi.registerTool(defineTool({
140
163
  return new Text(theme.fg("toolTitle", prefix) + theme.fg("muted", args?.objective ?? ""), 0, 0);
141
164
  },
142
165
  renderResult(result, _options, theme) {
143
- return renderGoalResult(result, theme);
166
+ return renderGoalResult(result, _options, theme);
144
167
  },
145
168
  }));
146
169
 
@@ -243,7 +266,7 @@ async function runGoalAgentPauseFlow(ctx: ExtensionContext, reason: string | und
243
266
  const suggestion = trimmedAction ? ` Suggested next step: ${trimmedAction}` : "";
244
267
  return {
245
268
  content: [{ type: "text", text: `Goal paused by the agent: ${trimmedReason}.${suggestion} Stop now; the user can resume with /goal-resume or revise with /goal-tweak.` }],
246
- details: goalDetails(core.state.goal),
269
+ details: goalDetails(core.state.goal, `Pause reason: ${trimmedReason}${trimmedAction ? `\nSuggested action: ${trimmedAction}` : ""}`), // E7
247
270
  terminate: true,
248
271
  };
249
272
  }
@@ -269,6 +292,9 @@ pi.registerTool(defineTool({
269
292
  }, { additionalProperties: false }),
270
293
  executionMode: "sequential",
271
294
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
295
+ // P1-3: persist any buffered in-turn mutations now so the auditor and
296
+ // status transitions observe the current task/state, not the stale disk.
297
+ core.flushGoalTransaction(ctx);
272
298
  if (params.status === "blocked") {
273
299
  return runGoalBlockedFlow(ctx);
274
300
  }
@@ -281,7 +307,7 @@ pi.registerTool(defineTool({
281
307
  return new Text(theme.fg("toolTitle", "update_goal ") + theme.fg("muted", args?.status ?? ""), 0, 0);
282
308
  },
283
309
  renderResult(result, _options, theme) {
284
- return renderGoalResult(result, theme);
310
+ return renderGoalResult(result, _options, theme);
285
311
  },
286
312
  }));
287
313
  }
@@ -1,4 +1,6 @@
1
1
  import type { GoalTask } from "./goal-record.ts";
2
+ import { promptSafeObjective } from "./goal-contract.ts";
3
+ import { renderConfirmationTasks } from "./goal-task-confirmation.ts";
2
4
 
3
5
  export type GoalDraftingFocus = "goal" | "sisyphus";
4
6
 
@@ -25,7 +27,6 @@ export type ToolGateDecision =
25
27
  | { block: true; reason: string };
26
28
 
27
29
  // ── Shared formatting helpers ──────────────────────────────────────────────
28
-
29
30
  function formatModeLabel(sisyphus: boolean): string {
30
31
  return sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal";
31
32
  }
@@ -49,75 +50,6 @@ function formatSection(title: string, content: string): string[] {
49
50
  return ["", `─── ${title} ───`, "", ...body];
50
51
  }
51
52
 
52
- export function renderConfirmationTasks(tasks: GoalTask[], indent: number): string[] {
53
- const prefix = " ".repeat(indent);
54
- const lines: string[] = [];
55
- for (const t of tasks) {
56
- const lw = t.lightweightSubtasks ? " (lightweight)" : "";
57
- const contract = t.verificationContract ? ` contract: ${t.verificationContract}` : "";
58
- lines.push(`${prefix}[ ] ${t.id}: ${t.title}${lw}${contract}`);
59
- if (t.subtasks && t.subtasks.length > 0) {
60
- lines.push(...renderConfirmationTasks(t.subtasks, indent + 1));
61
- }
62
- }
63
- return lines;
64
- }
65
-
66
- export function promptSafeObjective(objective: string): string {
67
- return objective.replace(/<\/?untrusted_objective>/gi, (tag) => tag.replace(/</g, "&lt;").replace(/>/g, "&gt;"));
68
- }
69
-
70
- const VERIFICATION_CONTRACT_RE = /^Verification contract:\s*(.+)$/im;
71
-
72
- const CONVENTIONAL_SECTION_NAMES = [
73
- "success criteria",
74
- "boundaries",
75
- "constraints",
76
- "if blocked",
77
- "if blocked / unclear / failing",
78
- "don'ts",
79
- "sisyphus reminder",
80
- "objective",
81
- "目标",
82
- "ordered steps",
83
- "order rules",
84
- "steps",
85
- ];
86
-
87
- /**
88
- * Extract a `Verification contract:` section from a goal objective and return
89
- * the cleaned objective (without the contract section) and the contract text.
90
- *
91
- * The contract section is a single line matching:
92
- * Verification contract: <text>
93
- *
94
- * It can appear anywhere in the objective, but by convention it goes after
95
- * the other sections (like Success criteria, Boundaries, Constraints).
96
- *
97
- * If no contract section is found, `verificationContract` is undefined.
98
- */
99
- export function extractVerificationContract(objective: string): { objective: string; verificationContract?: string } {
100
- const lines = objective.replace(/\r/g, "").split("\n");
101
- let contract: string | undefined;
102
- const filtered: string[] = [];
103
-
104
- for (const line of lines) {
105
- const trimmed = line.trim();
106
- const m = VERIFICATION_CONTRACT_RE.exec(trimmed);
107
- if (m) {
108
- contract = m[1].trim();
109
- // Skip this line — don't add it to the cleaned objective
110
- } else {
111
- filtered.push(line);
112
- }
113
- }
114
-
115
- return {
116
- objective: filtered.join("\n"),
117
- verificationContract: contract || undefined,
118
- };
119
- }
120
-
121
53
  export function buildDraftConfirmationText(args: {
122
54
  focus: GoalDraftingFocus;
123
55
  originalTopic: string;
@@ -2,7 +2,9 @@ import { Type } from "@earendil-works/pi-ai";
2
2
  import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
4
  import { extractVerificationContract, sisyphusObjectiveSufficient } from "./goal-contract.ts";
5
- import { buildDraftConfirmationText, buildTweakConfirmationText, goalDraftingPrompt, renderConfirmationTasks, type GoalDraftingFocus } from "./goal-draft.ts";
5
+ import { buildDraftConfirmationText, buildTweakConfirmationText, goalDraftingPrompt, type GoalDraftingFocus } from "./goal-draft.ts";
6
+ import { renderConfirmationTasks } from "./goal-task-confirmation.ts";
7
+ import { deriveTasksFromObjective } from "./goal-task-derive.ts";
6
8
  import { goalDetails, renderGoalResult } from "./goal-format.ts";
7
9
  import { buildGoalCreatedReport } from "./goal-policy.ts";
8
10
  import { loadGoalSettings } from "./goal-settings.ts";
@@ -39,6 +41,8 @@ interface ActiveGoalDraft {
39
41
  targetGoalId?: string;
40
42
  startedAt: string;
41
43
  auditorEnabled: boolean;
44
+ /** E5: formatted questionnaire Q&A to echo in the created-goal report. */
45
+ questionnaireEcho?: string;
42
46
  }
43
47
 
44
48
  const activeDrafts = new WeakMap<GoalCore, ActiveGoalDraft>();
@@ -183,7 +187,20 @@ function proposalText(draft: ActiveGoalDraft, objective: string, autoContinue: b
183
187
  const base = draft.mode === "tweak" && current
184
188
  ? buildTweakConfirmationText({ currentObjective: current.objective, newObjective: objective, changeSummary: draft.originalTopic || "Goal revised through guided drafting.", sisyphus: current.sisyphus, tasks: taskList?.tasks })
185
189
  : buildDraftConfirmationText({ focus: draft.mode === "sisyphus" ? "sisyphus" : "goal", originalTopic: draft.originalTopic, objective, autoContinue });
186
- return !taskList || draft.mode === "tweak" ? base : base + "\n\nTasks proposed for confirmation:\n" + renderConfirmationTasks(taskList.tasks, 0).join("\n");
190
+ // F2 (option A, user-approved 2026-08-05): when a new draft has no proposed
191
+ // task list but the objective has ≥2 checklist/ordered markers, preview the
192
+ // derived tree in the confirmation so the human can adjust it before the
193
+ // goal exists. Plain objectives render byte-identical to 383ae52.
194
+ let tasksText = "";
195
+ if (taskList && taskList.tasks.length > 0) {
196
+ tasksText = "\n\nTasks proposed for confirmation:\n" + renderConfirmationTasks(taskList.tasks, 0).join("\n");
197
+ } else if (draft.mode !== "tweak") {
198
+ const derived = deriveTasksFromObjective(base);
199
+ if (derived && derived.length > 0) {
200
+ tasksText = "\n\nTasks derived from the objective (confirm or ask the agent to adjust):\n" + renderConfirmationTasks(derived, 0).join("\n");
201
+ }
202
+ }
203
+ return !taskList && draft.mode === "tweak" ? base : base + tasksText;
187
204
  }
188
205
 
189
206
  function flatTaskSchema() {
@@ -220,7 +237,7 @@ export function registerDraftingTools(core: GoalCore): void {
220
237
  }
221
238
  },
222
239
  renderCall() { return new Text("goal_question", 0, 0); },
223
- renderResult(result, _opts, theme) { return renderGoalResult(result, theme); },
240
+ renderResult(result, _opts, theme) { return renderGoalResult(result, _opts, theme); },
224
241
  }));
225
242
 
226
243
  pi.registerTool(defineTool({
@@ -244,13 +261,17 @@ export function registerDraftingTools(core: GoalCore): void {
244
261
  core.enterGoalModal();
245
262
  try {
246
263
  const result = await runGoalQuestionnaire(ctx, questions);
264
+ if (!result.cancelled) {
265
+ const active = activeDraft(core);
266
+ if (active) active.questionnaireEcho = formatQuestionnaireAnswers(result); // E5
267
+ }
247
268
  return { content: [{ type: "text", text: result.cancelled ? "The user cancelled the questionnaire. Continue drafting conversationally." : formatQuestionnaireAnswers(result) }], details: goalDetails(core.state.goal) };
248
269
  } finally {
249
270
  core.exitGoalModal();
250
271
  }
251
272
  },
252
273
  renderCall() { return new Text("goal_questionnaire", 0, 0); },
253
- renderResult(result, _opts, theme) { return renderGoalResult(result, theme); },
274
+ renderResult(result, _opts, theme) { return renderGoalResult(result, _opts, theme); },
254
275
  }));
255
276
 
256
277
  pi.registerTool(defineTool({
@@ -316,9 +337,18 @@ export function registerDraftingTools(core: GoalCore): void {
316
337
  const settings = loadGoalSettings(ctx.cwd);
317
338
  const extracted = settings.disableContracts ? { objective, verificationContract: undefined } : extractVerificationContract(objective);
318
339
  if (draft.mode !== "tweak") {
319
- core.replaceGoal({ objective: extracted.objective, autoContinue: params.auto_continue !== false, sisyphus: expectedSisyphus, taskList: taskResult.value, skipAuditor }, ctx, true, extracted.verificationContract);
340
+ // F2: if the confirmation carried no task plan but the objective has
341
+ // structure, bootstrap the derived tree so the goal starts trackable.
342
+ const effectiveTaskList: GoalTaskList | undefined = taskResult.value ?? (() => {
343
+ const derived = deriveTasksFromObjective(extracted.objective);
344
+ return derived && derived.length > 0 ? { tasks: derived, blockCompletion: false, proposedAt: nowIso() } : undefined;
345
+ })();
346
+ core.replaceGoal({ objective: extracted.objective, autoContinue: params.auto_continue !== false, sisyphus: expectedSisyphus, taskList: effectiveTaskList, skipAuditor }, ctx, true, extracted.verificationContract);
320
347
  clearGoalDrafting(core, ctx);
321
- return { content: [{ type: "text", text: buildGoalCreatedReport({ objective: extracted.objective }) }], details: goalDetails(core.state.goal), terminate: true };
348
+ // E5: echo the questionnaire Q&A in the created-goal report so the
349
+ // user can verify their input survived the clarification loop.
350
+ const qaEcho = activeDraft(core)?.questionnaireEcho;
351
+ return { content: [{ type: "text", text: buildGoalCreatedReport({ objective: extracted.objective, detailedSummary: qaEcho }) }], details: goalDetails(core.state.goal), terminate: true };
322
352
  }
323
353
  if (!target) return { content: [{ type: "text", text: "The goal changed while drafting; review it and start /goal-tweak again." }], details: goalDetails(core.state.goal) };
324
354
  const token = core.focusedOperationToken(target.id);
@@ -335,6 +365,6 @@ export function registerDraftingTools(core: GoalCore): void {
335
365
  return { content: [{ type: "text", text: "Goal tweak confirmed and applied." }], details: goalDetails(result.goal), terminate: true };
336
366
  },
337
367
  renderCall(args, theme) { return new Text(theme.fg("toolTitle", "propose_goal_draft ") + theme.fg("muted", args?.objective ?? ""), 0, 0); },
338
- renderResult(result, _opts, theme) { return renderGoalResult(result, theme); },
368
+ renderResult(result, _opts, theme) { return renderGoalResult(result, _opts, theme); },
339
369
  }));
340
370
  }
@@ -14,8 +14,9 @@ import {
14
14
  import { buildCompactionSummary } from "./goal-compaction.ts";
15
15
  import { latestAuditorResultForGoal, readGoalLedger } from "./goal-ledger.ts";
16
16
  import { shouldArmPostCompactReminder, shouldInjectPostCompactReminder } from "./goal-policy.ts";
17
+ import { formatTokenValue } from "./goal-core.ts";
17
18
  import { loadGoalSettings } from "./goal-settings.ts";
18
- import { budgetLine } from "./goal-accounting.ts";
19
+ import { budgetLine, budgetRemaining } from "./goal-accounting.ts";
19
20
  import { asRecord, nowIso, type AssistantMessageLike } from "./goal-record.ts";
20
21
  import { goalSelectorLabel } from "./goal-pool.ts";
21
22
  import {
@@ -79,6 +80,8 @@ export function registerGoalEvents(core: GoalCore): void {
79
80
  core.advanceTurnSeq();
80
81
  core.goalWorkToolCalledThisTurn = false;
81
82
  core.beginAccounting();
83
+ core.goalService.beginTurn(ctx, core.focusedGoalId); // P1-3 transaction buffer
84
+ core.touchGoalActivity(); // F5
82
85
  core.updateUI(ctx);
83
86
  });
84
87
 
@@ -115,12 +118,14 @@ export function registerGoalEvents(core: GoalCore): void {
115
118
  });
116
119
 
117
120
  pi.on("tool_execution_end", async (_event, ctx) => {
121
+ core.touchGoalActivity(); // F5
118
122
  core.accountProgress(ctx);
119
123
  });
120
124
 
121
125
  pi.on("turn_end", async (event, ctx) => {
122
126
  const message = event.message as AssistantMessageLike;
123
127
  const tokens = assistantTurnTokens(message);
128
+ core.touchGoalActivity(); // F5
124
129
  core.accountProgress(ctx, { completedTurnTokens: tokens });
125
130
 
126
131
  if (isAbortedAssistantMessage(message)) {
@@ -185,6 +190,7 @@ export function registerGoalEvents(core: GoalCore): void {
185
190
  ) {
186
191
  core.queueContinuation(ctx);
187
192
  }
193
+ core.goalService.endTurn(ctx); // P1-3: single flush (lock + write + ledger batch)
188
194
  });
189
195
 
190
196
  pi.on("message_end", async (event, ctx) => {
@@ -196,7 +202,8 @@ export function registerGoalEvents(core: GoalCore): void {
196
202
  });
197
203
 
198
204
  pi.on("session_start", async (event, ctx) => {
199
- core.loadState(ctx);
205
+ core.goalService.flushTurn(ctx); // P1-3: persist any buffered transaction before reload
206
+ await core.loadState(ctx);
200
207
  core.installGoalToolProfile(!loadGoalSettings(ctx.cwd).disableTasks);
201
208
  rehydrateDraft(core, ctx);
202
209
  syncTerminalInputPause(core, ctx);
@@ -234,6 +241,7 @@ export function registerGoalEvents(core: GoalCore): void {
234
241
  });
235
242
 
236
243
  pi.on("session_compact", async (_event, ctx) => {
244
+ core.goalService.flushTurn(ctx); // P1-3: persist any buffered transaction before reload
237
245
  if (core.state.goal) core.persist(ctx);
238
246
  core.beginAccounting();
239
247
  // Arm a deterministic compaction summary for the next agent turn.
@@ -245,7 +253,8 @@ export function registerGoalEvents(core: GoalCore): void {
245
253
  });
246
254
 
247
255
  pi.on("session_tree", async (_event, ctx) => {
248
- core.loadState(ctx);
256
+ core.goalService.flushTurn(ctx); // P1-3: persist any buffered transaction before reload
257
+ await core.loadState(ctx);
249
258
  rehydrateDraft(core, ctx);
250
259
  syncTerminalInputPause(core, ctx);
251
260
  core.beginAccounting();
@@ -329,8 +338,15 @@ export function registerGoalEvents(core: GoalCore): void {
329
338
  if (core.state.goal?.status === "budget_limited") {
330
339
  const limitedGoal = core.state.goal;
331
340
  const budgetText = budgetLine(limitedGoal);
341
+ // E4: surface the remaining-vs-overshoot fact in the wrap-up steering.
342
+ const remaining = budgetRemaining(limitedGoal);
343
+ const balanceText = typeof remaining === "number"
344
+ ? remaining < 0
345
+ ? ` — ${formatTokenValue(-remaining)} over the budget`
346
+ : ` — ${formatTokenValue(remaining)} remaining`
347
+ : "";
332
348
  const reminder = core.runtime.consumePostBudgetReminder()
333
- ? `\n\n[TOKEN BUDGET REACHED goalId=${limitedGoal.id}]\nThe goal's token budget has been reached${budgetText ? ` (${budgetText})` : ""}. 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.`
349
+ ? `\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.`
334
350
  : "";
335
351
  return {
336
352
  systemPrompt: `${currentSystemPrompt()}\n\n[PI GOAL BUDGET LIMITED goalId=${limitedGoal.id}]\n${untrustedObjectiveBlock(limitedGoal)}${budgetText ? `\n${budgetText}` : ""}${reminder}`,
@@ -339,6 +355,9 @@ export function registerGoalEvents(core: GoalCore): void {
339
355
  const activeGoal = core.state.goal;
340
356
  const settings = loadGoalSettings(ctx.cwd);
341
357
  let prompt = goalPrompt(activeGoal, settings);
358
+ // F5: [GOAL STALLED] steering note when the detector fired.
359
+ const stalledNote = core.checkStall(ctx);
360
+ if (stalledNote) prompt += stalledNote;
342
361
  // Inject durable auditor feedback if the latest result was a rejection
343
362
  try {
344
363
  const ledger = readGoalLedger(ctx);