pi-goal-x 0.30.5 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -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 +12 -11
- package/extensions/goal-core-tools.ts +45 -51
- package/extensions/goal-detail.ts +65 -0
- package/extensions/goal-drafting.ts +16 -8
- package/extensions/goal-events.ts +41 -40
- 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 +76 -4
- package/extensions/goal-service.ts +120 -12
- package/extensions/goal-session-health.ts +25 -27
- package/extensions/goal-session-safety.ts +48 -0
- package/extensions/goal-settings.ts +28 -3
- package/extensions/goal-state.ts +17 -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/goal.ts +9 -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
|
@@ -35,6 +35,12 @@ Sisyphus goals work well for migrations, staged refactors, release procedures, d
|
|
|
35
35
|
|
|
36
36
|
The `/goal` and `/sisyphus` commands start a guided drafting process. The agent can ask focused questions, clarify the objective, and propose a task plan for confirmation.
|
|
37
37
|
|
|
38
|
+
RPC and browser hosts use native select/input dialogs when the terminal overlay is unavailable. Questions retain their context, and proposal confirmation includes a choice of completion auditor. Cancelling or losing a dialog never confirms an unseen proposal; the draft stays available.
|
|
39
|
+
|
|
40
|
+
When used with `pi-subagents`, goal mode stays in the parent session. Fresh, forked, resumed, and nested children inherit their assignment context without goal tools, goal state writes, or automatic goal continuations. Ordinary interactive forks retain goal mode.
|
|
41
|
+
|
|
42
|
+
Audit progress remains visible in the dashboard. Audit transcript messages appear after the agent settles and are excluded from model input; existing sessions containing interleaved audit events can resume after loading the updated extension, without rewriting their history.
|
|
43
|
+
|
|
38
44
|
The proposal is written to the conversation as a durable summary (objective, plan, verification, automatic continuation, auditor state); confirming it creates and focuses the goal and starts working automatically.
|
|
39
45
|
|
|
40
46
|
### Direct goal creation
|
|
@@ -251,6 +257,18 @@ Pressing `Esc` during active work pauses the goal.
|
|
|
251
257
|
|
|
252
258
|
The agent can divide a goal into tasks and subtasks and update them as work progresses. The current task is tracked explicitly (persisted as the goal's execution focus) and highlighted in the dashboard; starting a task with `update_goal_task(status="start")` sets it, and completing or skipping it clears it.
|
|
253
259
|
|
|
260
|
+
The model receives the objective excerpt, current task, next three pending tasks, progress, budget, and applicable rules. Full requirements, contracts, and evidence remain saved. `get_goal()` returns a compact summary; use `get_goal({section:"objective"})`, `section:"tasks"` (optionally `task_id`), or `section:"history"` for lossless pages of at most 4,000 content characters. Repeat the section and task with the returned `cursor` to continue. A changed source rejects stale cursors and asks for a restart. Existing `verbose` and `include_history` arguments remain supported.
|
|
261
|
+
|
|
262
|
+
Task progress can be recorded atomically in order:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{"updates":[{"task_id":"child","status":"complete","evidence":"Tests passed"},{"task_id":"parent","status":"complete"},{"task_id":"next","status":"start"}]}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Pass this to `update_goal_task`. A batch accepts 1–100 updates and validates every transition before applying any. Complete children before their parent; contracted tasks still require evidence, skips require reasons, and completed tasks cannot reopen. Single-task calls remain supported; do not mix the two forms. Each update retains its own ledger event. Turn-level writes check the original persisted revision, and completion audits wait for a successful flush.
|
|
269
|
+
|
|
270
|
+
Goal tools appear when applicable. Outside drafting, creation and inspection remain available; outcomes appear for active, paused, or budget-limited goals, task planning for active or paused goals, and task progress for active goals with a task tree. Disabling tasks hides both task tools. Drafting keeps its question/proposal tools, and ordinary work tools remain available.
|
|
271
|
+
|
|
254
272
|
Verification contracts describe the evidence required for completion. They can apply to the entire goal or to an individual task.
|
|
255
273
|
|
|
256
274
|
Examples include:
|
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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AgentToolResult, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { GOAL_AUDIT_ENTRY, detailedSummary, goalDetails
|
|
2
|
+
import { GOAL_AUDIT_ENTRY, detailedSummary, goalDetails } from "./goal-format.ts";
|
|
3
3
|
import {
|
|
4
4
|
buildCompletionReport,
|
|
5
5
|
buildTaskSummary,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { loadGoalSettings, loadGoalSettingsFileConfig } from "./goal-settings.ts";
|
|
10
10
|
import { runGoalCompletionAuditor } from "./goal-auditor.ts";
|
|
11
11
|
import { nowIso, type GoalRecord } from "./goal-record.ts";
|
|
12
|
-
import { latestEventsForGoal,
|
|
12
|
+
import { latestEventsForGoal, goalRuntimeEvents } from "./goal-ledger.ts";
|
|
13
13
|
import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
|
|
14
14
|
import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
|
|
15
15
|
import type { GoalCore } from "./goal-state.ts";
|
|
@@ -22,7 +22,8 @@ import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
|
22
22
|
// actual workspace evidence. An optional completion_summary is forwarded as an
|
|
23
23
|
// UNTRUSTED executor claim — never evidence and never an approval bypass.
|
|
24
24
|
export async function runGoalCompletionFlow(core: GoalCore, ctx: ExtensionContext, completionSummary?: string): Promise<AgentToolResult<unknown>> {
|
|
25
|
-
|
|
25
|
+
const flushError = core.goalService.flushForAudit(ctx);
|
|
26
|
+
if (flushError) return {content: [{type: "text", text: flushError}], details: goalDetails(core.state.goal)};
|
|
26
27
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
27
28
|
|
|
28
29
|
// -- Completion --
|
|
@@ -129,7 +130,7 @@ function commitGoalCompletion(core: GoalCore, ctx: ExtensionContext, opts: {
|
|
|
129
130
|
// records remain readable and honored for compatibility; no model tool or
|
|
130
131
|
// task dialog creates new per-goal bypass state).
|
|
131
132
|
if (auditTarget.skipAuditor) {
|
|
132
|
-
|
|
133
|
+
core.auditMessages.enqueue(ctx, {
|
|
133
134
|
customType: GOAL_AUDIT_ENTRY,
|
|
134
135
|
content: `Goal completed — per-goal auditor disabled.`,
|
|
135
136
|
display: true,
|
|
@@ -159,7 +160,7 @@ if (auditTarget.skipAuditor) {
|
|
|
159
160
|
// the auditor, records audit_skipped, and proceeds through the normal
|
|
160
161
|
// deferred-completion path. No model-side bypass flag is required.
|
|
161
162
|
if (settings.disabled === true) {
|
|
162
|
-
|
|
163
|
+
core.auditMessages.enqueue(ctx, {
|
|
163
164
|
customType: GOAL_AUDIT_ENTRY,
|
|
164
165
|
content: `Goal completed — auditor disabled in settings.`,
|
|
165
166
|
display: true,
|
|
@@ -186,7 +187,7 @@ if (settings.disabled === true) {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
// Auditor is enabled — run the normal audit flow
|
|
189
|
-
|
|
190
|
+
core.auditMessages.enqueue(ctx, {
|
|
190
191
|
customType: GOAL_AUDIT_ENTRY,
|
|
191
192
|
content: [
|
|
192
193
|
"Auditor: I am starting the independent completion audit.",
|
|
@@ -195,7 +196,7 @@ if (settings.disabled === true) {
|
|
|
195
196
|
].filter((line): line is string => line !== undefined).join("\n"),
|
|
196
197
|
display: true,
|
|
197
198
|
details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel },
|
|
198
|
-
}
|
|
199
|
+
});
|
|
199
200
|
if (!core.isFocusedOperationCurrent(completionFocus)) {
|
|
200
201
|
return core.focusedOperationCancelledResult("Goal completion", completionFocus);
|
|
201
202
|
}
|
|
@@ -239,7 +240,7 @@ if (settings.disabled === true) {
|
|
|
239
240
|
|
|
240
241
|
// P1-6: warm start — seed the auditor with the parent-rendered ledger tail
|
|
241
242
|
// (recent lifecycle + task evidence) so it does not re-derive session facts.
|
|
242
|
-
const ledger =
|
|
243
|
+
const ledger = goalRuntimeEvents(ctx, auditTarget.id);
|
|
243
244
|
const warmTail = latestEventsForGoal(ledger, auditTarget.id, 8);
|
|
244
245
|
const warmContext = warmTail.length > 0
|
|
245
246
|
? `Recent goal events (from the shared ledger):\n${warmTail.map((e) => `- ${e.at} ${e.type}${"taskId" in e ? ` (task ${e.taskId})` : ""}${"evidence" in e && e.evidence ? ` evidence: ${e.evidence}` : ""}`).join("\n")}`
|
|
@@ -296,7 +297,7 @@ if (settings.disabled === true) {
|
|
|
296
297
|
|
|
297
298
|
if (userChoice === "complete_without_audit") {
|
|
298
299
|
// ── Mark complete without audit ────────────────────────────
|
|
299
|
-
|
|
300
|
+
core.auditMessages.enqueue(ctx, {
|
|
300
301
|
customType: GOAL_AUDIT_ENTRY,
|
|
301
302
|
content: `Goal completed — user bypassed audit via Escape.`,
|
|
302
303
|
display: true,
|
|
@@ -377,7 +378,7 @@ if (settings.disabled === true) {
|
|
|
377
378
|
"",
|
|
378
379
|
auditor.output || "Auditor produced no approval marker.",
|
|
379
380
|
].filter((line): line is string => line !== undefined).join("\n");
|
|
380
|
-
|
|
381
|
+
core.auditMessages.enqueue(ctx, {
|
|
381
382
|
customType: GOAL_AUDIT_ENTRY,
|
|
382
383
|
content: rejectionText,
|
|
383
384
|
display: true,
|
|
@@ -394,7 +395,7 @@ if (settings.disabled === true) {
|
|
|
394
395
|
"",
|
|
395
396
|
auditor.output || "Auditor approved completion.",
|
|
396
397
|
].filter((line): line is string => line !== undefined).join("\n");
|
|
397
|
-
|
|
398
|
+
core.auditMessages.enqueue(ctx, {
|
|
398
399
|
customType: GOAL_AUDIT_ENTRY,
|
|
399
400
|
content: approvalText,
|
|
400
401
|
display: true,
|