pi-goal-x 0.30.4 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/docs/architecture.md +37 -10
- package/extensions/goal-activity.ts +38 -0
- package/extensions/goal-auditor.ts +40 -34
- package/extensions/goal-commands.ts +5 -5
- package/extensions/goal-compaction.ts +5 -11
- package/extensions/goal-completion.ts +4 -2
- package/extensions/goal-core-tools.ts +45 -51
- package/extensions/goal-detail.ts +65 -0
- package/extensions/goal-events.ts +48 -42
- package/extensions/goal-format.ts +24 -5
- package/extensions/goal-ledger-index.ts +57 -0
- package/extensions/goal-ledger.ts +117 -90
- package/extensions/goal-oracle.ts +23 -9
- package/extensions/goal-policy.ts +11 -6
- package/extensions/goal-pool.ts +3 -1
- package/extensions/goal-questionnaire.ts +2 -1
- package/extensions/goal-service.ts +120 -12
- package/extensions/goal-session-health.ts +25 -27
- package/extensions/goal-settings.ts +28 -3
- package/extensions/goal-state.ts +14 -9
- package/extensions/goal-task-confirmation.ts +2 -1
- package/extensions/goal-task-count.ts +2 -2
- package/extensions/goal-task-index.ts +57 -0
- package/extensions/goal-task-tools.ts +83 -34
- package/extensions/goal-tool-names.ts +11 -0
- package/extensions/prompts/goal-prompts.ts +71 -97
- package/extensions/storage/goal-files.ts +33 -29
- package/extensions/widgets/goal-dashboard-model.ts +27 -29
- package/extensions/widgets/goal-dashboard-renderer.ts +2 -1
- package/extensions/widgets/goal-escape-dialog.ts +2 -1
- package/extensions/widgets/goal-widget.ts +2 -1
- package/extensions/widgets/task-list-overlay.ts +2 -1
- package/extensions/widgets/text-cache.ts +30 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -251,6 +251,18 @@ Pressing `Esc` during active work pauses the goal.
|
|
|
251
251
|
|
|
252
252
|
The agent can divide a goal into tasks and subtasks and update them as work progresses. The current task is tracked explicitly (persisted as the goal's execution focus) and highlighted in the dashboard; starting a task with `update_goal_task(status="start")` sets it, and completing or skipping it clears it.
|
|
253
253
|
|
|
254
|
+
The model receives the objective excerpt, current task, next three pending tasks, progress, budget, and applicable rules. Full requirements, contracts, and evidence remain saved. `get_goal()` returns a compact summary; use `get_goal({section:"objective"})`, `section:"tasks"` (optionally `task_id`), or `section:"history"` for lossless pages of at most 4,000 content characters. Repeat the section and task with the returned `cursor` to continue. A changed source rejects stale cursors and asks for a restart. Existing `verbose` and `include_history` arguments remain supported.
|
|
255
|
+
|
|
256
|
+
Task progress can be recorded atomically in order:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{"updates":[{"task_id":"child","status":"complete","evidence":"Tests passed"},{"task_id":"parent","status":"complete"},{"task_id":"next","status":"start"}]}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Pass this to `update_goal_task`. A batch accepts 1–100 updates and validates every transition before applying any. Complete children before their parent; contracted tasks still require evidence, skips require reasons, and completed tasks cannot reopen. Single-task calls remain supported; do not mix the two forms. Each update retains its own ledger event. Turn-level writes check the original persisted revision, and completion audits wait for a successful flush.
|
|
263
|
+
|
|
264
|
+
Goal tools appear when applicable. Outside drafting, creation and inspection remain available; outcomes appear for active, paused, or budget-limited goals, task planning for active or paused goals, and task progress for active goals with a task tree. Disabling tasks hides both task tools. Drafting keeps its question/proposal tools, and ordinary work tools remain available.
|
|
265
|
+
|
|
254
266
|
Verification contracts describe the evidence required for completion. They can apply to the entire goal or to an individual task.
|
|
255
267
|
|
|
256
268
|
Examples include:
|
package/docs/architecture.md
CHANGED
|
@@ -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,
|
|
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` |
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
tools
|
|
212
|
-
|
|
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 (
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"
|
|
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(
|
|
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"
|
|
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
|
|
410
|
-
progress.recentOutput =
|
|
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")
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
173
|
-
for (const t of tasks) {
|
|
174
|
-
if (t.id === goal.currentTaskId) return t;
|
|
175
|
-
const nested = t.subtasks ? find(t.subtasks) : undefined;
|
|
176
|
-
if (nested) return nested;
|
|
177
|
-
}
|
|
178
|
-
return undefined;
|
|
179
|
-
};
|
|
180
|
-
const current = find(goal.taskList.tasks);
|
|
173
|
+
const current = taskIndex(goal.taskList.tasks).byId.get(goal.currentTaskId);
|
|
181
174
|
if (current) {
|
|
182
|
-
lines.push(`Current task: ${current.id} — ${current.title}${current.verificationContract ? ` (contract: ${current.verificationContract})` : ""}`);
|
|
175
|
+
lines.push(`Current task: ${truncateText(current.id, 80)} — ${truncateText(current.title, 180)}${current.verificationContract ? ` (contract: ${truncateText(current.verificationContract, 600)})` : ""}`);
|
|
176
|
+
if (current.id.length > 80 || current.title.length > 180 || (current.verificationContract?.length ?? 0) > 600) lines.push('Retrieve omitted task requirements with get_goal(section="tasks") before continuing.');
|
|
183
177
|
}
|
|
184
178
|
}
|
|
185
179
|
// Bounded recent-event tail.
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { loadGoalSettings, loadGoalSettingsFileConfig } from "./goal-settings.ts";
|
|
10
10
|
import { runGoalCompletionAuditor } from "./goal-auditor.ts";
|
|
11
11
|
import { nowIso, type GoalRecord } from "./goal-record.ts";
|
|
12
|
-
import { latestEventsForGoal,
|
|
12
|
+
import { latestEventsForGoal, goalRuntimeEvents } from "./goal-ledger.ts";
|
|
13
13
|
import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
|
|
14
14
|
import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
|
|
15
15
|
import type { GoalCore } from "./goal-state.ts";
|
|
@@ -23,6 +23,8 @@ import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
|
23
23
|
// UNTRUSTED executor claim — never evidence and never an approval bypass.
|
|
24
24
|
export async function runGoalCompletionFlow(core: GoalCore, ctx: ExtensionContext, completionSummary?: string): Promise<AgentToolResult<unknown>> {
|
|
25
25
|
const { pi } = core;
|
|
26
|
+
const flushError = core.goalService.flushForAudit(ctx);
|
|
27
|
+
if (flushError) return {content: [{type: "text", text: flushError}], details: goalDetails(core.state.goal)};
|
|
26
28
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
27
29
|
|
|
28
30
|
// -- Completion --
|
|
@@ -239,7 +241,7 @@ if (settings.disabled === true) {
|
|
|
239
241
|
|
|
240
242
|
// P1-6: warm start — seed the auditor with the parent-rendered ledger tail
|
|
241
243
|
// (recent lifecycle + task evidence) so it does not re-derive session facts.
|
|
242
|
-
const ledger =
|
|
244
|
+
const ledger = goalRuntimeEvents(ctx, auditTarget.id);
|
|
243
245
|
const warmTail = latestEventsForGoal(ledger, auditTarget.id, 8);
|
|
244
246
|
const warmContext = warmTail.length > 0
|
|
245
247
|
? `Recent goal events (from the shared ledger):\n${warmTail.map((e) => `- ${e.at} ${e.type}${"taskId" in e ? ` (task ${e.taskId})` : ""}${"evidence" in e && e.evidence ? ` evidence: ${e.evidence}` : ""}`).join("\n")}`
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { goalDetailPage, type GoalDetailSection } from "./goal-detail.ts";
|
|
2
|
+
import { taskIndex } from "./goal-task-index.ts";
|
|
1
3
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
2
4
|
import { defineTool, type AgentToolResult, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -7,7 +9,7 @@ import { detailedSummary, goalDetails, renderGoalResult } from "./goal-format.ts
|
|
|
7
9
|
import { budgetLine } from "./goal-accounting.ts";
|
|
8
10
|
import { buildGoalCreatedReport, buildTaskSummary, findTaskInTree, validateGoalAgentPause, validateGoalBlock } from "./goal-policy.ts";
|
|
9
11
|
import { buildUnfocusedOpenGoalsSummary, otherOpenGoalCount } from "./goal-pool.ts";
|
|
10
|
-
import { readGoalLedger } from "./goal-ledger.ts";
|
|
12
|
+
import { readGoalLedger, goalOracleState } from "./goal-ledger.ts";
|
|
11
13
|
import { loadGoalSettings } from "./goal-settings.ts";
|
|
12
14
|
import { buildGoalHistoryBlock, buildGoalTaskDetailBlock } from "./goal-format.ts";
|
|
13
15
|
import { sisyphusStepProgress } from "./goal-policy.ts";
|
|
@@ -20,7 +22,6 @@ import {
|
|
|
20
22
|
buildBlockerFingerprint,
|
|
21
23
|
consumeOracleFollowupMarker,
|
|
22
24
|
hasPendingOracleAdviceForFocusedGoal,
|
|
23
|
-
oracleStateForFingerprint,
|
|
24
25
|
renderActionableOracleAdvice,
|
|
25
26
|
renderOracleAdviceReminder,
|
|
26
27
|
runBlockerOracle,
|
|
@@ -30,15 +31,9 @@ import { loadSettingsSnapshot, type ResolvedGoalOracleSettings } from "./goal-se
|
|
|
30
31
|
/** Current + first-pending (excluding current) task pointers for concise get_goal. */
|
|
31
32
|
function conciseTaskPointers(goal: GoalRecord): { findCurrentTask?: GoalTask; firstPendingTask?: GoalTask } {
|
|
32
33
|
if (!goal.taskList) return {};
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
if (!firstPendingTask && t.status === "pending" && t.id !== goal.currentTaskId) firstPendingTask = t;
|
|
37
|
-
if (t.subtasks) walk(t.subtasks);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
walk(goal.taskList.tasks);
|
|
41
|
-
const findCurrentTask = goal.currentTaskId ? findTaskInTree(goal.taskList.tasks, goal.currentTaskId) : undefined;
|
|
34
|
+
const index = taskIndex(goal.taskList.tasks);
|
|
35
|
+
const firstPendingTask = index.pending.find(task => task.id !== goal.currentTaskId);
|
|
36
|
+
const findCurrentTask = goal.currentTaskId ? index.byId.get(goal.currentTaskId) : undefined;
|
|
42
37
|
return { findCurrentTask, firstPendingTask };
|
|
43
38
|
}
|
|
44
39
|
|
|
@@ -53,12 +48,13 @@ export function registerCoreTools(
|
|
|
53
48
|
pi.registerTool(defineTool({
|
|
54
49
|
name: "get_goal",
|
|
55
50
|
label: "Get Goal",
|
|
56
|
-
description: "
|
|
57
|
-
promptSnippet: "
|
|
58
|
-
promptGuidelines: [
|
|
59
|
-
"Use get_goal when you need the current goal before deciding whether to continue or mark it complete.",
|
|
60
|
-
],
|
|
51
|
+
description: "Read focused goal state. Default: compact summary. Retrieve full objective/contracts, tasks, or history in 4000-character pages with section and cursor. verbose preserves full legacy output.",
|
|
52
|
+
promptSnippet: "Inspect goal state or retrieve omitted requirements.",
|
|
53
|
+
promptGuidelines: [],
|
|
61
54
|
parameters: Type.Object({
|
|
55
|
+
section: Type.Optional(StringEnum(["summary", "objective", "tasks", "history"] as const)),
|
|
56
|
+
task_id: Type.Optional(Type.String({description: "With section=tasks, retrieve one task."})),
|
|
57
|
+
cursor: Type.Optional(Type.String({maxLength: 256, description: "Next page; repeat section/task."})),
|
|
62
58
|
verbose: Type.Optional(Type.Boolean({ description: "Full detail mode." })),
|
|
63
59
|
include_history: Type.Optional(Type.Boolean()),
|
|
64
60
|
}, { additionalProperties: false }),
|
|
@@ -66,7 +62,7 @@ pi.registerTool(defineTool({
|
|
|
66
62
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
67
63
|
if (core.state.goal) core.syncGoalPromptFromDisk(ctx);
|
|
68
64
|
const view = core.goalForDisplay() ?? core.state.goal;
|
|
69
|
-
const params = (_params ?? {}) as { verbose?: boolean; include_history?: boolean };
|
|
65
|
+
const params = (_params ?? {}) as { verbose?: boolean; include_history?: boolean; section?: "summary" | GoalDetailSection; task_id?: string; cursor?: string };
|
|
70
66
|
// PR E profile: legacy-v1 keeps pre-optimization verbose-by-default output;
|
|
71
67
|
// compact-v2 (default) returns a concise state line set because the full
|
|
72
68
|
// policy already lives in the injected active-goal system block.
|
|
@@ -74,15 +70,22 @@ pi.registerTool(defineTool({
|
|
|
74
70
|
const includeHistory = params.include_history === true || verbose;
|
|
75
71
|
const otherCount = otherOpenGoalCount(core.goalsById, core.focusedGoalId);
|
|
76
72
|
if (!view) {
|
|
77
|
-
const text = core.
|
|
78
|
-
? `${buildUnfocusedOpenGoalsSummary(core.
|
|
73
|
+
const text = otherOpenGoalCount(core.goalsById, null) > 0
|
|
74
|
+
? `${buildUnfocusedOpenGoalsSummary(otherOpenGoalCount(core.goalsById, null))}\n\nCall create_goal with the objective to create and focus a new goal, or ask the user to run /goal-focus to choose an open goal.`
|
|
79
75
|
: "No goal is set in this session. Call create_goal with the objective when the user explicitly asks to start a persistent goal.";
|
|
80
76
|
return {
|
|
81
77
|
content: [{ type: "text", text }],
|
|
82
78
|
details: goalDetails(view),
|
|
83
79
|
};
|
|
84
80
|
}
|
|
85
|
-
|
|
81
|
+
if (params.section && params.section !== "summary") {
|
|
82
|
+
if (!["objective", "tasks", "history"].includes(params.section)) return {content: [{type: "text", text: "Unknown goal section."}], details: goalDetails(view)};
|
|
83
|
+
const history = params.section === "history" ? readGoalLedger(ctx) : undefined;
|
|
84
|
+
const page = goalDetailPage(view, {section: params.section, task_id: params.task_id, cursor: params.cursor}, history?.events, history?.revision);
|
|
85
|
+
return {content: [{type: "text", text: page.text}], details: {...goalDetails(view), ...(page.ok ? {page: {content: page.content, nextCursor: page.nextCursor, totalChars: page.totalChars}} : {})}};
|
|
86
|
+
}
|
|
87
|
+
if (params.cursor || params.task_id) return {content: [{type: "text", text: "Use section=objective, tasks, or history for detail retrieval; task_id requires tasks."}], details: goalDetails(view)};
|
|
88
|
+
if (verbose && !params.section) {
|
|
86
89
|
const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`];
|
|
87
90
|
lines.push(`Objective: ${view.objective}`, "");
|
|
88
91
|
lines.push(`Status: ${statusLabel(view)}`);
|
|
@@ -125,20 +128,17 @@ pi.registerTool(defineTool({
|
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
//
|
|
129
|
-
// is kept — get_goal is an explicit state-read tool — but duplicated task
|
|
130
|
-
// headings and the lifecycle prose (already in the injected policy) are
|
|
131
|
-
// omitted.
|
|
131
|
+
// Compact state read; full requirements remain available through detail pages.
|
|
132
132
|
const lines: string[] = [`Goal ${view.id}: ${statusLabel(view)}, ${view.sisyphus ? "sisyphus" : "regular"}`];
|
|
133
|
-
lines.push(`Objective: ${view.objective}`);
|
|
133
|
+
lines.push(`Objective: ${truncateText(view.objective, 180)}${view.objective.length > 180 ? " (full: get_goal section=objective)" : ""}`);
|
|
134
134
|
if (view.taskList) {
|
|
135
135
|
const { findCurrentTask, firstPendingTask } = conciseTaskPointers(view);
|
|
136
136
|
if (findCurrentTask) {
|
|
137
|
-
const contract = findCurrentTask.verificationContract ? ` — contract: ${findCurrentTask.verificationContract}` : "";
|
|
138
|
-
lines.push(`Current task: ${findCurrentTask.id} — ${findCurrentTask.title}${contract}`);
|
|
137
|
+
const contract = findCurrentTask.verificationContract ? ` — contract: ${truncateText(findCurrentTask.verificationContract, 240)}` : "";
|
|
138
|
+
lines.push(`Current task: ${findCurrentTask.id} — ${truncateText(findCurrentTask.title, 180)}${contract}`);
|
|
139
139
|
}
|
|
140
140
|
if (firstPendingTask) {
|
|
141
|
-
lines.push(`Next pending: ${firstPendingTask.id} — ${firstPendingTask.title}`);
|
|
141
|
+
lines.push(`Next pending: ${firstPendingTask.id} — ${truncateText(firstPendingTask.title, 180)}`);
|
|
142
142
|
}
|
|
143
143
|
lines.push(`Tasks: ${buildTaskSummary(view.taskList)}`);
|
|
144
144
|
} else if (view.currentTaskId) {
|
|
@@ -149,6 +149,10 @@ pi.registerTool(defineTool({
|
|
|
149
149
|
if ((view.status === "paused" || view.status === "blocked") && view.pauseReason) {
|
|
150
150
|
lines.push(`Blocker: ${view.pauseReason}`);
|
|
151
151
|
}
|
|
152
|
+
if (params.include_history) {
|
|
153
|
+
const history = buildGoalHistoryBlock(view, readGoalLedger(ctx).events);
|
|
154
|
+
if (history) lines.push(history);
|
|
155
|
+
}
|
|
152
156
|
return {
|
|
153
157
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
154
158
|
details: goalDetails(view),
|
|
@@ -165,17 +169,13 @@ pi.registerTool(defineTool({
|
|
|
165
169
|
pi.registerTool(defineTool({
|
|
166
170
|
name: "create_goal",
|
|
167
171
|
label: "Create Goal",
|
|
168
|
-
description: "Create and focus a
|
|
169
|
-
promptSnippet: "Create a
|
|
170
|
-
promptGuidelines: [
|
|
171
|
-
"Call create_goal only when the user explicitly asks to start a long-running goal or hands you a concrete objective to pursue. Never infer a goal from an ordinary one-off task.",
|
|
172
|
-
"Creating a new goal focuses it and leaves other open goals untouched. Do not archive or replace existing goals unless the user explicitly asks through a user command.",
|
|
173
|
-
"Pass mode=\"sisyphus\" only when the user explicitly invoked Sisyphus mode.",
|
|
174
|
-
],
|
|
172
|
+
description: "Create and focus a persistent goal only on explicit user request; other open goals remain unchanged.",
|
|
173
|
+
promptSnippet: "Create a goal only when explicitly requested.",
|
|
174
|
+
promptGuidelines: ["Never infer persistent goals, Sisyphus mode, or token budgets from an ordinary task. The objective must faithfully preserve all user requirements and ordered steps."],
|
|
175
175
|
parameters: Type.Object({
|
|
176
|
-
objective: Type.String({ description: "Full
|
|
177
|
-
mode: Type.Optional(StringEnum(["regular", "sisyphus"] as const, { description: "
|
|
178
|
-
token_budget: Type.Optional(Type.Integer({ minimum: 1, description: "
|
|
176
|
+
objective: Type.String({ description: "Full objective; preserve ordered steps and done criteria." }),
|
|
177
|
+
mode: Type.Optional(StringEnum(["regular", "sisyphus"] as const, { description: "Default regular; sisyphus only if requested." })),
|
|
178
|
+
token_budget: Type.Optional(Type.Integer({ minimum: 1, description: "Whole-token budget, only if supplied by the user." })),
|
|
179
179
|
}, { additionalProperties: false }),
|
|
180
180
|
executionMode: "sequential",
|
|
181
181
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -311,7 +311,7 @@ pi.registerTool(defineTool({
|
|
|
311
311
|
const goalAtBlock = core.state.goal;
|
|
312
312
|
const focusToken = core.focusedOperationToken(goalAtBlock.id);
|
|
313
313
|
const fingerprint = buildBlockerFingerprint(goalAtBlock, reason);
|
|
314
|
-
const consult =
|
|
314
|
+
const consult = goalOracleState(ctx, goalAtBlock.id, fingerprint);
|
|
315
315
|
|
|
316
316
|
// One actionable result already exists.
|
|
317
317
|
if (consult.result?.disposition === "actionable") {
|
|
@@ -508,21 +508,15 @@ async function runGoalAgentPauseFlow(ctx: ExtensionContext, reason: string | und
|
|
|
508
508
|
pi.registerTool(defineTool({
|
|
509
509
|
name: "update_goal",
|
|
510
510
|
label: "Update Goal",
|
|
511
|
-
description: "Report
|
|
512
|
-
promptSnippet: "
|
|
513
|
-
promptGuidelines: [
|
|
514
|
-
// PR E §54: capability + hard boundary here; the WHEN rules (evidence,
|
|
515
|
-
// third-identical-blocker, objective immutability) live once in the
|
|
516
|
-
// canonical active-goal policy block — do not restate them in all four
|
|
517
|
-
// schema surfaces.
|
|
518
|
-
"An optional completion_summary is passed to the auditor as an UNTRUSTED claim — it is never evidence and can never substitute for real artifacts.",
|
|
519
|
-
],
|
|
511
|
+
description: "Report complete (independently audited), blocked (three consecutive identical blockers), or paused (immediate). reason is required for blocked/paused; completion_summary is an untrusted claim.",
|
|
512
|
+
promptSnippet: "Complete, block, or pause the focused goal.",
|
|
513
|
+
promptGuidelines: [],
|
|
520
514
|
parameters: Type.Object({
|
|
521
|
-
status: StringEnum(["complete", "blocked", "paused"] as const, { description: "
|
|
515
|
+
status: StringEnum(["complete", "blocked", "paused"] as const, { description: "Run outcome." }),
|
|
522
516
|
reason: Type.Optional(Type.String({ description: "Required when status is paused or blocked: describe the concrete blocker." })),
|
|
523
|
-
attempted_actions: Type.Optional(Type.Array(Type.String({ maxLength: 240 }), { maxItems: 8, description: "
|
|
517
|
+
attempted_actions: Type.Optional(Type.Array(Type.String({ maxLength: 240 }), { maxItems: 8, description: "Actions attempted against the blocker." })),
|
|
524
518
|
suggested_action: Type.Optional(Type.String({ description: "Optional suggested next step when status is paused." })),
|
|
525
|
-
completion_summary: Type.Optional(Type.String({ description: "
|
|
519
|
+
completion_summary: Type.Optional(Type.String({ description: "Untrusted completion claim; never evidence." })),
|
|
526
520
|
}, { additionalProperties: false }),
|
|
527
521
|
executionMode: "sequential",
|
|
528
522
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|