pi-goal-x 0.27.5 → 0.28.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 +11 -2
- package/extensions/goal-commands.ts +168 -101
- package/extensions/goal-settings.ts +828 -204
- package/extensions/goal-state.ts +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -400,12 +400,21 @@ the timestamped `.backup-*` file created next to the session.
|
|
|
400
400
|
|
|
401
401
|
## Configuration
|
|
402
402
|
|
|
403
|
-
Settings
|
|
403
|
+
Settings resolve per setting in this order (highest wins):
|
|
404
404
|
|
|
405
405
|
```text
|
|
406
|
-
|
|
406
|
+
environment > project layer > global layer > defaults
|
|
407
407
|
```
|
|
408
408
|
|
|
409
|
+
Files:
|
|
410
|
+
|
|
411
|
+
```text
|
|
412
|
+
global: ~/.pi/pi-goal-x-settings.json (or $PI_CODING_AGENT_DIR, or $PI_GOAL_GLOBAL_SETTINGS_FILE)
|
|
413
|
+
project: <cwd>/.pi/pi-goal-x-settings.json (or $PI_GOAL_SETTINGS_FILE)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Define shared configuration once in the global file and override per project. Explicit `false`/`0` values in a lower layer override inherited values; nested `keybindings` inherit per key. `/goal-settings` shows each row's effective value and source, can switch the edited scope, and can remove a local override to return to inheritance.
|
|
417
|
+
|
|
409
418
|
Use `/goal-settings` to configure task lists, verification contracts, subtask depth, automatic goal selection, and completion auditing. Goal objectives have no hard length limit by default; set `objectiveMaxChars` (or `PI_GOAL_OBJECTIVE_MAX_CHARS`, `0` = no limit) to cap objective length across `create_goal`, `propose_goal_draft`, and `/goal-tweak`.
|
|
410
419
|
|
|
411
420
|
Configure the task shortcuts in the same file when the terminal captures the defaults:
|
|
@@ -5,10 +5,15 @@ import { extractVerificationContract, sisyphusObjectiveSufficient } from "./goal
|
|
|
5
5
|
import { detailedSummary, oneLineSummary } from "./goal-format.ts";
|
|
6
6
|
import {
|
|
7
7
|
goalSettingsPath,
|
|
8
|
+
goalGlobalSettingsPath,
|
|
8
9
|
loadGoalSettings,
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
loadSettingsSnapshot,
|
|
11
|
+
mutateSettingsLayer,
|
|
12
|
+
readSettingsLayer,
|
|
13
|
+
envOverrideFor,
|
|
11
14
|
type GoalSettings,
|
|
15
|
+
type SettingsMutation,
|
|
16
|
+
type SettingsScope,
|
|
12
17
|
} from "./goal-settings.ts";
|
|
13
18
|
import {
|
|
14
19
|
buildGoalListText,
|
|
@@ -19,7 +24,7 @@ import {
|
|
|
19
24
|
import { clearGoalCommandMessage, validateResumeGoal } from "./goal-policy.ts";
|
|
20
25
|
import { invalidateGoalLedgerCache, readGoalLedger } from "./goal-ledger.ts";
|
|
21
26
|
import { buildGoalStatusText } from "./goal-status.ts";
|
|
22
|
-
import { effectiveSettingsReport,
|
|
27
|
+
import { effectiveSettingsReport, invalidateGoalSettingsCache, loadGoalSettingsFileConfig } from "./goal-settings.ts";
|
|
23
28
|
import { invalidateGoalPoolCache, mergeGoalPromptFromDisk, readActiveGoalPool } from "./storage/goal-files.ts";
|
|
24
29
|
import { nowIso, type GoalMode, type GoalRecord } from "./goal-record.ts";
|
|
25
30
|
import { clearGoalDrafting, hasActiveDraft, startGoalDrafting } from "./goal-drafting.ts";
|
|
@@ -383,6 +388,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
383
388
|
|
|
384
389
|
const SETTING_ROWS: readonly SettingRow[] = [
|
|
385
390
|
{ key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
|
|
391
|
+
{ key: "hideUnfocusedBanner", label: "hideUnfocusedBanner", section: "Goal behavior", kind: "boolean" },
|
|
386
392
|
{ key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
|
|
387
393
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
|
|
388
394
|
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
|
|
@@ -395,7 +401,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
395
401
|
];
|
|
396
402
|
|
|
397
403
|
function settingsValue(config: GoalSettings, key: keyof GoalSettings): string {
|
|
398
|
-
if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources") {
|
|
404
|
+
if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
|
|
399
405
|
return config[key] === true ? "true" : "false";
|
|
400
406
|
}
|
|
401
407
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
@@ -412,29 +418,52 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
412
418
|
|
|
413
419
|
async function handleSettingsMenu(ctx: ExtensionContext): Promise<void> {
|
|
414
420
|
if (!ctx.hasUI) {
|
|
415
|
-
|
|
421
|
+
// Headless: read-only report of both layer paths.
|
|
422
|
+
ctx.ui.notify(
|
|
423
|
+
`Settings — project file: ${goalSettingsPath(ctx.cwd)}\nGlobal file: ${goalGlobalSettingsPath()}`,
|
|
424
|
+
"info",
|
|
425
|
+
);
|
|
416
426
|
return;
|
|
417
427
|
}
|
|
428
|
+
|
|
418
429
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* off within one menu session reinstalls on every real change instead of
|
|
424
|
-
* comparing against the value captured when the menu opened.
|
|
430
|
+
* After any mutation: reinstall the fixed three/five tool profile when
|
|
431
|
+
* the effective disableTasks value changed since the last install, and
|
|
432
|
+
* refresh the UI. This is the central settings side-effect hook — no
|
|
433
|
+
* ad hoc per-row refresh code.
|
|
425
434
|
*/
|
|
426
|
-
const
|
|
427
|
-
saveGoalSettingsFileConfig(ctx.cwd, next);
|
|
435
|
+
const applyEffectiveSettings = (): void => {
|
|
428
436
|
const tasksEnabledNow = !loadGoalSettings(ctx.cwd).disableTasks;
|
|
429
437
|
if (tasksEnabledNow !== core.tasksEnabled) {
|
|
430
438
|
core.installGoalToolProfile(tasksEnabledNow);
|
|
431
439
|
}
|
|
440
|
+
// PR #29: settings changes must be visible immediately — the banner
|
|
441
|
+
// hide/restore happens through this coalesced UI refresh.
|
|
442
|
+
core.updateUI(ctx);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const applyMutation = (scope: SettingsScope, mutation: SettingsMutation): boolean => {
|
|
446
|
+
try {
|
|
447
|
+
mutateSettingsLayer({ scope, cwd: ctx.cwd, mutation });
|
|
448
|
+
applyEffectiveSettings();
|
|
449
|
+
return true;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
ctx.ui.notify(`Settings change failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
432
454
|
};
|
|
455
|
+
|
|
456
|
+
const scopePath = (scope: SettingsScope): string =>
|
|
457
|
+
scope === "global" ? goalGlobalSettingsPath() : goalSettingsPath(ctx.cwd);
|
|
458
|
+
|
|
433
459
|
core.enterGoalModal();
|
|
434
460
|
try {
|
|
461
|
+
let scope: SettingsScope = "project";
|
|
435
462
|
while (true) {
|
|
436
|
-
const
|
|
463
|
+
const snapshot = loadSettingsSnapshot(ctx.cwd);
|
|
464
|
+
const localLayer = snapshot[scope].layer as Record<string, unknown>;
|
|
437
465
|
const options: string[] = [];
|
|
466
|
+
options.push(`─── Editing: ${scope} (${scopePath(scope)}) ───`);
|
|
438
467
|
let lastSection: string | null = null;
|
|
439
468
|
for (const row of SETTING_ROWS) {
|
|
440
469
|
if (row.section !== lastSection) {
|
|
@@ -442,109 +471,147 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
442
471
|
lastSection = row.section;
|
|
443
472
|
}
|
|
444
473
|
const envVar = envOverrideFor(row.key);
|
|
445
|
-
|
|
474
|
+
if (envVar) {
|
|
475
|
+
options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} (environment; read-only)`);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const provenance = snapshot.provenance.get(String(row.key));
|
|
479
|
+
let source: string;
|
|
480
|
+
if (provenance?.source === "global") source = scope === "project" ? "(inherited from global)" : "(global override)";
|
|
481
|
+
else if (provenance?.source === "project") source = scope === "project" ? "(project override)" : "(project; edit via project scope)";
|
|
482
|
+
else source = "(default)";
|
|
483
|
+
const hasLocalOverride = localLayer[row.key] !== undefined;
|
|
484
|
+
if (hasLocalOverride && provenance?.source === scope) source = `(${scope} override)`;
|
|
485
|
+
options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} ${source}`);
|
|
446
486
|
}
|
|
447
487
|
options.push("Done");
|
|
448
488
|
const selected = await ctx.ui.select("Goal settings", options);
|
|
449
489
|
if (!selected || selected === "Done") break;
|
|
450
|
-
if (selected.startsWith("───")) continue; //
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
490
|
+
if (selected.startsWith("───")) continue; // headers are not rows
|
|
491
|
+
const trimmed = selected.trim();
|
|
492
|
+
if (trimmed.startsWith("Scope:")) {
|
|
493
|
+
scope = scope === "project" ? "global" : "project";
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const colon = trimmed.indexOf(":");
|
|
454
497
|
if (colon === -1) continue;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
ctx.ui.notify(`${row.label} is read-only: overridden by the ${envOverrideFor(key)} env var.`, "warning");
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
if (row.kind === "boolean") {
|
|
464
|
-
const next = { ...config, [key]: config[key] !== true };
|
|
465
|
-
saveSettings(next);
|
|
466
|
-
ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
if (row.kind === "positiveInteger") {
|
|
470
|
-
const input = await ctx.ui.input(`Set ${row.label}`, settingsValue(config, key));
|
|
471
|
-
if (input === undefined) continue;
|
|
472
|
-
// Row-driven lower bound: subtaskDepth is a nesting depth (min 1);
|
|
473
|
-
// stallTimeoutMinutes and objectiveMaxChars default to 0 meaning
|
|
474
|
-
// "off / no limit".
|
|
475
|
-
const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
|
|
476
|
-
// Full-string decimal validation: no partial parseInt. Rejects
|
|
477
|
-
// 1.5, 1x, negatives, infinity, and unsafe integers alike.
|
|
478
|
-
const trimmed = input.trim();
|
|
479
|
-
if (!/^[0-9]+$/.test(trimmed) || !Number.isSafeInteger(Number(trimmed)) || Number(trimmed) < min) {
|
|
480
|
-
ctx.ui.notify(`${row.label} must be an integer >= ${min} (e.g. ${min}, ${min + 1}, ${min + 2})`, "warning");
|
|
498
|
+
const label = trimmed.slice(0, colon).trim();
|
|
499
|
+
const row = SETTING_ROWS.find((r) => r.label === label);
|
|
500
|
+
if (!row) continue;
|
|
501
|
+
if (envOverrideFor(row.key)) {
|
|
502
|
+
ctx.ui.notify(`${row.label} is read-only: overridden by the ${envOverrideFor(row.key)} env var.`, "warning");
|
|
481
503
|
continue;
|
|
482
504
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
505
|
+
|
|
506
|
+
const inheritLabel = scope === "project" ? "Use inherited value" : "Use default (delete global override)";
|
|
507
|
+
const hasLocalOverride = localLayer[row.key] !== undefined;
|
|
508
|
+
|
|
509
|
+
if (row.kind === "boolean") {
|
|
510
|
+
const effective = snapshot.provenance.get(String(row.key))?.value === true;
|
|
511
|
+
const actions = [
|
|
512
|
+
`Set ${scope} override to true`,
|
|
513
|
+
`Set ${scope} override to false`,
|
|
514
|
+
];
|
|
515
|
+
if (hasLocalOverride) actions.push(inheritLabel);
|
|
516
|
+
actions.push("Cancel");
|
|
517
|
+
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
518
|
+
if (!action || action === "Cancel") continue;
|
|
519
|
+
if (action === inheritLabel) {
|
|
520
|
+
applyMutation(scope, { op: "unset", path: [row.key] });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const value = action.endsWith("true");
|
|
524
|
+
if (value !== effective || !hasLocalOverride) {
|
|
525
|
+
applyMutation(scope, { op: "set", path: [row.key], value });
|
|
526
|
+
}
|
|
501
527
|
continue;
|
|
502
528
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
delete next.model;
|
|
526
|
-
} else if (choice.kind === "manual") {
|
|
527
|
-
const input = await ctx.ui.input("Set auditor provider/model", configured ?? "provider/model");
|
|
528
|
-
if (input === undefined) continue;
|
|
529
|
-
const parsed = parseManualAuditorModel(input);
|
|
530
|
-
if ("error" in parsed) {
|
|
531
|
-
ctx.ui.notify(parsed.error, "warning");
|
|
529
|
+
|
|
530
|
+
if (row.kind === "positiveInteger") {
|
|
531
|
+
const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
|
|
532
|
+
const actions = [`Set ${scope} override...`];
|
|
533
|
+
if (hasLocalOverride) actions.push(inheritLabel);
|
|
534
|
+
actions.push("Cancel");
|
|
535
|
+
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
536
|
+
if (!action || action === "Cancel") continue;
|
|
537
|
+
if (action === inheritLabel) {
|
|
538
|
+
applyMutation(scope, { op: "unset", path: [row.key] });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const input = await ctx.ui.input(`Set ${row.label}`, String(snapshot.provenance.get(String(row.key))?.value ?? min));
|
|
542
|
+
if (input === undefined) continue;
|
|
543
|
+
// Full-string decimal validation: rejects 1.5, 1x, negatives,
|
|
544
|
+
// infinity, and unsafe integers alike.
|
|
545
|
+
const value = input.trim();
|
|
546
|
+
if (!/^[0-9]+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min) {
|
|
547
|
+
ctx.ui.notify(`${row.label} must be an integer >= ${min} (e.g. ${min}, ${min + 1}, ${min + 2})`, "warning");
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
applyMutation(scope, { op: "set", path: [row.key], value: Number(value) });
|
|
532
551
|
continue;
|
|
533
552
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
553
|
+
|
|
554
|
+
if (row.kind === "thinking") {
|
|
555
|
+
const currentValue = settingsValue(snapshot.value, row.key);
|
|
556
|
+
const levels = thinkingLevelChoices(currentValue === "(default)" ? undefined : currentValue);
|
|
557
|
+
const picked = await ctx.ui.select(`Set ${row.label} (${scope})`, levels);
|
|
558
|
+
if (!picked) continue;
|
|
559
|
+
const choice = picked.trim().replace(/^\u2713\s+/, "");
|
|
560
|
+
if (choice === "(default)") {
|
|
561
|
+
if (hasLocalOverride) applyMutation(scope, { op: "unset", path: ["thinkingLevel"] });
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (!(AUDITOR_THINKING_LEVELS as readonly string[]).includes(choice)) {
|
|
565
|
+
ctx.ui.notify(`thinking_level must be one of: ${AUDITOR_THINKING_LEVELS.join(", ")} (or "(default)")`, "warning");
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
applyMutation(scope, { op: "set", path: ["thinkingLevel"], value: choice });
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// modelSelector rows (provider, model): searchable auditor model
|
|
573
|
+
// picker with explicit inherit/default handling. Either row applies
|
|
574
|
+
// the same provider+model pair.
|
|
575
|
+
const configured = configuredAuditorModelKey(snapshot.value as GoalSettings);
|
|
576
|
+
const session = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
577
|
+
const choices = buildAuditorModelChoices(ctx.modelRegistry.getAvailable(), configured, session);
|
|
578
|
+
const filter = await ctx.ui.input("Filter auditor models (provider/id/name; blank = all)", "");
|
|
579
|
+
if (filter === undefined) continue;
|
|
580
|
+
const filtered = filterAuditorModelChoices(choices, filter);
|
|
581
|
+
const picked = await ctx.ui.select("Select auditor model", filtered.map((choice: AuditorChoice) => choice.label));
|
|
582
|
+
if (!picked) continue;
|
|
583
|
+
const choice = filtered.find((candidate: AuditorChoice) => candidate.label === picked);
|
|
584
|
+
if (!choice) continue;
|
|
585
|
+
if (choice.kind === "default") {
|
|
586
|
+
for (const key of ["provider", "model"] as const) {
|
|
587
|
+
if (localLayer[key] !== undefined) applyMutation(scope, { op: "unset", path: [key] });
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
let providerValue: string;
|
|
592
|
+
let modelValue: string;
|
|
593
|
+
if (choice.kind === "manual") {
|
|
594
|
+
const input = await ctx.ui.input("Set auditor provider/model", configured ?? "provider/model");
|
|
595
|
+
if (input === undefined) continue;
|
|
596
|
+
const parsed = parseManualAuditorModel(input);
|
|
597
|
+
if ("error" in parsed) {
|
|
598
|
+
ctx.ui.notify(parsed.error, "warning");
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
providerValue = parsed.provider;
|
|
602
|
+
modelValue = parsed.model;
|
|
603
|
+
} else {
|
|
604
|
+
providerValue = choice.provider;
|
|
605
|
+
modelValue = choice.model;
|
|
606
|
+
}
|
|
607
|
+
if (applyMutation(scope, { op: "set", path: ["provider"], value: providerValue })) {
|
|
608
|
+
applyMutation(scope, { op: "set", path: ["model"], value: modelValue });
|
|
609
|
+
}
|
|
542
610
|
}
|
|
543
611
|
} finally {
|
|
544
612
|
core.exitGoalModal();
|
|
545
613
|
}
|
|
546
614
|
}
|
|
547
|
-
|
|
548
615
|
async function handleGoalClear(ctx: ExtensionContext): Promise<void> {
|
|
549
616
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
550
617
|
if (!core.state.goal && core.openGoals().length > 0) {
|