pi-goal-x 0.25.2 → 0.25.3
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 -2
- package/docs/unified-dashboard.md +12 -3
- package/extensions/goal-activity.ts +2 -0
- package/extensions/goal-commands.ts +11 -5
- package/extensions/goal-compaction.ts +3 -0
- package/extensions/goal-core-tools.ts +5 -3
- package/extensions/goal-drafting.ts +47 -3
- package/extensions/goal-ledger.ts +5 -0
- package/extensions/goal-settings.ts +26 -1
- package/extensions/goal-state.ts +35 -0
- package/extensions/goal-widget.ts +10 -0
- package/extensions/widgets/goal-dashboard-model.ts +3 -0
- package/extensions/widgets/goal-dashboard-renderer.ts +22 -8
- package/package.json +9 -3
- package/docs/.pi/goals/.goals-pool-snapshot.json +0 -1
- package/docs/.pi/goals/archived/goal_2026080600262071_msglt8us-fsjgtu.md +0 -157
- package/docs/.pi/goals/archived/goal_2026080611065364_mshc1y08-b6pnc1.md +0 -76
- package/docs/.pi/goals/archived/goal_2026080613543398_mshg3jex-svqfyt.md +0 -86
- package/docs/.pi/goals/archived/goal_2026080614052654_mshisjml-bl1gar.md +0 -82
- package/docs/.pi/goals/archived/goal_2026080614173417_mshjewld-9z3e9p.md +0 -72
- package/docs/.pi/goals/archived/goal_2026080614441814_mshk5uvt-nnkztu.md +0 -80
- package/docs/.pi/goals/archived/goal_2026080708140425_msilpn44-qqa4fa.md +0 -76
- package/docs/.pi/goals/archived/goal_2026080708322737_msim9m81-5aa98r.md +0 -92
- package/docs/.pi/goals/goal_events.jsonl +0 -122
package/README.md
CHANGED
|
@@ -260,9 +260,13 @@ Always visible above the editor while a goal is focused:
|
|
|
260
260
|
│ Current t3 · Add the download button │
|
|
261
261
|
│ Verify Run npm test with zero failures. │
|
|
262
262
|
│ File .pi/goals/active_goal_... │
|
|
263
|
-
╰─ Ctrl+Shift+T: expand tasks
|
|
263
|
+
╰─ Ctrl+Shift+T: expand tasks · Ctrl+Shift+A ──────────────────── ● ─╯
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
+
The green/gray `●` at the bottom-right of the border is the focused goal's
|
|
267
|
+
independent-auditor status (green = on, gray = off); `Ctrl+Shift+A` toggles
|
|
268
|
+
it per-goal (wide/medium footers advertise the chord).
|
|
269
|
+
|
|
266
270
|
### Expanded mode
|
|
267
271
|
|
|
268
272
|
`Ctrl+Shift+T` expands the same component: full task tree (✓ complete, ▸ current, ~ skipped, · pending), the current-task block with its contract and evidence, goal-level verification, and recent activity.
|
|
@@ -351,7 +355,7 @@ Settings are stored in:
|
|
|
351
355
|
.pi/pi-goal-x-settings.json
|
|
352
356
|
```
|
|
353
357
|
|
|
354
|
-
Use `/goal-settings` to configure task lists, verification contracts, subtask depth, automatic goal selection, and completion auditing.
|
|
358
|
+
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`.
|
|
355
359
|
|
|
356
360
|
## License
|
|
357
361
|
|
|
@@ -32,7 +32,7 @@ Always visible above the editor while a goal is focused:
|
|
|
32
32
|
│ Current t3 · Add the download button │
|
|
33
33
|
│ Verify Run npm test with zero failures. │
|
|
34
34
|
│ File .pi/goals/active_goal_g1.md │
|
|
35
|
-
╰─ Ctrl+Shift+T: expand tasks
|
|
35
|
+
╰─ Ctrl+Shift+T: expand tasks · Ctrl+Shift+A ──────────────────────────────────────── ● ─╯
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Compact rows, in order:
|
|
@@ -52,7 +52,7 @@ Compact rows, in order:
|
|
|
52
52
|
tasks. The subtask segment appears only when the current task has direct
|
|
53
53
|
children. The whole header row is neutral gray (label, counts, bar fill,
|
|
54
54
|
and empty cells) so nothing in the progress info is colourful.
|
|
55
|
-
|
|
55
|
+
6. Task list section: the top-level tasks shown by default in **pastel
|
|
56
56
|
amber** (`mdHeading`) with colour-coded markers and ids (✓ complete muted
|
|
57
57
|
green, ▸ current teal, ~ skipped gray, · pending amber), an aligned id
|
|
58
58
|
column, truncated amber titles, and a `… +N more` overflow line when the
|
|
@@ -72,7 +72,15 @@ Compact rows, in order:
|
|
|
72
72
|
shortcut hint and trailing fill — is drawn in one frame tone (the
|
|
73
73
|
neutral-gray `muted`), so the color spans the full width with no two-tone
|
|
74
74
|
split; the header line is likewise one tone with only the `pi-goal-x`
|
|
75
|
-
brand in accent.
|
|
75
|
+
brand in accent. The auditor status lives in the bottom-right of this
|
|
76
|
+
border: a single `●` — green when the focused goal's independent auditor
|
|
77
|
+
is on, gray when off — drawn right before the `╯` corner, and the
|
|
78
|
+
wide/medium footers also append ` · Ctrl+Shift+A` to the hint (narrow and
|
|
79
|
+
minimal keep the dot alone). `Ctrl+Shift+A` toggles the auditor per-goal
|
|
80
|
+
(persisted, `auditor_toggled` ledger event, dashboard refresh,
|
|
81
|
+
notification; inert with no focused goal, a goal modal open, or a
|
|
82
|
+
complete goal). The expanded dashboard does not render the auditor
|
|
83
|
+
indicator.
|
|
76
84
|
|
|
77
85
|
When every top-level task is done, the current line reads
|
|
78
86
|
`Current All tasks complete`. With `disableTasks` enabled the task rows are
|
|
@@ -212,6 +220,7 @@ auditor's findings, and returns to the normal dashboard so work can continue.
|
|
|
212
220
|
| Key | Action |
|
|
213
221
|
| --- | --- |
|
|
214
222
|
| `Ctrl+Shift+T` | Toggle the dashboard between compact and expanded |
|
|
223
|
+
| `Ctrl+Shift+A` | Toggle the focused goal's independent auditor on/off (persisted per-goal; inert with no goal, a goal modal open, or a complete goal) |
|
|
215
224
|
| `↑` / `↓`, `PgUp` / `PgDn`, `Home` / `End` | Scroll the expanded task tree (see [Scrolling the task list](#scrolling-the-task-list)) |
|
|
216
225
|
| `Ctrl+Shift+↑` / `Ctrl+Shift+↓`, `Ctrl+Shift+PgUp` / `Ctrl+Shift+PgDn`, `Ctrl+Shift+Home` / `Ctrl+Shift+End` | Scroll the compact task list |
|
|
217
226
|
| `Esc` | Collapse the expanded dashboard; otherwise pause the goal |
|
|
@@ -61,6 +61,8 @@ function mapEvent(event: GoalLedgerEvent, taskTitles: ReadonlyMap<string, string
|
|
|
61
61
|
return { at: event.at, kind: "goal", text: "Created and focused the goal." };
|
|
62
62
|
case "goal_tweaked":
|
|
63
63
|
return { at: event.at, kind: "goal", text: "Updated the goal objective and task plan." };
|
|
64
|
+
case "auditor_toggled":
|
|
65
|
+
return { at: event.at, kind: "goal", text: event.enabled ? "Turned the independent auditor on." : "Turned the independent auditor off." };
|
|
64
66
|
case "goal_paused":
|
|
65
67
|
return {
|
|
66
68
|
at: event.at,
|
|
@@ -262,6 +262,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
262
262
|
{ key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
|
|
263
263
|
{ key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
|
|
264
264
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
|
|
265
|
+
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
|
|
265
266
|
{ key: "disableTasks", label: "disableTasks", section: "Task tracking", kind: "boolean" },
|
|
266
267
|
{ key: "subtaskDepth", label: "subtaskDepth", section: "Task tracking", kind: "positiveInteger" },
|
|
267
268
|
{ key: "disabled", label: "auditor disabled", section: "Completion auditor", kind: "boolean" },
|
|
@@ -276,6 +277,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
276
277
|
}
|
|
277
278
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
278
279
|
if (key === "stallTimeoutMinutes") return config.stallTimeoutMinutes !== undefined ? String(config.stallTimeoutMinutes) : "0";
|
|
280
|
+
if (key === "objectiveMaxChars") return config.objectiveMaxChars !== undefined ? String(config.objectiveMaxChars) : "0";
|
|
279
281
|
return config[key] ?? "(default)";
|
|
280
282
|
}
|
|
281
283
|
|
|
@@ -343,8 +345,9 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
343
345
|
const input = await ctx.ui.input(`Set ${row.label}`, settingsValue(config, key));
|
|
344
346
|
if (input === undefined) continue;
|
|
345
347
|
// Row-driven lower bound: subtaskDepth is a nesting depth (min 1);
|
|
346
|
-
// stallTimeoutMinutes
|
|
347
|
-
|
|
348
|
+
// stallTimeoutMinutes and objectiveMaxChars default to 0 meaning
|
|
349
|
+
// "off / no limit".
|
|
350
|
+
const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
|
|
348
351
|
// Full-string decimal validation: no partial parseInt. Rejects
|
|
349
352
|
// 1.5, 1x, negatives, infinity, and unsafe integers alike.
|
|
350
353
|
const trimmed = input.trim();
|
|
@@ -478,9 +481,12 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
478
481
|
ctx.ui.notify("Provide the replacement objective: /goal-tweak <new objective>", "info");
|
|
479
482
|
return;
|
|
480
483
|
}
|
|
481
|
-
if (trimmed.length >
|
|
482
|
-
|
|
483
|
-
|
|
484
|
+
if (trimmed.length > (loadGoalSettings(ctx.cwd).objectiveMaxChars ?? 0)) {
|
|
485
|
+
const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
|
|
486
|
+
if (max !== undefined && max > 0) {
|
|
487
|
+
ctx.ui.notify(`Replacement objective exceeds ${max} characters (${trimmed.length}).`, "warning");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
484
490
|
}
|
|
485
491
|
await startGoalDrafting(core, ctx, "tweak", trimmed, currentGoal);
|
|
486
492
|
}
|
|
@@ -37,6 +37,9 @@ export function buildGoalCompactSummary(goal: GoalRecord, events: GoalLedgerEven
|
|
|
37
37
|
case "goal_tweaked":
|
|
38
38
|
lines.push(` - tweaked: ${event.changeSummary}`);
|
|
39
39
|
break;
|
|
40
|
+
case "auditor_toggled":
|
|
41
|
+
lines.push(` - auditor toggled ${event.enabled ? "on" : "off"}`);
|
|
42
|
+
break;
|
|
40
43
|
case "completion_requested":
|
|
41
44
|
lines.push(` - completion requested${event.summary ? `: ${truncateText(event.summary, 80)}` : ""}`);
|
|
42
45
|
break;
|
|
@@ -8,6 +8,7 @@ import { budgetLine } from "./goal-accounting.ts";
|
|
|
8
8
|
import { buildGoalCreatedReport, buildTaskSummary, validateGoalAgentPause, validateGoalBlock } from "./goal-policy.ts";
|
|
9
9
|
import { buildUnfocusedOpenGoalsSummary, otherOpenGoalCount } from "./goal-pool.ts";
|
|
10
10
|
import { readGoalLedger } from "./goal-ledger.ts";
|
|
11
|
+
import { loadGoalSettings } from "./goal-settings.ts";
|
|
11
12
|
import { buildGoalHistoryBlock, buildGoalTaskDetailBlock } from "./goal-format.ts";
|
|
12
13
|
import { sisyphusStepProgress } from "./goal-policy.ts";
|
|
13
14
|
import { deriveTasksFromObjective } from "./goal-task-derive.ts";
|
|
@@ -105,7 +106,7 @@ pi.registerTool(defineTool({
|
|
|
105
106
|
"Pass mode=\"sisyphus\" only when the user explicitly invoked Sisyphus mode.",
|
|
106
107
|
],
|
|
107
108
|
parameters: Type.Object({
|
|
108
|
-
objective: Type.String({ description: "Full goal text. For Sisyphus goals this MUST include the user's numbered steps + per-step done criteria, taken faithfully from the user's input.
|
|
109
|
+
objective: Type.String({ description: "Full goal text. For Sisyphus goals this MUST include the user's numbered steps + per-step done criteria, taken faithfully from the user's input. Length is capped by the `max objective length` goal setting (0/unset = no limit)." }),
|
|
109
110
|
mode: Type.Optional(StringEnum(["regular", "sisyphus"] as const, { description: "Goal mode. Defaults to regular. Use sisyphus only when the user explicitly invoked Sisyphus mode." })),
|
|
110
111
|
token_budget: Type.Optional(Type.Integer({ minimum: 1, description: "Optional token budget in whole tokens. Accept it only when the user explicitly supplied a budget; never invent one." })),
|
|
111
112
|
}, { additionalProperties: false }),
|
|
@@ -114,9 +115,10 @@ pi.registerTool(defineTool({
|
|
|
114
115
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
115
116
|
const objective = params.objective.trim();
|
|
116
117
|
if (!objective) throw new Error("create_goal requires a non-empty objective.");
|
|
117
|
-
|
|
118
|
+
const objectiveMaxChars = loadGoalSettings(ctx.cwd).objectiveMaxChars ?? 0;
|
|
119
|
+
if (objectiveMaxChars > 0 && objective.length > objectiveMaxChars) {
|
|
118
120
|
return {
|
|
119
|
-
content: [{ type: "text", text: `create_goal objective exceeds
|
|
121
|
+
content: [{ type: "text", text: `create_goal objective exceeds ${objectiveMaxChars} characters (${objective.length}). Shorten the objective or raise the max objective length setting.` }],
|
|
120
122
|
details: goalDetails(core.state.goal),
|
|
121
123
|
};
|
|
122
124
|
}
|
|
@@ -153,7 +153,14 @@ export async function startGoalDrafting(core: GoalCore, ctx: ExtensionContext, m
|
|
|
153
153
|
clearGoalDrafting(core, ctx);
|
|
154
154
|
}
|
|
155
155
|
const startedAt = nowIso();
|
|
156
|
-
|
|
156
|
+
// §tweak-persist: a tweak confirmation defaults to the target goal's
|
|
157
|
+
// persisted per-goal auditor setting (skipAuditor); the global `auditor
|
|
158
|
+
// disabled` setting is only the fallback when the goal has no per-goal
|
|
159
|
+
// value. Confirming a tweak must never silently re-enable or disable the
|
|
160
|
+
// auditor the user chose when the goal was created.
|
|
161
|
+
const auditorEnabled = mode === "tweak"
|
|
162
|
+
? !(targetGoal?.skipAuditor ?? loadGoalSettings(ctx.cwd).disabled)
|
|
163
|
+
: !loadGoalSettings(ctx.cwd).disabled;
|
|
157
164
|
activeDrafts.set(core, { mode, originalTopic: trimmed, targetGoalId: targetGoal?.id, startedAt, auditorEnabled });
|
|
158
165
|
draftSessionEntry(core, { version: 1, mode, seed: trimmed, targetGoalId: targetGoal?.id, startedAt, auditorEnabled });
|
|
159
166
|
core.clearContinuationState();
|
|
@@ -305,7 +312,9 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
305
312
|
const draft = activeDraft(core);
|
|
306
313
|
if (!draft) return { content: [{ type: "text", text: "No guided goal draft is active. Do not create a goal without the user starting /goal or /sisyphus." }], details: goalDetails(core.state.goal) };
|
|
307
314
|
const objective = params.objective.trim();
|
|
308
|
-
if (!objective
|
|
315
|
+
if (!objective) return { content: [{ type: "text", text: "The proposed objective must be at least 1 character." }], details: goalDetails(core.state.goal) };
|
|
316
|
+
const objectiveMaxChars = loadGoalSettings(ctx.cwd).objectiveMaxChars ?? 0;
|
|
317
|
+
if (objectiveMaxChars > 0 && objective.length > objectiveMaxChars) return { content: [{ type: "text", text: `The proposed objective must be at most ${objectiveMaxChars} characters (${objective.length} given).` }], details: goalDetails(core.state.goal) };
|
|
309
318
|
const expectedSisyphus = draft.mode === "sisyphus";
|
|
310
319
|
if (draft.mode !== "tweak" && ((params.sisyphus === true) !== expectedSisyphus)) return { content: [{ type: "text", text: "Proposal mode does not match the command that began this draft." }], details: goalDetails(core.state.goal) };
|
|
311
320
|
const taskResult = proposedTaskList(core, ctx, params.tasks as FlatTaskInput[] | undefined, params.block_completion);
|
|
@@ -389,6 +398,11 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
389
398
|
// keep status/evidence/completedAt/skippedAt/skipReason, new ids start
|
|
390
399
|
// pending, removed ids drop. currentTaskId survives only while its task
|
|
391
400
|
// is still pending in the merged tree.
|
|
401
|
+
// §tweak-resume: a confirmed tweak is a deliberate revision of a
|
|
402
|
+
// stalled goal — set when the goal actually transitions out of
|
|
403
|
+
// paused/blocked so the post-apply glue can resume accounting,
|
|
404
|
+
// continuation, and the ledger event.
|
|
405
|
+
let resumed = false;
|
|
392
406
|
const result = core.goalService.apply(ctx, {
|
|
393
407
|
reconcile: false, focusToken: token, refreshFromDisk: true,
|
|
394
408
|
mutate: (goal) => {
|
|
@@ -400,13 +414,43 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
400
414
|
const currentTaskId = mergedTaskList && goal.currentTaskId && !currentTaskIdIsPending(mergedTaskList.tasks, goal.currentTaskId)
|
|
401
415
|
? undefined
|
|
402
416
|
: goal.currentTaskId;
|
|
403
|
-
|
|
417
|
+
// §tweak-resume: resume a paused/blocked goal (mirror the
|
|
418
|
+
// /goal-resume transition: status active + pause metadata
|
|
419
|
+
// cleared); budget_limited stays behind its hard resource
|
|
420
|
+
// gate and an active goal stays active.
|
|
421
|
+
const stalled = goal.status === "paused" || goal.status === "blocked";
|
|
422
|
+
if (stalled) resumed = true;
|
|
423
|
+
return {
|
|
424
|
+
...goal,
|
|
425
|
+
objective: extracted.objective,
|
|
426
|
+
verificationContract: extracted.verificationContract ?? goal.verificationContract,
|
|
427
|
+
taskList, currentTaskId, skipAuditor,
|
|
428
|
+
status: stalled ? "active" : goal.status,
|
|
429
|
+
autoContinue: stalled ? true : goal.autoContinue,
|
|
430
|
+
stopReason: stalled ? undefined : goal.stopReason,
|
|
431
|
+
pauseReason: stalled ? undefined : goal.pauseReason,
|
|
432
|
+
pauseSuggestedAction: stalled ? undefined : goal.pauseSuggestedAction,
|
|
433
|
+
updatedAt: now,
|
|
434
|
+
};
|
|
404
435
|
},
|
|
405
436
|
ledger: (written) => [{ type: "goal_tweaked", goalId: written.id, changeSummary: "Goal revised through /goal-tweak drafting.", at: written.updatedAt }, ...(taskResult.value ? [{ type: "task_list_set" as const, goalId: written.id, taskCount: countTasks(written.taskList?.tasks), blockCompletion: taskResult.value.blockCompletion, at: written.updatedAt }] : [])],
|
|
406
437
|
});
|
|
407
438
|
if (!result.ok) return { content: [{ type: "text", text: "Goal tweak was not applied: " + result.message }], details: goalDetails(core.state.goal) };
|
|
439
|
+
if (resumed) {
|
|
440
|
+
try {
|
|
441
|
+
core.goalService.appendEvents(ctx, [{ type: "goal_resumed", goalId: result.goal.id, reason: "tweak", at: nowIso() }]);
|
|
442
|
+
} catch {
|
|
443
|
+
// Ledger append failure should not crash the tweak.
|
|
444
|
+
}
|
|
445
|
+
}
|
|
408
446
|
core.clearContinuationState();
|
|
409
447
|
core.updateUI(ctx);
|
|
448
|
+
if (resumed) {
|
|
449
|
+
// Resume glue mirrors replaceGoal: restart accounting and queue
|
|
450
|
+
// the auto-continuation so the revived goal keeps going.
|
|
451
|
+
core.beginAccounting();
|
|
452
|
+
core.queueContinuation(ctx, true);
|
|
453
|
+
}
|
|
410
454
|
clearGoalDrafting(core, ctx);
|
|
411
455
|
return { content: [{ type: "text", text: `${summary}\n\nGoal tweak confirmed and applied.` }], details: goalDetails(result.goal), terminate: true };
|
|
412
456
|
},
|
|
@@ -11,6 +11,7 @@ export type GoalLedgerEvent =
|
|
|
11
11
|
| { type: "goal_paused"; goalId: string; reason: string; suggestedAction?: string; status?: "paused"; source?: "user" | "agent"; at: string }
|
|
12
12
|
| { type: "goal_resumed"; goalId: string; reason: string; at: string }
|
|
13
13
|
| { type: "goal_tweaked"; goalId: string; changeSummary: string; at: string }
|
|
14
|
+
| { type: "auditor_toggled"; goalId: string; enabled: boolean; at: string }
|
|
14
15
|
| { type: "completion_requested"; goalId: string; summary?: string; at: string }
|
|
15
16
|
| { type: "audit_started"; goalId: string; provider?: string; model?: string; thinkingLevel?: string; at: string }
|
|
16
17
|
| { type: "audit_result"; goalId: string; verdict: "approved" | "disapproved" | "error"; report: string; at: string }
|
|
@@ -224,6 +225,8 @@ function isValidLedgerEvent(value: unknown): value is GoalLedgerEvent {
|
|
|
224
225
|
return typeof obj.goalId === "string" && typeof obj.reason === "string";
|
|
225
226
|
case "goal_tweaked":
|
|
226
227
|
return typeof obj.goalId === "string" && typeof obj.changeSummary === "string";
|
|
228
|
+
case "auditor_toggled":
|
|
229
|
+
return typeof obj.goalId === "string" && typeof obj.enabled === "boolean";
|
|
227
230
|
case "completion_requested":
|
|
228
231
|
return typeof obj.goalId === "string" && (obj.summary === undefined || typeof obj.summary === "string");
|
|
229
232
|
case "audit_started":
|
|
@@ -275,6 +278,8 @@ function sanitizeEvent(event: GoalLedgerEvent): GoalLedgerEvent {
|
|
|
275
278
|
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
276
279
|
case "goal_tweaked":
|
|
277
280
|
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
281
|
+
case "auditor_toggled":
|
|
282
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
278
283
|
case "completion_requested":
|
|
279
284
|
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
280
285
|
case "audit_started":
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Reads `.pi/pi-goal-x-settings.json` with env var overrides:
|
|
5
5
|
* PI_GOAL_DISABLE_TASKS — "true" to disable, any other value = use file config
|
|
6
6
|
* PI_GOAL_DISABLE_CONTRACTS — "true" to disable, any other value = use file config
|
|
7
|
+
* PI_GOAL_OBJECTIVE_MAX_CHARS — objective length cap (0 = no limit), overrides file
|
|
7
8
|
* PI_GOAL_SETTINGS_FILE — alternative settings file path (relative to cwd or absolute)
|
|
8
9
|
*
|
|
9
10
|
* The file may contain:
|
|
10
11
|
* disableTasks, disableContracts, subtaskDepth,
|
|
11
|
-
* provider, model, thinkingLevel, disabled
|
|
12
|
+
* provider, model, thinkingLevel, disabled, objectiveMaxChars
|
|
12
13
|
*
|
|
13
14
|
* additionalProperties: false — unknown keys are rejected.
|
|
14
15
|
*/
|
|
@@ -31,6 +32,12 @@ export interface GoalSettings {
|
|
|
31
32
|
auditorProjectResources?: boolean;
|
|
32
33
|
/** F5: stall detector timeout in minutes (0 = off). */
|
|
33
34
|
stallTimeoutMinutes?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Maximum objective length in characters (0/unset = no limit, the
|
|
37
|
+
* default; >0 caps objectives in create_goal, propose_goal_draft, and
|
|
38
|
+
* /goal-tweak).
|
|
39
|
+
*/
|
|
40
|
+
objectiveMaxChars?: number;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export const PI_GOAL_SETTINGS_FILE_ENV = "PI_GOAL_SETTINGS_FILE";
|
|
@@ -76,6 +83,7 @@ const ALLOWED_SETTINGS_KEYS = new Set([
|
|
|
76
83
|
"autoSelectSingleGoal",
|
|
77
84
|
"auditorProjectResources",
|
|
78
85
|
"stallTimeoutMinutes",
|
|
86
|
+
"objectiveMaxChars",
|
|
79
87
|
]);
|
|
80
88
|
|
|
81
89
|
/**
|
|
@@ -110,6 +118,16 @@ function asPositiveInt(value: unknown): number | undefined {
|
|
|
110
118
|
return undefined;
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
/** Positive-integer-or-zero parser (for settings where 0 = off / no limit). */
|
|
122
|
+
function asNonNegativeInt(value: unknown): number | undefined {
|
|
123
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
const n = parseInt(value, 10);
|
|
126
|
+
if (!isNaN(n) && n >= 0) return n;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
function asThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
114
132
|
const text = asNonEmptyString(value);
|
|
115
133
|
return text && THINKING_LEVELS.has(text) ? text as ThinkingLevel : undefined;
|
|
@@ -144,6 +162,8 @@ export function parseGoalSettings(raw: unknown): GoalSettings {
|
|
|
144
162
|
if (record.auditorProjectResources === true || record.auditorProjectResources === "true") settings.auditorProjectResources = true;
|
|
145
163
|
const stallTimeoutMinutes = asPositiveInt(record.stallTimeoutMinutes);
|
|
146
164
|
if (stallTimeoutMinutes !== undefined) settings.stallTimeoutMinutes = stallTimeoutMinutes;
|
|
165
|
+
const objectiveMaxChars = asNonNegativeInt(record.objectiveMaxChars);
|
|
166
|
+
if (objectiveMaxChars !== undefined) settings.objectiveMaxChars = objectiveMaxChars;
|
|
147
167
|
return settings;
|
|
148
168
|
}
|
|
149
169
|
|
|
@@ -186,6 +206,7 @@ export function loadGoalSettings(cwd: string, env: NodeJS.ProcessEnv = process.e
|
|
|
186
206
|
autoSelectSingleGoal: fileConfig.autoSelectSingleGoal ?? false,
|
|
187
207
|
auditorProjectResources: fileConfig.auditorProjectResources ?? false,
|
|
188
208
|
stallTimeoutMinutes: fileConfig.stallTimeoutMinutes,
|
|
209
|
+
objectiveMaxChars: asNonNegativeInt(env.PI_GOAL_OBJECTIVE_MAX_CHARS) ?? fileConfig.objectiveMaxChars,
|
|
189
210
|
};
|
|
190
211
|
}
|
|
191
212
|
|
|
@@ -204,6 +225,7 @@ export function loadGoalSettings(cwd: string, env: NodeJS.ProcessEnv = process.e
|
|
|
204
225
|
export function envOverrideFor(key: keyof GoalSettings | "settingsFile", env: NodeJS.ProcessEnv = process.env): string | null {
|
|
205
226
|
if (key === "disableTasks" && env.PI_GOAL_DISABLE_TASKS !== undefined) return "PI_GOAL_DISABLE_TASKS";
|
|
206
227
|
if (key === "disableContracts" && env.PI_GOAL_DISABLE_CONTRACTS !== undefined) return "PI_GOAL_DISABLE_CONTRACTS";
|
|
228
|
+
if (key === "objectiveMaxChars" && env.PI_GOAL_OBJECTIVE_MAX_CHARS !== undefined) return "PI_GOAL_OBJECTIVE_MAX_CHARS";
|
|
207
229
|
if (key === "settingsFile" && env[PI_GOAL_SETTINGS_FILE_ENV] !== undefined) return PI_GOAL_SETTINGS_FILE_ENV;
|
|
208
230
|
return null;
|
|
209
231
|
}
|
|
@@ -227,6 +249,7 @@ export function effectiveSettingsReport(cwd: string, env: NodeJS.ProcessEnv = pr
|
|
|
227
249
|
{ key: "thinkingLevel", label: "thinking_level", format: (v) => v.thinkingLevel ?? "(default)" },
|
|
228
250
|
{ key: "auditorProjectResources", label: "auditor project resources", format: (v) => (v.auditorProjectResources === true ? "true" : "false") },
|
|
229
251
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", format: (v) => String(v.stallTimeoutMinutes ?? 0) },
|
|
252
|
+
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", format: (v) => String(v.objectiveMaxChars ?? 0) },
|
|
230
253
|
];
|
|
231
254
|
for (const row of rows) {
|
|
232
255
|
const envVar = envOverrideFor(row.key, env);
|
|
@@ -262,6 +285,7 @@ export function saveGoalSettingsFileConfig(cwd: string, settings: GoalSettings):
|
|
|
262
285
|
if (settings.autoSelectSingleGoal === true) clean.autoSelectSingleGoal = true;
|
|
263
286
|
if (settings.auditorProjectResources === true) clean.auditorProjectResources = true;
|
|
264
287
|
if (settings.stallTimeoutMinutes !== undefined) clean.stallTimeoutMinutes = settings.stallTimeoutMinutes;
|
|
288
|
+
if (settings.objectiveMaxChars !== undefined) clean.objectiveMaxChars = settings.objectiveMaxChars;
|
|
265
289
|
const configPath = goalSettingsPath(cwd);
|
|
266
290
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
267
291
|
settingsFileCache.delete(configPath);
|
|
@@ -276,6 +300,7 @@ export function saveGoalSettingsFileConfig(cwd: string, settings: GoalSettings):
|
|
|
276
300
|
if (settings.autoSelectSingleGoal === true) persisted.autoSelectSingleGoal = true;
|
|
277
301
|
if (settings.auditorProjectResources === true) persisted.auditorProjectResources = true;
|
|
278
302
|
if (clean.stallTimeoutMinutes !== undefined) persisted.stallTimeoutMinutes = clean.stallTimeoutMinutes;
|
|
303
|
+
if (clean.objectiveMaxChars !== undefined) persisted.objectiveMaxChars = clean.objectiveMaxChars;
|
|
279
304
|
fs.writeFileSync(configPath, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
|
|
280
305
|
return clean;
|
|
281
306
|
}
|
package/extensions/goal-state.ts
CHANGED
|
@@ -121,6 +121,8 @@ export interface GoalCore {
|
|
|
121
121
|
archiveCurrentGoal(ctx: ExtensionContext, reason: StopReason | undefined): GoalRecord | null;
|
|
122
122
|
stopActiveGoal(status: Exclude<GoalStatus, "active">, reason: StopReason | undefined, ctx: ExtensionContext): void;
|
|
123
123
|
pauseActiveGoal(ctx: ExtensionContext): void;
|
|
124
|
+
/** §auditor-toggle: flip the focused goal's persisted per-goal skipAuditor and record the ledger event. */
|
|
125
|
+
toggleGoalAuditor(ctx: ExtensionContext): void;
|
|
124
126
|
queueContinuation(ctx: ExtensionContext, force?: boolean): void;
|
|
125
127
|
flushGoalTransaction(ctx: ExtensionContext): void;
|
|
126
128
|
replaceGoal(config: GoalCreationConfig, ctx: ExtensionContext, startNow?: boolean, verificationContract?: string, tokenBudget?: number): void;
|
|
@@ -810,6 +812,38 @@ export function createGoalCore(
|
|
|
810
812
|
ctx.ui.notify("Goal paused.", "info");
|
|
811
813
|
}
|
|
812
814
|
|
|
815
|
+
/**
|
|
816
|
+
* §auditor-toggle: flip the focused goal's independent-auditor setting.
|
|
817
|
+
* Persisted per-goal (revision-safe via goalService.apply), recorded as an
|
|
818
|
+
* auditor_toggled ledger event, reflected in the dashboard, and announced.
|
|
819
|
+
* Inert when no goal is focused or the goal is complete; the widget's
|
|
820
|
+
* modal-depth guard keeps it inert while a goal modal is open.
|
|
821
|
+
*/
|
|
822
|
+
function toggleGoalAuditor(ctx: ExtensionContext): void {
|
|
823
|
+
if (!state.goal) {
|
|
824
|
+
ctx.ui.notify("No focused goal to toggle the auditor for.", "info");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (state.goal.status === "complete") {
|
|
828
|
+
ctx.ui.notify("This goal is complete; the auditor no longer applies.", "info");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const nextEnabled = state.goal.skipAuditor === true;
|
|
832
|
+
const result = goalService.apply(ctx, {
|
|
833
|
+
reconcile: false,
|
|
834
|
+
refreshFromDisk: true,
|
|
835
|
+
mutate: (g) => ({ ...g, skipAuditor: g.skipAuditor === true ? undefined : true, updatedAt: nowIso() }),
|
|
836
|
+
ledger: (written) => [{ type: "auditor_toggled" as const, goalId: written.id, enabled: nextEnabled, at: written.updatedAt }],
|
|
837
|
+
});
|
|
838
|
+
if (!result.ok) {
|
|
839
|
+
ctx.ui.notify("Could not toggle the auditor: " + result.message, "error");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
goalService.flushTurn(ctx); // P1-3: user-visible setting change persists now, not at turn end
|
|
843
|
+
updateUI(ctx);
|
|
844
|
+
ctx.ui.notify(nextEnabled ? "Auditor enabled for this goal." : "Auditor disabled for this goal.", "info");
|
|
845
|
+
}
|
|
846
|
+
|
|
813
847
|
function flushGoalTransaction(ctx: ExtensionContext): void {
|
|
814
848
|
goalService.flushTurn(ctx);
|
|
815
849
|
}
|
|
@@ -994,6 +1028,7 @@ export function createGoalCore(
|
|
|
994
1028
|
archiveCurrentGoal,
|
|
995
1029
|
stopActiveGoal,
|
|
996
1030
|
pauseActiveGoal,
|
|
1031
|
+
toggleGoalAuditor,
|
|
997
1032
|
queueContinuation,
|
|
998
1033
|
flushGoalTransaction,
|
|
999
1034
|
touchGoalActivity,
|
|
@@ -162,6 +162,16 @@ export function syncTerminalInputPause(core: GoalCore, ctx: ExtensionContext): v
|
|
|
162
162
|
return { consume: true };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// Ctrl+Shift+A — toggle the focused goal's independent auditor.
|
|
166
|
+
// Persisted per-goal (revision-safe), ledger event, dashboard
|
|
167
|
+
// refresh, notification; inert with no focused goal or a complete
|
|
168
|
+
// goal (the modal-depth guard above covers goal modals).
|
|
169
|
+
if (matchesKey(data, "ctrl+shift+a")) {
|
|
170
|
+
core.toggleGoalAuditor(ctx);
|
|
171
|
+
core.goalWidgetComponentRef.current?.invalidate();
|
|
172
|
+
return { consume: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
// Navigation keys: plain arrows scroll the expanded dashboard (it is
|
|
166
176
|
// modal while open); Ctrl+Shift+↑/↓/PgUp/PgDn/Home/End scroll the
|
|
167
177
|
// compact task list — free chords, consumed only when the compact
|
|
@@ -34,6 +34,8 @@ export interface GoalDashboardModel {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
focused: boolean;
|
|
37
|
+
/** §auditor-toggle: whether the independent completion auditor is enabled for this goal (derived from the goal's persisted skipAuditor; undefined = on). */
|
|
38
|
+
auditorEnabled: boolean;
|
|
37
39
|
filePath?: string;
|
|
38
40
|
|
|
39
41
|
usage: {
|
|
@@ -498,6 +500,7 @@ export function deriveGoalDashboardModel(
|
|
|
498
500
|
title: displayObjectiveTitle(goal.objective),
|
|
499
501
|
status,
|
|
500
502
|
focused,
|
|
503
|
+
auditorEnabled: goal.skipAuditor !== true,
|
|
501
504
|
filePath: goal.activePath ?? goal.archivedPath,
|
|
502
505
|
usage: {
|
|
503
506
|
activeSeconds: goal.usage.activeSeconds,
|
|
@@ -96,16 +96,20 @@ function boxSectionRule(theme: Theme, width: number, label: string, right = ""):
|
|
|
96
96
|
return `${frame(theme, "├")}${l}${frame(theme, H.repeat(fill))}${r}${frame(theme, "┤")}`;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function boxFooter(theme: Theme, width: number, content: string): string {
|
|
99
|
+
function boxFooter(theme: Theme, width: number, content: string, right = ""): string {
|
|
100
100
|
const inner = Math.max(4, width - 2);
|
|
101
|
-
|
|
101
|
+
// Optional right slot mirrors boxHeader's right-meta treatment (` ● ─`
|
|
102
|
+
// before the corner), so the auditor dot sits at the bottom-right of the
|
|
103
|
+
// border. The dot survives; the hint (left content) truncates when tight.
|
|
104
|
+
const r = right ? ` ${right} ${frame(theme, H)}` : "";
|
|
105
|
+
if (!content && !r) {
|
|
102
106
|
return frame(theme, `╰${H.repeat(inner)}╯`);
|
|
103
107
|
}
|
|
104
108
|
// The footer is one frame tone: leading dash, hint text and trailing fill
|
|
105
109
|
// all carry the frame color, so the blue-gray spans the whole line.
|
|
106
|
-
const l = frame(theme, `${H} ${fit(content, inner - 4)}`);
|
|
107
|
-
const fill = Math.max(1, inner - visibleWidth(l));
|
|
108
|
-
return `${frame(theme, "╰")}${l}${frame(theme, H.repeat(fill))}${frame(theme, "╯")}`;
|
|
110
|
+
const l = content ? frame(theme, `${H} ${fit(content, Math.max(0, inner - 4 - visibleWidth(r)))}`) : "";
|
|
111
|
+
const fill = Math.max(1, inner - visibleWidth(l) - visibleWidth(r));
|
|
112
|
+
return `${frame(theme, "╰")}${l}${frame(theme, H.repeat(fill))}${r}${frame(theme, "╯")}`;
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
// ── §5.2 status colors ──────────────────────────────────────────────────────
|
|
@@ -311,6 +315,10 @@ export function renderCompactDashboard(
|
|
|
311
315
|
lines.push(boxLine(theme, safeWidth, `${theme.fg(fuel as RenderColor, "⛽")} ${muted(theme, "Budget")} ${theme.fg(fuel as RenderColor, formatBudget(model.budget.used, model.budget.total))}`));
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
// §auditor-toggle: the focused goal's independent-auditor status is the
|
|
319
|
+
// bottom-right border dot (green on / muted gray off, see the footer
|
|
320
|
+
// below); the Ctrl+Shift+A chord is advertised in wide/medium footers.
|
|
321
|
+
|
|
314
322
|
// §9.1 compact task counts + progress bars live in the Tasks header row
|
|
315
323
|
// (below); the standalone progress lines were removed in favor of it.
|
|
316
324
|
|
|
@@ -388,15 +396,21 @@ export function renderCompactDashboard(
|
|
|
388
396
|
lines.push(boxLine(theme, safeWidth, `${dim(theme, `File ${model.filePath}`)}`));
|
|
389
397
|
}
|
|
390
398
|
|
|
391
|
-
const footerHint = opts.footerHint
|
|
399
|
+
const footerHint = (opts.footerHint
|
|
392
400
|
?? (listOverflows
|
|
393
401
|
? mode === "minimal"
|
|
394
402
|
? "↑↓: scroll"
|
|
395
403
|
: mode === "narrow"
|
|
396
404
|
? "Ctrl+Shift+↑↓: scroll"
|
|
397
405
|
: "Ctrl+Shift+T: expand · Ctrl+Shift+↑↓: scroll"
|
|
398
|
-
: spec.footerHint)
|
|
399
|
-
|
|
406
|
+
: spec.footerHint))
|
|
407
|
+
// §auditor-toggle: wide/medium footers also advertise the Ctrl+Shift+A
|
|
408
|
+
// toggle; narrow/minimal keep only the border dot.
|
|
409
|
+
+ (mode === "wide" || mode === "medium" ? " · Ctrl+Shift+A" : "");
|
|
410
|
+
// The auditor dot lives in the bottom-right of the border: green when the
|
|
411
|
+
// focused goal's independent auditor is on, muted gray when off.
|
|
412
|
+
const auditorDot = model.auditorEnabled ? success(theme, "●") : muted(theme, "●");
|
|
413
|
+
lines.push(boxFooter(theme, safeWidth, footerHint, auditorDot));
|
|
400
414
|
return lines;
|
|
401
415
|
}
|
|
402
416
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.3",
|
|
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",
|
|
@@ -26,8 +26,14 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"extensions",
|
|
28
28
|
"README.md",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"docs/agent-flow-design.md",
|
|
31
|
+
"docs/agentic-runtime-prd.md",
|
|
32
|
+
"docs/architecture.md",
|
|
33
|
+
"docs/goal-ts-refactor-test-strategy.md",
|
|
34
|
+
"docs/pi-autoresearch-survey.md",
|
|
35
|
+
"docs/pi-goal-x-unified-dashboard-plan.md",
|
|
36
|
+
"docs/unified-dashboard.md"
|
|
31
37
|
],
|
|
32
38
|
"pi": {
|
|
33
39
|
"extensions": [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":1,"dirMtimeMs":1786087947377.8738,"goals":[]}
|