pi-goal-x 0.31.0 → 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 +6 -0
- package/extensions/goal-completion.ts +8 -9
- package/extensions/goal-drafting.ts +16 -8
- package/extensions/goal-events.ts +7 -2
- package/extensions/goal-questionnaire.ts +74 -3
- package/extensions/goal-session-safety.ts +48 -0
- package/extensions/goal-state.ts +3 -0
- package/extensions/goal.ts +9 -0
- package/package.json +1 -1
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
|
|
@@ -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,
|
|
@@ -22,7 +22,6 @@ import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
|
22
22
|
// actual workspace evidence. An optional completion_summary is forwarded as an
|
|
23
23
|
// UNTRUSTED executor claim — never evidence and never an approval bypass.
|
|
24
24
|
export async function runGoalCompletionFlow(core: GoalCore, ctx: ExtensionContext, completionSummary?: string): Promise<AgentToolResult<unknown>> {
|
|
25
|
-
const { pi } = core;
|
|
26
25
|
const flushError = core.goalService.flushForAudit(ctx);
|
|
27
26
|
if (flushError) return {content: [{type: "text", text: flushError}], details: goalDetails(core.state.goal)};
|
|
28
27
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
@@ -131,7 +130,7 @@ function commitGoalCompletion(core: GoalCore, ctx: ExtensionContext, opts: {
|
|
|
131
130
|
// records remain readable and honored for compatibility; no model tool or
|
|
132
131
|
// task dialog creates new per-goal bypass state).
|
|
133
132
|
if (auditTarget.skipAuditor) {
|
|
134
|
-
|
|
133
|
+
core.auditMessages.enqueue(ctx, {
|
|
135
134
|
customType: GOAL_AUDIT_ENTRY,
|
|
136
135
|
content: `Goal completed — per-goal auditor disabled.`,
|
|
137
136
|
display: true,
|
|
@@ -161,7 +160,7 @@ if (auditTarget.skipAuditor) {
|
|
|
161
160
|
// the auditor, records audit_skipped, and proceeds through the normal
|
|
162
161
|
// deferred-completion path. No model-side bypass flag is required.
|
|
163
162
|
if (settings.disabled === true) {
|
|
164
|
-
|
|
163
|
+
core.auditMessages.enqueue(ctx, {
|
|
165
164
|
customType: GOAL_AUDIT_ENTRY,
|
|
166
165
|
content: `Goal completed — auditor disabled in settings.`,
|
|
167
166
|
display: true,
|
|
@@ -188,7 +187,7 @@ if (settings.disabled === true) {
|
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
// Auditor is enabled — run the normal audit flow
|
|
191
|
-
|
|
190
|
+
core.auditMessages.enqueue(ctx, {
|
|
192
191
|
customType: GOAL_AUDIT_ENTRY,
|
|
193
192
|
content: [
|
|
194
193
|
"Auditor: I am starting the independent completion audit.",
|
|
@@ -197,7 +196,7 @@ if (settings.disabled === true) {
|
|
|
197
196
|
].filter((line): line is string => line !== undefined).join("\n"),
|
|
198
197
|
display: true,
|
|
199
198
|
details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel },
|
|
200
|
-
}
|
|
199
|
+
});
|
|
201
200
|
if (!core.isFocusedOperationCurrent(completionFocus)) {
|
|
202
201
|
return core.focusedOperationCancelledResult("Goal completion", completionFocus);
|
|
203
202
|
}
|
|
@@ -298,7 +297,7 @@ if (settings.disabled === true) {
|
|
|
298
297
|
|
|
299
298
|
if (userChoice === "complete_without_audit") {
|
|
300
299
|
// ── Mark complete without audit ────────────────────────────
|
|
301
|
-
|
|
300
|
+
core.auditMessages.enqueue(ctx, {
|
|
302
301
|
customType: GOAL_AUDIT_ENTRY,
|
|
303
302
|
content: `Goal completed — user bypassed audit via Escape.`,
|
|
304
303
|
display: true,
|
|
@@ -379,7 +378,7 @@ if (settings.disabled === true) {
|
|
|
379
378
|
"",
|
|
380
379
|
auditor.output || "Auditor produced no approval marker.",
|
|
381
380
|
].filter((line): line is string => line !== undefined).join("\n");
|
|
382
|
-
|
|
381
|
+
core.auditMessages.enqueue(ctx, {
|
|
383
382
|
customType: GOAL_AUDIT_ENTRY,
|
|
384
383
|
content: rejectionText,
|
|
385
384
|
display: true,
|
|
@@ -396,7 +395,7 @@ if (settings.disabled === true) {
|
|
|
396
395
|
"",
|
|
397
396
|
auditor.output || "Auditor approved completion.",
|
|
398
397
|
].filter((line): line is string => line !== undefined).join("\n");
|
|
399
|
-
|
|
398
|
+
core.auditMessages.enqueue(ctx, {
|
|
400
399
|
customType: GOAL_AUDIT_ENTRY,
|
|
401
400
|
content: approvalText,
|
|
402
401
|
display: true,
|
|
@@ -8,7 +8,7 @@ import { deriveTasksFromObjective } from "./goal-task-derive.ts";
|
|
|
8
8
|
import { goalDetails, renderGoalResult } from "./goal-format.ts";
|
|
9
9
|
import { buildGoalCreatedReport } from "./goal-policy.ts";
|
|
10
10
|
import { loadGoalSettings } from "./goal-settings.ts";
|
|
11
|
-
import { formatQuestionnaireAnswers, runGoalQuestionnaire, shouldAutoConfirmProposal, showProposalDialog, type GoalQuestionnaireQuestion, type ProposalDecision } from "./goal-questionnaire.ts";
|
|
11
|
+
import { DIALOG_UNAVAILABLE_HINT, proposalDialogFailureMessage, formatQuestionnaireAnswers, runGoalQuestionnaire, shouldAutoConfirmProposal, showProposalDialog, type GoalQuestionnaireQuestion, type ProposalDecision } from "./goal-questionnaire.ts";
|
|
12
12
|
import { currentTaskIdIsPending, nowIso, type GoalRecord, type GoalTaskList } from "./goal-record.ts";
|
|
13
13
|
import type { GoalCore } from "./goal-state.ts";
|
|
14
14
|
import { convertFlatTasks, countTasks, mergeTasksWithExisting, type FlatTaskInput } from "./goal-task-tools.ts";
|
|
@@ -250,7 +250,10 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
250
250
|
core.enterGoalModal();
|
|
251
251
|
try {
|
|
252
252
|
const result = await runGoalQuestionnaire(ctx, [{ id: "question", question: params.question, options: params.options ?? [], recommended: params.recommended, allowCustom: params.allow_custom }]);
|
|
253
|
+
if (result.unavailable === true) return { content: [{ type: "text", text: `${DIALOG_UNAVAILABLE_HINT} Ask the question in chat instead.` }], details: goalDetails(core.state.goal) };
|
|
253
254
|
return { content: [{ type: "text", text: result.cancelled ? "The user cancelled the question. Continue drafting conversationally." : formatQuestionnaireAnswers(result) }], details: goalDetails(core.state.goal) };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return { content: [{ type: "text", text: `Goal drafting dialog failed: ${error instanceof Error ? error.message : String(error)}. Drafting remains active; ask in chat instead of retrying the dialog.` }], details: goalDetails(core.state.goal) };
|
|
254
257
|
} finally {
|
|
255
258
|
core.exitGoalModal();
|
|
256
259
|
}
|
|
@@ -284,7 +287,10 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
284
287
|
const active = activeDraft(core);
|
|
285
288
|
if (active) active.questionnaireEcho = formatQuestionnaireAnswers(result); // E5
|
|
286
289
|
}
|
|
290
|
+
if (result.unavailable === true) return { content: [{ type: "text", text: `${DIALOG_UNAVAILABLE_HINT} Ask the questions in chat instead.` }], details: goalDetails(core.state.goal) };
|
|
287
291
|
return { content: [{ type: "text", text: result.cancelled ? "The user cancelled the questionnaire. Continue drafting conversationally." : formatQuestionnaireAnswers(result) }], details: goalDetails(core.state.goal) };
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return { content: [{ type: "text", text: `Goal drafting dialog failed: ${error instanceof Error ? error.message : String(error)}. Drafting remains active; ask in chat instead of retrying the dialog.` }], details: goalDetails(core.state.goal) };
|
|
288
294
|
} finally {
|
|
289
295
|
core.exitGoalModal();
|
|
290
296
|
}
|
|
@@ -326,16 +332,15 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
326
332
|
const target = draft.mode === "tweak" ? core.state.goal : undefined;
|
|
327
333
|
if (draft.mode === "tweak" && (!target || target.id !== draft.targetGoalId)) return { content: [{ type: "text", text: "The goal changed while drafting; review it and start /goal-tweak again." }], details: goalDetails(core.state.goal) };
|
|
328
334
|
if (draft.mode === "sisyphus" && !sisyphusObjectiveSufficient(objective)) return { content: [{ type: "text", text: "A Sisyphus goal needs ordered steps with explicit per-step done criteria. Refine the objective with numbered steps (1) ..., 2) ...) or Step N: blocks before proposing again." }], details: goalDetails(core.state.goal) };
|
|
329
|
-
|
|
330
|
-
? "\n\nAuditor for this goal: enabled (independent approval required before completion)."
|
|
331
|
-
: "\n\nAuditor for this goal: disabled (completion skips the audit).";
|
|
332
|
-
let confirmation: { decision: ProposalDecision; auditorEnabled: boolean };
|
|
335
|
+
let confirmation: { decision: ProposalDecision; auditorEnabled: boolean; unavailable: boolean };
|
|
333
336
|
if (shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM })) {
|
|
334
|
-
confirmation = { decision: "confirm" as const, auditorEnabled: draft.auditorEnabled };
|
|
337
|
+
confirmation = { decision: "confirm" as const, auditorEnabled: draft.auditorEnabled, unavailable: false };
|
|
335
338
|
} else {
|
|
336
339
|
core.enterGoalModal();
|
|
337
340
|
try {
|
|
338
|
-
confirmation = await showProposalDialog(ctx, proposalText(draft, objective, params.auto_continue !== false, taskResult.value, target ?? undefined)
|
|
341
|
+
confirmation = await showProposalDialog(ctx, proposalText(draft, objective, params.auto_continue !== false, taskResult.value, target ?? undefined), draft.mode === "sisyphus" ? "sisyphus" : "goal", draft.auditorEnabled);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return { content: [{ type: "text", text: `${proposalDialogFailureMessage(error)} Do not retry the dialog until the host issue is resolved.` }], details: goalDetails(core.state.goal) };
|
|
339
344
|
} finally {
|
|
340
345
|
core.exitGoalModal();
|
|
341
346
|
}
|
|
@@ -350,12 +355,15 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
350
355
|
taskList: taskResult.value,
|
|
351
356
|
verificationContract: extracted.verificationContract,
|
|
352
357
|
autoContinue: params.auto_continue !== false,
|
|
353
|
-
auditorEnabled:
|
|
358
|
+
auditorEnabled: confirmation.auditorEnabled,
|
|
354
359
|
});
|
|
355
360
|
if (confirmation.decision === "cancel") {
|
|
356
361
|
clearGoalDrafting(core, ctx);
|
|
357
362
|
return { content: [{ type: "text", text: `${summary}\n\nDraft cancelled; no goal was created. Run /goal or /sisyphus to start a new draft.` }], details: goalDetails(core.state.goal) };
|
|
358
363
|
}
|
|
364
|
+
if (confirmation.unavailable) {
|
|
365
|
+
return { content: [{ type: "text", text: `${summary}\n\n${DIALOG_UNAVAILABLE_HINT} The goal was NOT created and drafting remains active; do not retry the dialog until the host supports it or the user explicitly restarts with PI_GOAL_AUTO_CONFIRM=1.` }], details: goalDetails(core.state.goal) };
|
|
366
|
+
}
|
|
359
367
|
if (confirmation.decision !== "confirm") {
|
|
360
368
|
// Continue refining: preserve the user's auditor choice for the
|
|
361
369
|
// next proposal (memory and the durable session entry).
|
|
@@ -31,6 +31,7 @@ import { consumeOracleFollowupMarker, hasPendingOracleAdviceForFocusedGoal } fro
|
|
|
31
31
|
import { hasActiveDraft, rehydrateDraft } from "./goal-drafting.ts";
|
|
32
32
|
import { syncTerminalInputPause } from "./goal-widget.ts";
|
|
33
33
|
import type { GoalCore } from "./goal-state.ts";
|
|
34
|
+
import { filterGoalSessionContext } from "./goal-session-safety.ts";
|
|
34
35
|
import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -87,8 +88,8 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
87
88
|
let networkErrorRecoveryAfterSettleFor: string | null = null;
|
|
88
89
|
|
|
89
90
|
pi.on("context", async (event) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
91
|
+
const filtered = filterGoalSessionContext(event.messages);
|
|
92
|
+
const messages = compactGoalCheckpointContext(filtered ?? event.messages, core.state.goal) ?? filtered;
|
|
92
93
|
return messages === null ? undefined : { messages: messages as typeof event.messages };
|
|
93
94
|
});
|
|
94
95
|
|
|
@@ -270,6 +271,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
270
271
|
});
|
|
271
272
|
|
|
272
273
|
pi.on("session_start", async (event, ctx) => {
|
|
274
|
+
core.auditMessages.clear();
|
|
273
275
|
// NAF: the zero-op read caches are session-scoped — a new session always
|
|
274
276
|
// re-reads settings/pool/ledger fresh from disk (cross-process and
|
|
275
277
|
// hand-edited changes are picked up at the session boundary).
|
|
@@ -327,6 +329,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
327
329
|
});
|
|
328
330
|
|
|
329
331
|
pi.on("session_tree", async (_event, ctx) => {
|
|
332
|
+
core.auditMessages.clear();
|
|
330
333
|
core.goalService.flushTurn(ctx); // P1-3: persist any buffered transaction before reload
|
|
331
334
|
await core.loadState(ctx);
|
|
332
335
|
rehydrateDraft(core, ctx);
|
|
@@ -530,6 +533,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
530
533
|
});
|
|
531
534
|
|
|
532
535
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
536
|
+
core.auditMessages.flush(ctx, pi);
|
|
533
537
|
const goalId = continuationAfterSettleFor;
|
|
534
538
|
continuationAfterSettleFor = null;
|
|
535
539
|
const networkErrorGoalId = networkErrorRecoveryAfterSettleFor;
|
|
@@ -560,6 +564,7 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
560
564
|
});
|
|
561
565
|
|
|
562
566
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
567
|
+
core.auditMessages.clear();
|
|
563
568
|
continuationAfterSettleFor = null;
|
|
564
569
|
networkErrorRecoveryAfterSettleFor = null;
|
|
565
570
|
core.accountProgress(ctx);
|
|
@@ -26,6 +26,13 @@ export interface GoalQuestionnaireResult {
|
|
|
26
26
|
answers: GoalQuestionnaireAnswer[];
|
|
27
27
|
cancelled: boolean;
|
|
28
28
|
auditorEnabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The host could not present the dialog at all, so `cancelled` reflects an
|
|
31
|
+
* absent UI rather than a decision the user made. Hosts that expose a
|
|
32
|
+
* non-terminal UI keep `ctx.hasUI` true while `ctx.ui.custom` stays the
|
|
33
|
+
* SDK's headless default, which resolves to `undefined`.
|
|
34
|
+
*/
|
|
35
|
+
unavailable?: boolean;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export type ProposalDecision = "confirm" | "continue" | "cancel";
|
|
@@ -298,6 +305,60 @@ export function isHeadlessQuestionSufficientForDraft(args: { topic: string; ques
|
|
|
298
305
|
return !vagueTopic;
|
|
299
306
|
}
|
|
300
307
|
|
|
308
|
+
const CUSTOM_ANSWER_LABEL = "Write your own answer...";
|
|
309
|
+
|
|
310
|
+
/** Sequential native dialogs for hosts that cannot render terminal components. */
|
|
311
|
+
async function runQuestionnaireWithBasicDialogs(
|
|
312
|
+
ctx: ExtensionContext,
|
|
313
|
+
questions: GoalQuestionnaireQuestion[],
|
|
314
|
+
auditorToggleInit?: { defaultEnabled: boolean },
|
|
315
|
+
): Promise<GoalQuestionnaireResult> {
|
|
316
|
+
const unavailable = (): GoalQuestionnaireResult => ({ questions: [], answers: [], cancelled: true, unavailable: true, ...(auditorToggleInit ? { auditorEnabled: auditorToggleInit.defaultEnabled } : {}) });
|
|
317
|
+
// Require only the primitives this questionnaire can actually use.
|
|
318
|
+
if ((auditorToggleInit || questions.some(q => q.options.length > 0)) && typeof ctx.ui.select !== "function") return unavailable();
|
|
319
|
+
if (questions.some(q => q.options.length === 0 || q.allowCustom !== false) && typeof ctx.ui.input !== "function") return unavailable();
|
|
320
|
+
let auditorEnabled = auditorToggleInit?.defaultEnabled;
|
|
321
|
+
const cancelled = (): GoalQuestionnaireResult => ({ questions, answers: [], cancelled: true, auditorEnabled });
|
|
322
|
+
if (auditorToggleInit) {
|
|
323
|
+
const enabled = "Enabled — require independent approval";
|
|
324
|
+
const disabled = "Disabled — skip the completion audit";
|
|
325
|
+
const choices = auditorEnabled ? [enabled, disabled] : [disabled, enabled];
|
|
326
|
+
const picked = await ctx.ui.select(`Completion auditor (currently ${auditorEnabled ? "enabled" : "disabled"})`, choices);
|
|
327
|
+
if (picked === undefined) return cancelled();
|
|
328
|
+
if (!choices.includes(picked)) throw new Error("The host returned an unknown auditor choice");
|
|
329
|
+
auditorEnabled = picked === enabled;
|
|
330
|
+
}
|
|
331
|
+
const answers: GoalQuestionnaireAnswer[] = [];
|
|
332
|
+
for (const question of questions) {
|
|
333
|
+
let prompt = question.context ? `${question.question}\n\n${question.context}` : question.question;
|
|
334
|
+
if (auditorToggleInit) prompt += `\n\nAuditor for this goal: ${auditorEnabled ? "enabled (independent approval required)" : "disabled (completion skips the audit)"}.`;
|
|
335
|
+
let answer: string | undefined;
|
|
336
|
+
let wasCustom = question.options.length === 0;
|
|
337
|
+
if (!wasCustom) {
|
|
338
|
+
// Numbered labels keep duplicate/reserved option text unambiguous.
|
|
339
|
+
const labels = question.options.map((option, i) => `${i + 1}. ${option}${question.recommended === i ? " (Recommended)" : ""}`);
|
|
340
|
+
const customLabel = `${labels.length + 1}. ${CUSTOM_ANSWER_LABEL}`;
|
|
341
|
+
const options = question.allowCustom === false ? labels : [...labels, customLabel];
|
|
342
|
+
const picked = await ctx.ui.select(prompt, options);
|
|
343
|
+
if (picked === undefined) return cancelled();
|
|
344
|
+
if (!options.includes(picked)) throw new Error("The host returned an unknown questionnaire choice");
|
|
345
|
+
wasCustom = picked === customLabel && question.allowCustom !== false;
|
|
346
|
+
if (!wasCustom) answer = question.options[labels.indexOf(picked)];
|
|
347
|
+
}
|
|
348
|
+
if (wasCustom) {
|
|
349
|
+
// Match the terminal editor: whitespace alone is not a submitted answer.
|
|
350
|
+
do {
|
|
351
|
+
answer = (await ctx.ui.input(prompt, "Write your answer"))?.trim();
|
|
352
|
+
} while (answer === "");
|
|
353
|
+
}
|
|
354
|
+
if (answer === undefined) return cancelled();
|
|
355
|
+
answers.push({ id: question.id, question: question.question, answer, wasCustom });
|
|
356
|
+
}
|
|
357
|
+
return { questions, answers, cancelled: false, auditorEnabled };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export const DIALOG_UNAVAILABLE_HINT = "This host cannot display the required goal-drafting dialogs. Use a host with select/input support or the pi TUI, or explicitly restart with PI_GOAL_AUTO_CONFIRM=1 to confirm proposals without a dialog.";
|
|
361
|
+
|
|
301
362
|
export function proposalDialogFailureMessage(error: unknown): string {
|
|
302
363
|
const detail = error instanceof Error ? error.message : String(error);
|
|
303
364
|
return `Goal draft confirmation failed: ${detail}. The goal was NOT created; drafting remains active.`;
|
|
@@ -314,10 +375,18 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
314
375
|
}
|
|
315
376
|
|
|
316
377
|
const questions = normalizeQuestionnaireQuestions(rawQuestions);
|
|
378
|
+
if (ctx.mode === "rpc" || typeof ctx.ui.custom !== "function") {
|
|
379
|
+
return runQuestionnaireWithBasicDialogs(ctx, questions, auditorToggleInit);
|
|
380
|
+
}
|
|
317
381
|
const isMulti = questions.length > 1;
|
|
318
382
|
const totalTabs = questions.length + 1;
|
|
319
383
|
|
|
320
|
-
|
|
384
|
+
const result = await ctx.ui.custom<GoalQuestionnaireResult | undefined>((tui, theme, _kb, done) => {
|
|
385
|
+
// Some web hosts invoke the factory with a render callback in place of a TUI.
|
|
386
|
+
if (!tui || typeof tui.getShowHardwareCursor !== "function" || typeof tui.setShowHardwareCursor !== "function" || typeof tui.requestRender !== "function") {
|
|
387
|
+
done(undefined);
|
|
388
|
+
return { render: () => [], invalidate: () => {} };
|
|
389
|
+
}
|
|
321
390
|
// Suppress hardware cursor during dialog to reduce TUI auto-scroll
|
|
322
391
|
// (the TUI render loop runs at ~60fps and writes ANSI cursor positioning
|
|
323
392
|
// sequences every cycle, which can cause terminal viewport snapping).
|
|
@@ -947,6 +1016,8 @@ function advanceAfterAnswer() {
|
|
|
947
1016
|
},
|
|
948
1017
|
};
|
|
949
1018
|
});
|
|
1019
|
+
if (result !== undefined) return result;
|
|
1020
|
+
return runQuestionnaireWithBasicDialogs(ctx, questions, auditorToggleInit);
|
|
950
1021
|
}
|
|
951
1022
|
|
|
952
1023
|
/**
|
|
@@ -958,7 +1029,7 @@ export async function showProposalDialog(
|
|
|
958
1029
|
confirmationText: string,
|
|
959
1030
|
focus: GoalDraftingFocus,
|
|
960
1031
|
defaultAuditorEnabled?: boolean,
|
|
961
|
-
): Promise<{ decision: ProposalDecision; auditorEnabled: boolean }> {
|
|
1032
|
+
): Promise<{ decision: ProposalDecision; auditorEnabled: boolean; unavailable: boolean }> {
|
|
962
1033
|
const headerTitle = focus === "sisyphus" ? "Confirm Sisyphus Goal Draft" : "Confirm Goal Draft";
|
|
963
1034
|
const result = await runGoalQuestionnaire(ctx, [{
|
|
964
1035
|
id: "confirm",
|
|
@@ -972,5 +1043,5 @@ export async function showProposalDialog(
|
|
|
972
1043
|
cancelled: result.cancelled,
|
|
973
1044
|
answer: result.answers[0]?.answer,
|
|
974
1045
|
});
|
|
975
|
-
return { decision, auditorEnabled: result.auditorEnabled ?? true };
|
|
1046
|
+
return { decision, auditorEnabled: result.auditorEnabled ?? true, unavailable: result.unavailable === true };
|
|
976
1047
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { GOAL_AUDIT_ENTRY, goalEventMessageId } from "./goal-format.ts";
|
|
3
|
+
|
|
4
|
+
/** pi-subagents marks fresh, forked, resumed and nested worker processes. */
|
|
5
|
+
export function isDelegatedGoalSession(env: Record<string, string | undefined> = process.env): boolean {
|
|
6
|
+
const depth = Number(env.PI_SUBAGENT_DEPTH);
|
|
7
|
+
return env.PI_SUBAGENT_CHILD === "1" || (Number.isFinite(depth) && depth > 0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Display-only audits must never become user messages between a tool and its result. */
|
|
11
|
+
export function filterGoalSessionContext<T>(messages: readonly T[], child = false): T[] | null {
|
|
12
|
+
let filtered: T[] | null = null;
|
|
13
|
+
for (let i = 0; i < messages.length; i++) {
|
|
14
|
+
const message = messages[i]!;
|
|
15
|
+
const entry = message as { role?: string; customType?: string; details?: unknown; content?: unknown } | null;
|
|
16
|
+
const remove = entry && typeof entry === "object" && entry.role === "custom"
|
|
17
|
+
&& (entry.customType === GOAL_AUDIT_ENTRY || (child && goalEventMessageId(entry) !== null));
|
|
18
|
+
if (remove) filtered ??= messages.slice(0, i);
|
|
19
|
+
else filtered?.push(message);
|
|
20
|
+
}
|
|
21
|
+
return filtered;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type TranscriptMessage = Parameters<ExtensionAPI["sendMessage"]>[0];
|
|
25
|
+
type SessionKey = string | ExtensionContext["sessionManager"];
|
|
26
|
+
const sessionKey = (ctx: ExtensionContext): SessionKey => ctx.sessionManager.getSessionId?.() ?? ctx.sessionManager;
|
|
27
|
+
|
|
28
|
+
/** Transcript output waits for the entire agent run, including tool results, to settle. */
|
|
29
|
+
export class GoalAuditMessages {
|
|
30
|
+
private pending: { session: SessionKey; message: TranscriptMessage }[] = [];
|
|
31
|
+
|
|
32
|
+
enqueue(ctx: ExtensionContext, message: TranscriptMessage): void {
|
|
33
|
+
this.pending.push({ session: sessionKey(ctx), message });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear(): void {
|
|
37
|
+
this.pending = [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
flush(ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
41
|
+
const session = sessionKey(ctx);
|
|
42
|
+
this.pending = this.pending.filter(item => item.session === session);
|
|
43
|
+
if (!ctx.isIdle()) return;
|
|
44
|
+
const pending = this.pending;
|
|
45
|
+
this.pending = [];
|
|
46
|
+
for (const item of pending) pi.sendMessage(item.message, { triggerTurn: false });
|
|
47
|
+
}
|
|
48
|
+
}
|
package/extensions/goal-state.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { GoalService } from "./goal-service.ts";
|
|
|
32
32
|
import { goalActivityEvents } from "./goal-ledger.ts";
|
|
33
33
|
import { GoalAccounting } from "./goal-accounting.ts";
|
|
34
34
|
import { GoalRuntime } from "./goal-runtime.ts";
|
|
35
|
+
import { GoalAuditMessages } from "./goal-session-safety.ts";
|
|
35
36
|
import {
|
|
36
37
|
focusedGoalFromPool,
|
|
37
38
|
openGoalsFromPool,
|
|
@@ -78,6 +79,7 @@ export interface GoalCore {
|
|
|
78
79
|
goalWidgetComponentRef: { current: GoalWidgetComponent | null };
|
|
79
80
|
goalService: GoalService;
|
|
80
81
|
runtime: GoalRuntime;
|
|
82
|
+
auditMessages: GoalAuditMessages;
|
|
81
83
|
accounting: GoalAccounting;
|
|
82
84
|
|
|
83
85
|
assignFocusedGoalId(goalId: string | null): void;
|
|
@@ -1000,6 +1002,7 @@ export function createGoalCore(
|
|
|
1000
1002
|
goalWidgetComponentRef,
|
|
1001
1003
|
goalService,
|
|
1002
1004
|
runtime,
|
|
1005
|
+
auditMessages: new GoalAuditMessages(),
|
|
1003
1006
|
accounting,
|
|
1004
1007
|
assignFocusedGoalId,
|
|
1005
1008
|
focusedOperationToken,
|
package/extensions/goal.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { GoalEventDetails } from "./goal-record.ts";
|
|
13
13
|
import { createGoalCore } from "./goal-state.ts";
|
|
14
14
|
import { registerGoalTools } from "./goal-tools.ts";
|
|
15
|
+
import { filterGoalSessionContext, isDelegatedGoalSession } from "./goal-session-safety.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* pi-goal thin installer. All state lives in the GoalCore (goal-state.ts);
|
|
@@ -23,6 +24,14 @@ export default function goalExtension(
|
|
|
23
24
|
pi: ExtensionAPI,
|
|
24
25
|
dependencies: { runCompletionAuditor?: typeof runGoalCompletionAuditor } = {},
|
|
25
26
|
): void {
|
|
27
|
+
if (isDelegatedGoalSession()) {
|
|
28
|
+
// Inherit conversation without inheriting ownership of the parent's goal.
|
|
29
|
+
pi.on("context", async event => {
|
|
30
|
+
const messages = filterGoalSessionContext(event.messages, true);
|
|
31
|
+
return messages === null ? undefined : { messages };
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
26
35
|
pi.registerMessageRenderer<GoalEventDetails>(GOAL_EVENT_ENTRY, renderGoalEvent);
|
|
27
36
|
pi.registerMessageRenderer<GoalAuditEventDetails>(GOAL_AUDIT_ENTRY, renderGoalAuditEvent);
|
|
28
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.1",
|
|
4
4
|
"description": "Goal mode extension for pi: durable long-running objectives, five model tools, structured tasks, independent completion audit, Sisyphus mode, auto-continue, and a status overlay.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "pi-goal-x contributors",
|