pi-goal-x 0.28.0 → 0.30.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 +17 -0
- package/extensions/goal-auditor.ts +72 -60
- package/extensions/goal-commands.ts +73 -27
- package/extensions/goal-compaction.ts +49 -1
- package/extensions/goal-completion.ts +1 -1
- package/extensions/goal-core-tools.ts +290 -70
- package/extensions/goal-draft.ts +1 -1
- package/extensions/goal-events.ts +70 -12
- package/extensions/goal-format.ts +14 -0
- package/extensions/goal-ledger.ts +39 -1
- package/extensions/goal-oracle.ts +332 -0
- package/extensions/goal-runtime.ts +40 -1
- package/extensions/goal-settings.ts +143 -1
- package/extensions/goal-state.ts +3 -3
- package/extensions/network-error-backoff.ts +25 -0
- package/extensions/prompts/goal-prompts.ts +36 -9
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -433,6 +433,23 @@ Configure the task shortcuts in the same file when the terminal captures the def
|
|
|
433
433
|
|
|
434
434
|
The default task bindings are `ctrl+shift+t`, `ctrl+shift+up`, and `ctrl+shift+down`. Use pi key names such as `ctrl+shift+up`.
|
|
435
435
|
|
|
436
|
+
### Blocker Oracle (opt-in)
|
|
437
|
+
|
|
438
|
+
When an active goal reports blocked, a stronger read-only model can be consulted once per distinct blocker before the goal is allowed to stop. Off by default; configure under `/goal-settings → Blocker Oracle`:
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"oracle": {
|
|
443
|
+
"enabled": true,
|
|
444
|
+
"provider": "anthropic",
|
|
445
|
+
"model": "<a stronger model>",
|
|
446
|
+
"maxFailedAttemptsPerBlocker": 2
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Both `provider` and `model` must be set explicitly — the executor model is never used as a silent fallback. The Oracle session can only read files (`read`, `grep`, `find`, `ls`) and returns structured advice. Actionable advice keeps the goal running until you attempt it; advice that needs human input lets the goal block immediately.
|
|
452
|
+
|
|
436
453
|
## License
|
|
437
454
|
|
|
438
455
|
MIT
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type { GoalRecord, GoalTask, GoalTaskList } from "./goal-record.ts";
|
|
16
16
|
import { countTaskSubtree } from "./goal-task-count.ts";
|
|
17
17
|
import { loadGoalSettings, type GoalSettings, type ThinkingLevel } from "./goal-settings.ts";
|
|
18
|
+
import { statusLabel } from "./goal-core.ts";
|
|
18
19
|
|
|
19
20
|
export interface AuditorProgress {
|
|
20
21
|
/** Current tool being executed by the auditor, if any */
|
|
@@ -108,6 +109,47 @@ function escapePromptPayload(value: string): string {
|
|
|
108
109
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/** §60: human-readable labels for the auditor's read-only tool set. */
|
|
113
|
+
export function labelForReadOnlyTool(toolName: string): string {
|
|
114
|
+
switch (toolName) {
|
|
115
|
+
case "read": return "Inspecting files...";
|
|
116
|
+
case "grep": return "Searching content...";
|
|
117
|
+
case "find": return "Locating files...";
|
|
118
|
+
case "ls": return "Listing directory...";
|
|
119
|
+
case "bash": return "Running verification commands...";
|
|
120
|
+
default: return `Inspecting (${toolName})...`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** §60: rough phase estimate by tool kind (display only). */
|
|
125
|
+
export function estimateAuditProgress(toolName: string): number {
|
|
126
|
+
switch (toolName) {
|
|
127
|
+
case "read":
|
|
128
|
+
case "ls":
|
|
129
|
+
case "find":
|
|
130
|
+
return 25;
|
|
131
|
+
case "grep":
|
|
132
|
+
return 50;
|
|
133
|
+
case "bash":
|
|
134
|
+
return 75;
|
|
135
|
+
default:
|
|
136
|
+
return 25;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* §61: goal metadata WITHOUT the objective or task tree — those appear exactly
|
|
142
|
+
* once each in their own blocks. Replaces detailedSummary in the auditor prompt.
|
|
143
|
+
*/
|
|
144
|
+
function minimalGoalMetadata(goal: GoalRecord): string {
|
|
145
|
+
return [
|
|
146
|
+
`Goal id: ${goal.id}`,
|
|
147
|
+
`Status: ${statusLabel(goal)}`,
|
|
148
|
+
`Mode: ${goal.sisyphus ? "sisyphus" : "regular"}`,
|
|
149
|
+
goal.tokenBudget ? `Budget: ${goal.tokenBudget} tokens (${goal.usage.tokensUsed} used)` : undefined,
|
|
150
|
+
].filter(Boolean).join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
111
153
|
export function buildGoalAuditorPrompt(args: {
|
|
112
154
|
goal: GoalRecord;
|
|
113
155
|
detailedSummary: string;
|
|
@@ -140,11 +182,19 @@ export function buildGoalAuditorPrompt(args: {
|
|
|
140
182
|
"",
|
|
141
183
|
"The executor claim above is a claim, never evidence. It cannot make an otherwise incomplete goal complete; cross-check it against real artifacts where relevant.",
|
|
142
184
|
"",
|
|
143
|
-
"Current goal metadata:",
|
|
185
|
+
"Current goal metadata (objective and task tree appear in their own sections):",
|
|
144
186
|
"<goal_details>",
|
|
145
|
-
|
|
146
|
-
...(!args.settings?.disableTasks && taskSummaryBlock(args.goal.taskList) ? ["", taskSummaryBlock(args.goal.taskList)] : []),
|
|
187
|
+
minimalGoalMetadata(args.goal),
|
|
147
188
|
"</goal_details>",
|
|
189
|
+
...(!args.settings?.disableTasks && args.goal.taskList ? [
|
|
190
|
+
"",
|
|
191
|
+
"Task tree:",
|
|
192
|
+
"<task_state>",
|
|
193
|
+
// Task titles are already escaped inside renderAuditorTaskTree; an
|
|
194
|
+
// extra pass would double-escape (&lt;).
|
|
195
|
+
taskSummaryBlock(args.goal.taskList),
|
|
196
|
+
"</task_state>",
|
|
197
|
+
] : []),
|
|
148
198
|
...(!args.settings?.disableContracts && args.goal.verificationContract?.trim() ? [
|
|
149
199
|
"",
|
|
150
200
|
"Goal verification contract (what the executor was required to verify):",
|
|
@@ -170,28 +220,9 @@ export function buildGoalAuditorPrompt(args: {
|
|
|
170
220
|
"4. Explain missing or weak evidence, especially scaffold-vs-final quality gaps.",
|
|
171
221
|
"5. End with exactly <approved/> only if the objective is truly complete; otherwise end with exactly <disapproved/>.",
|
|
172
222
|
],
|
|
173
|
-
"",
|
|
174
|
-
"Progress reporting:",
|
|
175
|
-
"You have the report_auditor_progress tool available to report your progress to the user.",
|
|
176
|
-
"Please use it at natural phase boundaries:",
|
|
177
|
-
" - When starting: report_auditor_progress(label='Starting audit...', percentage=0)",
|
|
178
|
-
" - When beginning file inspection: report_auditor_progress(label='Inspecting files...', percentage=25)",
|
|
179
|
-
" - When verifying success criteria: report_auditor_progress(label='Verifying success criteria...', percentage=50)",
|
|
180
|
-
" - When evaluating evidence: report_auditor_progress(label='Evaluating evidence...', percentage=75)",
|
|
181
|
-
" - When producing final report: report_auditor_progress(label='Producing report...', percentage=90)",
|
|
182
|
-
"This is purely for user visibility and does not affect the audit outcome.",
|
|
183
223
|
].join("\n");
|
|
184
224
|
}
|
|
185
225
|
|
|
186
|
-
/** Tool name for auditor progress reporting */
|
|
187
|
-
export const REPORT_AUDITOR_PROGRESS_TOOL_NAME = "report_auditor_progress";
|
|
188
|
-
|
|
189
|
-
/** Parameters for the report_auditor_progress tool */
|
|
190
|
-
export const reportAuditorProgressParams = Type.Object({
|
|
191
|
-
label: Type.String({ description: "Current step label describing what the auditor is doing (e.g. 'Inspecting files...', 'Verifying success criteria...', 'Producing report...')" }),
|
|
192
|
-
percentage: Type.Number({ description: "Completion percentage from 0 to 100", minimum: 0, maximum: 100 }),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
226
|
function makeAuditorResourceLoader(): ResourceLoader {
|
|
196
227
|
return {
|
|
197
228
|
getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
|
|
@@ -203,11 +234,6 @@ function makeAuditorResourceLoader(): ResourceLoader {
|
|
|
203
234
|
"You are a read-only completion auditor running in an isolated pi agent session.",
|
|
204
235
|
"Inspect the repository and decide whether the claimed goal completion is genuinely satisfied.",
|
|
205
236
|
"Never modify files. Never approve unless the actual user objective is complete.",
|
|
206
|
-
"",
|
|
207
|
-
"You have the report_auditor_progress tool available. Use it to report your audit progress",
|
|
208
|
-
"to the user at natural phase boundaries (starting, inspecting files, verifying criteria,",
|
|
209
|
-
"producing report). This helps the user understand what the auditor is doing and how far",
|
|
210
|
-
"along it is.",
|
|
211
237
|
].join("\n"),
|
|
212
238
|
getSystemPromptSource: () => undefined,
|
|
213
239
|
getAppendSystemPrompt: () => [],
|
|
@@ -312,37 +338,6 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
312
338
|
args.onProgress?.({ ...progress });
|
|
313
339
|
}
|
|
314
340
|
|
|
315
|
-
// Build the report_auditor_progress tool, capturing the progress state
|
|
316
|
-
const reportProgressTool = defineTool({
|
|
317
|
-
name: REPORT_AUDITOR_PROGRESS_TOOL_NAME,
|
|
318
|
-
label: "Report Auditor Progress",
|
|
319
|
-
description: "Report current progress of the audit to the user. Call this at natural phase boundaries (starting, inspecting files, verifying criteria, producing report) to keep the user informed.",
|
|
320
|
-
promptSnippet: "Report current audit progress (step label and completion percentage) to the user.",
|
|
321
|
-
promptGuidelines: [
|
|
322
|
-
"Use report_auditor_progress at natural phase boundaries during the audit:",
|
|
323
|
-
" - When starting the audit: label='Starting audit...' percentage=0",
|
|
324
|
-
" - When beginning file inspection: label='Inspecting files...' percentage=25",
|
|
325
|
-
" - When verifying success criteria: label='Verifying success criteria...' percentage=50",
|
|
326
|
-
" - When evaluating evidence: label='Evaluating evidence...' percentage=75",
|
|
327
|
-
" - When producing final report: label='Producing report...' percentage=90",
|
|
328
|
-
"This is purely for user visibility — it does not affect the audit outcome.",
|
|
329
|
-
"Do not call this tool more than once every few seconds to avoid flooding.",
|
|
330
|
-
],
|
|
331
|
-
parameters: reportAuditorProgressParams,
|
|
332
|
-
executionMode: "sequential",
|
|
333
|
-
async execute(_toolCallId, params) {
|
|
334
|
-
const { label, percentage } = params as Static<typeof reportAuditorProgressParams>;
|
|
335
|
-
progress.label = label;
|
|
336
|
-
progress.percentage = percentage;
|
|
337
|
-
progress.phase = "running";
|
|
338
|
-
emitProgress();
|
|
339
|
-
return {
|
|
340
|
-
content: [{ type: "text", text: `Progress reported: ${label} (${percentage}%)` }],
|
|
341
|
-
details: {},
|
|
342
|
-
};
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
|
|
346
341
|
const projectResources = args.settings?.auditorProjectResources === true;
|
|
347
342
|
const { session } = await createSession({
|
|
348
343
|
cwd: args.ctx.cwd,
|
|
@@ -357,10 +352,18 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
357
352
|
: { resourceLoader: makeAuditorResourceLoader() }),
|
|
358
353
|
sessionManager: SessionManager.inMemory(args.ctx.cwd),
|
|
359
354
|
settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
|
|
360
|
-
tools: ["read", "grep", "find", "ls", "bash"
|
|
361
|
-
customTools: [reportProgressTool],
|
|
355
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
362
356
|
} as Parameters<typeof createAgentSession>[0]);
|
|
363
357
|
const unsubscribe = session.subscribe((event) => {
|
|
358
|
+
if (event.type === "agent_start") {
|
|
359
|
+
// PR E §60: progress is derived from session events — no model-facing
|
|
360
|
+
// report_auditor_progress tool exists anymore.
|
|
361
|
+
progress.label = "Starting audit...";
|
|
362
|
+
progress.percentage = 0;
|
|
363
|
+
progress.phase = "running";
|
|
364
|
+
emitProgress();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
364
367
|
if (event.type === "tool_execution_start") {
|
|
365
368
|
progress.currentTool = event.toolName;
|
|
366
369
|
progress.currentToolArgs = typeof event.args === "object" && event.args !== null
|
|
@@ -368,6 +371,9 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
368
371
|
: String(event.args ?? "").slice(0, 120);
|
|
369
372
|
progress.currentToolStartedAt = Date.now();
|
|
370
373
|
progress.phase = "tool_executing";
|
|
374
|
+
// §60 derive the human label + estimate from the read-only tool.
|
|
375
|
+
progress.label = labelForReadOnlyTool(event.toolName);
|
|
376
|
+
progress.percentage = Math.max(progress.percentage ?? 0, estimateAuditProgress(event.toolName));
|
|
371
377
|
emitProgress();
|
|
372
378
|
return;
|
|
373
379
|
}
|
|
@@ -414,6 +420,12 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
414
420
|
for (const part of message.content ?? []) {
|
|
415
421
|
if (part.type === "text" && typeof part.text === "string") outputParts.push(part.text);
|
|
416
422
|
}
|
|
423
|
+
// Final report production: derived label for the widget.
|
|
424
|
+
if (outputParts.some((t) => t.trim())) {
|
|
425
|
+
progress.label = "Producing report...";
|
|
426
|
+
progress.percentage = Math.max(progress.percentage ?? 0, 90);
|
|
427
|
+
progress.phase = "producing_report";
|
|
428
|
+
}
|
|
417
429
|
// Show the accumulated output in progress
|
|
418
430
|
const fullText = outputParts.join("\n\n");
|
|
419
431
|
const lines = fullText.split("\n").filter((l: string) => l.trim());
|
|
@@ -380,10 +380,12 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
380
380
|
* eight persisted fields are present and operable.
|
|
381
381
|
*/
|
|
382
382
|
type SettingRow = {
|
|
383
|
-
key: keyof GoalSettings;
|
|
383
|
+
key: keyof GoalSettings | string;
|
|
384
384
|
label: string;
|
|
385
|
-
section: "Goal behavior" | "Task tracking" | "Completion auditor";
|
|
385
|
+
section: "Goal behavior" | "Task tracking" | "Completion auditor" | "Blocker Oracle";
|
|
386
386
|
kind: "boolean" | "modelSelector" | "thinking" | "positiveInteger";
|
|
387
|
+
/** Settings path for the mutation (defaults to [key]). */
|
|
388
|
+
path?: string[];
|
|
387
389
|
};
|
|
388
390
|
|
|
389
391
|
const SETTING_ROWS: readonly SettingRow[] = [
|
|
@@ -398,9 +400,16 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
398
400
|
{ key: "provider", label: "provider", section: "Completion auditor", kind: "modelSelector" },
|
|
399
401
|
{ key: "model", label: "model", section: "Completion auditor", kind: "modelSelector" },
|
|
400
402
|
{ key: "thinkingLevel", label: "thinking_level", section: "Completion auditor", kind: "thinking" },
|
|
403
|
+
// Issue #26: opt-in blocker Oracle. Disabled by default; provider/model
|
|
404
|
+
// must BOTH be set explicitly — the executor model is never used silently.
|
|
405
|
+
{ key: "oracleEnabled", label: "oracle enabled", section: "Blocker Oracle", kind: "boolean", path: ["oracle", "enabled"] },
|
|
406
|
+
{ key: "oracleProviderModel", label: "oracle provider/model", section: "Blocker Oracle", kind: "modelSelector", path: ["oracle"] },
|
|
407
|
+
{ key: "oracleThinkingLevel", label: "oracle thinking_level", section: "Blocker Oracle", kind: "thinking", path: ["oracle", "thinkingLevel"] },
|
|
408
|
+
{ key: "oracleProjectResources", label: "oracle project resources", section: "Blocker Oracle", kind: "boolean", path: ["oracle", "projectResources"] },
|
|
409
|
+
{ key: "oracleMaxFailedAttemptsPerBlocker", label: "max failed attempts per blocker", section: "Blocker Oracle", kind: "positiveInteger", path: ["oracle", "maxFailedAttemptsPerBlocker"] },
|
|
401
410
|
];
|
|
402
411
|
|
|
403
|
-
function settingsValue(config: GoalSettings, key: keyof GoalSettings): string {
|
|
412
|
+
function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
|
|
404
413
|
if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
|
|
405
414
|
return config[key] === true ? "true" : "false";
|
|
406
415
|
}
|
|
@@ -408,7 +417,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
408
417
|
if (key === "stallTimeoutMinutes") return config.stallTimeoutMinutes !== undefined ? String(config.stallTimeoutMinutes) : "0";
|
|
409
418
|
if (key === "objectiveMaxChars") return config.objectiveMaxChars !== undefined ? String(config.objectiveMaxChars) : "0";
|
|
410
419
|
if (key === "keybindings") return config.keybindings ? `${config.keybindings.dashboard.toggleExpand}, ${config.keybindings.dashboard.scrollUp}, ${config.keybindings.dashboard.scrollDown}` : "(default)";
|
|
411
|
-
const value = config[key];
|
|
420
|
+
const value = (config as Record<string, unknown>)[key];
|
|
412
421
|
return typeof value === "string" ? value : "(default)";
|
|
413
422
|
}
|
|
414
423
|
|
|
@@ -465,24 +474,38 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
465
474
|
const options: string[] = [];
|
|
466
475
|
options.push(`─── Editing: ${scope} (${scopePath(scope)}) ───`);
|
|
467
476
|
let lastSection: string | null = null;
|
|
477
|
+
const pathOf = (row: SettingRow): string[] => row.path ?? [String(row.key)];
|
|
478
|
+
const valueAtPath = (obj: unknown, path: string[]): unknown => {
|
|
479
|
+
let cursor: unknown = obj;
|
|
480
|
+
for (const segment of path) {
|
|
481
|
+
if (!cursor || typeof cursor !== "object") return undefined;
|
|
482
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
483
|
+
}
|
|
484
|
+
return cursor;
|
|
485
|
+
};
|
|
468
486
|
for (const row of SETTING_ROWS) {
|
|
469
487
|
if (row.section !== lastSection) {
|
|
470
488
|
options.push(`─── ${row.section} ───`);
|
|
471
489
|
lastSection = row.section;
|
|
472
490
|
}
|
|
473
|
-
const envVar = envOverrideFor(row.key);
|
|
491
|
+
const envVar = typeof row.key === "string" ? envOverrideFor(row.key as keyof GoalSettings) : null;
|
|
474
492
|
if (envVar) {
|
|
475
493
|
options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} (environment; read-only)`);
|
|
476
494
|
continue;
|
|
477
495
|
}
|
|
478
|
-
const
|
|
496
|
+
const rowPath = pathOf(row);
|
|
497
|
+
const provenance = snapshot.provenance.get(rowPath.join("."));
|
|
479
498
|
let source: string;
|
|
480
499
|
if (provenance?.source === "global") source = scope === "project" ? "(inherited from global)" : "(global override)";
|
|
481
500
|
else if (provenance?.source === "project") source = scope === "project" ? "(project override)" : "(project; edit via project scope)";
|
|
482
501
|
else source = "(default)";
|
|
483
|
-
const hasLocalOverride = localLayer
|
|
502
|
+
const hasLocalOverride = valueAtPath(localLayer, rowPath) !== undefined;
|
|
484
503
|
if (hasLocalOverride && provenance?.source === scope) source = `(${scope} override)`;
|
|
485
|
-
|
|
504
|
+
// Nested rows render their leaf value directly.
|
|
505
|
+
const displayValue = row.path
|
|
506
|
+
? String(valueAtPath(snapshot.value, rowPath) ?? "(default)")
|
|
507
|
+
: settingsValue(snapshot.value, row.key);
|
|
508
|
+
options.push(` ${row.label}: ${displayValue} ${source}`);
|
|
486
509
|
}
|
|
487
510
|
options.push("Done");
|
|
488
511
|
const selected = await ctx.ui.select("Goal settings", options);
|
|
@@ -498,16 +521,29 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
498
521
|
const label = trimmed.slice(0, colon).trim();
|
|
499
522
|
const row = SETTING_ROWS.find((r) => r.label === label);
|
|
500
523
|
if (!row) continue;
|
|
501
|
-
|
|
502
|
-
|
|
524
|
+
const envVarForRow2 = envOverrideFor(row.key as keyof GoalSettings);
|
|
525
|
+
if (envVarForRow2) {
|
|
526
|
+
ctx.ui.notify(`${row.label} is read-only: overridden by the ${envVarForRow2} env var.`, "warning");
|
|
503
527
|
continue;
|
|
504
528
|
}
|
|
529
|
+
// Per-row settings path (flat keys default to [key]; nested Blocker
|
|
530
|
+
// Oracle rows carry explicit paths).
|
|
531
|
+
const rowPath = row.path ?? [String(row.key)];
|
|
532
|
+
const pathKeyStr = rowPath.join(".");
|
|
533
|
+
const localValueAtPath = (() => {
|
|
534
|
+
let cursor: unknown = localLayer;
|
|
535
|
+
for (const segment of rowPath) {
|
|
536
|
+
if (!cursor || typeof cursor !== "object") return undefined;
|
|
537
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
538
|
+
}
|
|
539
|
+
return cursor;
|
|
540
|
+
})();
|
|
505
541
|
|
|
506
542
|
const inheritLabel = scope === "project" ? "Use inherited value" : "Use default (delete global override)";
|
|
507
|
-
const hasLocalOverride =
|
|
543
|
+
const hasLocalOverride = localValueAtPath !== undefined;
|
|
508
544
|
|
|
509
545
|
if (row.kind === "boolean") {
|
|
510
|
-
const effective = snapshot.provenance.get(
|
|
546
|
+
const effective = snapshot.provenance.get(pathKeyStr)?.value === true;
|
|
511
547
|
const actions = [
|
|
512
548
|
`Set ${scope} override to true`,
|
|
513
549
|
`Set ${scope} override to false`,
|
|
@@ -517,28 +553,28 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
517
553
|
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
518
554
|
if (!action || action === "Cancel") continue;
|
|
519
555
|
if (action === inheritLabel) {
|
|
520
|
-
applyMutation(scope, { op: "unset", path:
|
|
556
|
+
applyMutation(scope, { op: "unset", path: rowPath });
|
|
521
557
|
continue;
|
|
522
558
|
}
|
|
523
559
|
const value = action.endsWith("true");
|
|
524
560
|
if (value !== effective || !hasLocalOverride) {
|
|
525
|
-
applyMutation(scope, { op: "set", path:
|
|
561
|
+
applyMutation(scope, { op: "set", path: rowPath, value });
|
|
526
562
|
}
|
|
527
563
|
continue;
|
|
528
564
|
}
|
|
529
565
|
|
|
530
566
|
if (row.kind === "positiveInteger") {
|
|
531
|
-
const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
|
|
567
|
+
const min = row.path ? 1 : ((row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1);
|
|
532
568
|
const actions = [`Set ${scope} override...`];
|
|
533
569
|
if (hasLocalOverride) actions.push(inheritLabel);
|
|
534
570
|
actions.push("Cancel");
|
|
535
571
|
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
536
572
|
if (!action || action === "Cancel") continue;
|
|
537
573
|
if (action === inheritLabel) {
|
|
538
|
-
applyMutation(scope, { op: "unset", path:
|
|
574
|
+
applyMutation(scope, { op: "unset", path: rowPath });
|
|
539
575
|
continue;
|
|
540
576
|
}
|
|
541
|
-
const input = await ctx.ui.input(`Set ${row.label}`, String(snapshot.provenance.get(
|
|
577
|
+
const input = await ctx.ui.input(`Set ${row.label}`, String(snapshot.provenance.get(pathKeyStr)?.value ?? min));
|
|
542
578
|
if (input === undefined) continue;
|
|
543
579
|
// Full-string decimal validation: rejects 1.5, 1x, negatives,
|
|
544
580
|
// infinity, and unsafe integers alike.
|
|
@@ -547,7 +583,12 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
547
583
|
ctx.ui.notify(`${row.label} must be an integer >= ${min} (e.g. ${min}, ${min + 1}, ${min + 2})`, "warning");
|
|
548
584
|
continue;
|
|
549
585
|
}
|
|
550
|
-
|
|
586
|
+
// Oracle attempt cap is bounded at 3 by the settings parser.
|
|
587
|
+
if (row.path?.[0] === "oracle" && Number(value) > 3) {
|
|
588
|
+
ctx.ui.notify(`${row.label} must be an integer between 1 and 3`, "warning");
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
applyMutation(scope, { op: "set", path: rowPath, value: Number(value) });
|
|
551
592
|
continue;
|
|
552
593
|
}
|
|
553
594
|
|
|
@@ -558,21 +599,26 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
558
599
|
if (!picked) continue;
|
|
559
600
|
const choice = picked.trim().replace(/^\u2713\s+/, "");
|
|
560
601
|
if (choice === "(default)") {
|
|
561
|
-
if (hasLocalOverride) applyMutation(scope, { op: "unset", path:
|
|
602
|
+
if (hasLocalOverride) applyMutation(scope, { op: "unset", path: rowPath });
|
|
562
603
|
continue;
|
|
563
604
|
}
|
|
564
605
|
if (!(AUDITOR_THINKING_LEVELS as readonly string[]).includes(choice)) {
|
|
565
606
|
ctx.ui.notify(`thinking_level must be one of: ${AUDITOR_THINKING_LEVELS.join(", ")} (or "(default)")`, "warning");
|
|
566
607
|
continue;
|
|
567
608
|
}
|
|
568
|
-
applyMutation(scope, { op: "set", path:
|
|
609
|
+
applyMutation(scope, { op: "set", path: rowPath, value: choice });
|
|
569
610
|
continue;
|
|
570
611
|
}
|
|
571
612
|
|
|
572
|
-
// modelSelector rows (provider, model): searchable
|
|
573
|
-
//
|
|
574
|
-
// the
|
|
575
|
-
const
|
|
613
|
+
// modelSelector rows (provider, model): searchable model picker with
|
|
614
|
+
// explicit inherit/default handling. Auditor rows apply provider+model
|
|
615
|
+
// at the flat paths; the Oracle row writes oracle.provider/oracle.model.
|
|
616
|
+
const oraclePair = row.path?.[0] === "oracle";
|
|
617
|
+
const pairPrefix = oraclePair ? ["oracle"] : [];
|
|
618
|
+
const configuredBase = oraclePair
|
|
619
|
+
? [snapshot.value.oracle?.provider, snapshot.value.oracle?.model].filter(Boolean).join("/")
|
|
620
|
+
: configuredAuditorModelKey(snapshot.value as GoalSettings);
|
|
621
|
+
const configured = configuredBase || undefined;
|
|
576
622
|
const session = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
577
623
|
const choices = buildAuditorModelChoices(ctx.modelRegistry.getAvailable(), configured, session);
|
|
578
624
|
const filter = await ctx.ui.input("Filter auditor models (provider/id/name; blank = all)", "");
|
|
@@ -584,7 +630,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
584
630
|
if (!choice) continue;
|
|
585
631
|
if (choice.kind === "default") {
|
|
586
632
|
for (const key of ["provider", "model"] as const) {
|
|
587
|
-
if (localLayer[key] !== undefined) applyMutation(scope, { op: "unset", path: [key] });
|
|
633
|
+
if (localLayer[key] !== undefined) applyMutation(scope, { op: "unset", path: [...pairPrefix, key] });
|
|
588
634
|
}
|
|
589
635
|
continue;
|
|
590
636
|
}
|
|
@@ -604,8 +650,8 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
604
650
|
providerValue = choice.provider;
|
|
605
651
|
modelValue = choice.model;
|
|
606
652
|
}
|
|
607
|
-
if (applyMutation(scope, { op: "set", path: ["provider"], value: providerValue })) {
|
|
608
|
-
applyMutation(scope, { op: "set", path: ["model"], value: modelValue });
|
|
653
|
+
if (applyMutation(scope, { op: "set", path: [...pairPrefix, "provider"], value: providerValue })) {
|
|
654
|
+
applyMutation(scope, { op: "set", path: [...pairPrefix, "model"], value: modelValue });
|
|
609
655
|
}
|
|
610
656
|
}
|
|
611
657
|
} finally {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type GoalLedgerEvent,
|
|
12
12
|
type LedgerStateReadResult,
|
|
13
13
|
} from "./goal-ledger.ts";
|
|
14
|
-
import { type GoalRecord } from "./goal-record.ts";
|
|
14
|
+
import { type GoalRecord, type GoalTask } from "./goal-record.ts";
|
|
15
15
|
|
|
16
16
|
export function buildGoalCompactSummary(
|
|
17
17
|
goal: GoalRecord,
|
|
@@ -151,3 +151,51 @@ export function buildCompactionSummary(args: {
|
|
|
151
151
|
|
|
152
152
|
return lines.join("\n");
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* PR E §62: post-compaction DELTA for the focused goal. The active system
|
|
157
|
+
* goal block already contains the objective, full policy, task gate, and
|
|
158
|
+
* verification contract — this delta adds only what compaction may have
|
|
159
|
+
* lost: the execution focus, a bounded recent-events tail, the latest
|
|
160
|
+
* unresolved auditor finding, and the other-open-goals count.
|
|
161
|
+
*/
|
|
162
|
+
export function buildPostCompactionGoalDelta(args: {
|
|
163
|
+
goal: GoalRecord;
|
|
164
|
+
ledgerEvents: GoalLedgerEvent[];
|
|
165
|
+
otherOpenCount: number;
|
|
166
|
+
}): string {
|
|
167
|
+
const { goal, ledgerEvents, otherOpenCount } = args;
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
lines.push(`[POST-COMPACTION RESYNC goalId=${goal.id}]`);
|
|
170
|
+
// Execution focus (with contract) — the one thing memory must not lose.
|
|
171
|
+
if (goal.currentTaskId && goal.taskList) {
|
|
172
|
+
const find = (tasks: GoalTask[]): GoalTask | undefined => {
|
|
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);
|
|
181
|
+
if (current) {
|
|
182
|
+
lines.push(`Current task: ${current.id} — ${current.title}${current.verificationContract ? ` (contract: ${current.verificationContract})` : ""}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Bounded recent-event tail.
|
|
186
|
+
const recent = latestEventsForGoal(ledgerEvents, goal.id, 5);
|
|
187
|
+
if (recent.length > 0) {
|
|
188
|
+
lines.push("Recent events:");
|
|
189
|
+
for (const event of recent) {
|
|
190
|
+
lines.push(` ${event.at.slice(11, 19)} ${event.type}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Latest unresolved auditor finding.
|
|
194
|
+
const audit = latestAuditorResultForGoal(ledgerEvents, goal.id);
|
|
195
|
+
if (audit && audit.verdict === "disapproved") {
|
|
196
|
+
lines.push(`Latest unresolved auditor finding: ${audit.report.slice(0, 300)}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(`Other open goals: ${otherOpenCount}`);
|
|
199
|
+
lines.push("Continue from authoritative files and goal storage.");
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
@@ -195,7 +195,7 @@ if (settings.disabled === true) {
|
|
|
195
195
|
].filter((line): line is string => line !== undefined).join("\n"),
|
|
196
196
|
display: true,
|
|
197
197
|
details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel },
|
|
198
|
-
}, { triggerTurn:
|
|
198
|
+
}, { triggerTurn: false });
|
|
199
199
|
if (!core.isFocusedOperationCurrent(completionFocus)) {
|
|
200
200
|
return core.focusedOperationCancelledResult("Goal completion", completionFocus);
|
|
201
201
|
}
|