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.
- package/extensions/goal-auditor.ts +21 -20
- package/extensions/goal-commands.ts +20 -4
- package/extensions/goal-completion.ts +10 -0
- package/extensions/goal-core-tools.ts +32 -6
- package/extensions/goal-draft.ts +2 -70
- package/extensions/goal-drafting.ts +37 -7
- package/extensions/goal-events.ts +23 -4
- package/extensions/goal-format.ts +62 -3
- package/extensions/goal-ledger.ts +131 -7
- package/extensions/goal-policy.ts +29 -20
- package/extensions/goal-record.ts +2 -0
- package/extensions/goal-service.ts +217 -14
- package/extensions/goal-settings.ts +92 -4
- package/extensions/goal-state.ts +114 -39
- package/extensions/goal-task-confirmation.ts +5 -8
- package/extensions/goal-task-count.ts +59 -0
- package/extensions/goal-task-derive.ts +46 -0
- package/extensions/goal-task-tools.ts +2 -2
- package/extensions/goal-widget.ts +79 -2
- package/extensions/prompts/goal-prompts.ts +82 -35
- package/extensions/storage/goal-files.ts +112 -7
- package/extensions/storage/goal-lock.ts +7 -1
- package/extensions/widgets/dialog-scaffold.ts +26 -0
- package/extensions/widgets/goal-escape-dialog.ts +5 -9
- package/extensions/widgets/goal-widget.ts +140 -7
- package/extensions/widgets/task-list-overlay.ts +58 -39
- package/package.json +4 -2
|
@@ -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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
}
|
package/extensions/goal-draft.ts
CHANGED
|
@@ -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, "<").replace(/>/g, ">"));
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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);
|