pi-goal-x 0.25.1 → 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 +10 -8
- package/docs/unified-dashboard.md +47 -33
- 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 +41 -4
- package/extensions/goal-widget.ts +10 -0
- package/extensions/widgets/goal-dashboard-model.ts +35 -12
- package/extensions/widgets/goal-dashboard-renderer.ts +96 -67
- package/package.json +9 -3
- package/docs/.pi/goals/active_goal_2026080612411949_mshg3jex-svqfyt.md +0 -86
- package/docs/.pi/goals/active_goal_2026080613564540_mshisjml-bl1gar.md +0 -82
- 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_2026080614173417_mshjewld-9z3e9p.md +0 -72
- package/docs/.pi/goals/goal_events.jsonl +0 -80
package/README.md
CHANGED
|
@@ -249,22 +249,24 @@ Verify that the generated report contains every required section.
|
|
|
249
249
|
Always visible above the editor while a goal is focused:
|
|
250
250
|
|
|
251
251
|
```text
|
|
252
|
-
╭─ pi-goal-x ─ Add CSV export to reports
|
|
253
|
-
│
|
|
254
|
-
|
|
255
|
-
├─ Tasks ────────────────────────────────────────────────────────────┤
|
|
252
|
+
╭─ pi-goal-x ─ Add CSV export to reports ────────────────────────────╮
|
|
253
|
+
│ goal: running [12m47s 18.2K] (+2 open) │
|
|
254
|
+
├─ Tasks · ✓3 done · 2 open ──────────── [█████░░░] · Sub 2/3 [██░░] ┤
|
|
256
255
|
│ ✓ t1 Review reports page and data source │
|
|
257
256
|
│ ✓ t2 Implement filtered CSV export │
|
|
258
|
-
│ ▸ t3 Add the download button
|
|
257
|
+
│ ▸ t3 Add the download button ☑ ▸ 2/3 │
|
|
259
258
|
│ · t4 Add documentation │
|
|
260
259
|
│ … +1 more task │
|
|
261
260
|
│ Current t3 · Add the download button │
|
|
262
|
-
│ Subtasks [███████░░░] 2/3 · 67% │
|
|
263
261
|
│ Verify Run npm test with zero failures. │
|
|
264
262
|
│ File .pi/goals/active_goal_... │
|
|
265
|
-
╰─ Ctrl+Shift+T: expand tasks
|
|
263
|
+
╰─ Ctrl+Shift+T: expand tasks · Ctrl+Shift+A ──────────────────── ● ─╯
|
|
266
264
|
```
|
|
267
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
|
+
|
|
268
270
|
### Expanded mode
|
|
269
271
|
|
|
270
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.
|
|
@@ -353,7 +355,7 @@ Settings are stored in:
|
|
|
353
355
|
.pi/pi-goal-x-settings.json
|
|
354
356
|
```
|
|
355
357
|
|
|
356
|
-
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`.
|
|
357
359
|
|
|
358
360
|
## License
|
|
359
361
|
|
|
@@ -21,53 +21,66 @@ activity are never fabricated for display.
|
|
|
21
21
|
Always visible above the editor while a goal is focused:
|
|
22
22
|
|
|
23
23
|
```text
|
|
24
|
-
╭─ pi-goal-x ─ Add CSV export to reports
|
|
25
|
-
│
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
│ ✓
|
|
29
|
-
│
|
|
30
|
-
│
|
|
31
|
-
│
|
|
32
|
-
│
|
|
33
|
-
│
|
|
34
|
-
│
|
|
35
|
-
|
|
36
|
-
│ File .pi/goals/active_goal_g1.md │
|
|
37
|
-
╰─ Ctrl+Shift+T: expand tasks─────────────────────────────────────────────╯
|
|
24
|
+
╭─ pi-goal-x ─ Add CSV export to reports ────────────────────────────────────────────────╮
|
|
25
|
+
│ goal: running [12m47s 18.2K] (+2 open) │
|
|
26
|
+
├─ Tasks · ✓3 done · 2 open ─────────────────────────────────── [████░░] · Sub 2/3 [██░] ┤
|
|
27
|
+
│ ✓ t1 Review reports page and data source │
|
|
28
|
+
│ ✓ t2 Implement filtered CSV export │
|
|
29
|
+
│ ▸ t3 Add the download button ☑ ▸ 2/3 │
|
|
30
|
+
│ · t4 Add documentation │
|
|
31
|
+
│ … +1 more task │
|
|
32
|
+
│ Current t3 · Add the download button │
|
|
33
|
+
│ Verify Run npm test with zero failures. │
|
|
34
|
+
│ File .pi/goals/active_goal_g1.md │
|
|
35
|
+
╰─ Ctrl+Shift+T: expand tasks · Ctrl+Shift+A ──────────────────────────────────────── ● ─╯
|
|
38
36
|
```
|
|
39
37
|
|
|
40
38
|
Compact rows, in order:
|
|
41
39
|
|
|
42
|
-
1. Header (rounded corners): title
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
1. Header (rounded corners): title. (Usage lives in the status line below,
|
|
41
|
+
not in the header.)
|
|
42
|
+
2. Status: `goal: <status> [<elapsed> <tokens>] (+N open)` — the same
|
|
43
|
+
terminology as the pi footer status line.
|
|
45
44
|
3. Token budget (when configured): `⛽ Budget 18.2K / 50K · 36%`.
|
|
46
|
-
4.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
4. Tasks header row: `Tasks · ✓N done · M open` with the compact task
|
|
46
|
+
progress bar, then the current task's subtask bar beside it
|
|
47
|
+
(` · Sub done/total ` + bar — the word `Sub` is dropped in narrow mode so
|
|
48
|
+
the full counts still fit at 50 columns, and the segment is omitted
|
|
49
|
+
entirely in minimal mode, where the counts and task bar alone use all
|
|
50
|
+
available space). `N` counts top-level tasks complete **or skipped** — a
|
|
51
|
+
skipped top-level task counts as done (§9.1) — and `M` counts pending
|
|
52
|
+
tasks. The subtask segment appears only when the current task has direct
|
|
53
|
+
children. The whole header row is neutral gray (label, counts, bar fill,
|
|
54
|
+
and empty cells) so nothing in the progress info is colourful.
|
|
55
|
+
6. Task list section: the top-level tasks shown by default in **pastel
|
|
50
56
|
amber** (`mdHeading`) with colour-coded markers and ids (✓ complete muted
|
|
51
57
|
green, ▸ current teal, ~ skipped gray, · pending amber), an aligned id
|
|
52
58
|
column, truncated amber titles, and a `… +N more` overflow line when the
|
|
53
59
|
list is longer than the row budget (5 rows at wide, 4 medium, 3 narrow, 2
|
|
54
|
-
minimal).
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
minimal). Tasks that have direct subtasks show a muted `▸ done/total`
|
|
61
|
+
marker at the end of their row (leaf tasks show none). The current task
|
|
62
|
+
is fully highlighted in the theme's accent color (marker, id, title). The
|
|
63
|
+
whole box chrome — top/bottom frame, left/right edges, corners and
|
|
64
|
+
interior rules — is drawn in one tone: the theme's neutral gray
|
|
65
|
+
(`muted`). No blue tinge.
|
|
58
66
|
6. Current task: `Current t3 · Add the download button` (id and title in the
|
|
59
67
|
accent color).
|
|
60
|
-
7.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
9. Blocked or paused detail (reason, suggested action).
|
|
65
|
-
10. Active (or archived) goal file path.
|
|
66
|
-
11. Footer with the expansion shortcut. The whole footer line — leading dash,
|
|
68
|
+
7. Goal-level verification contract (first line, truncated).
|
|
69
|
+
8. Blocked or paused detail (reason, suggested action).
|
|
70
|
+
9. Active (or archived) goal file path.
|
|
71
|
+
10. Footer with the expansion shortcut. The whole footer line — leading dash,
|
|
67
72
|
shortcut hint and trailing fill — is drawn in one frame tone (the
|
|
68
73
|
neutral-gray `muted`), so the color spans the full width with no two-tone
|
|
69
74
|
split; the header line is likewise one tone with only the `pi-goal-x`
|
|
70
|
-
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.
|
|
71
84
|
|
|
72
85
|
When every top-level task is done, the current line reads
|
|
73
86
|
`Current All tasks complete`. With `disableTasks` enabled the task rows are
|
|
@@ -207,6 +220,7 @@ auditor's findings, and returns to the normal dashboard so work can continue.
|
|
|
207
220
|
| Key | Action |
|
|
208
221
|
| --- | --- |
|
|
209
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) |
|
|
210
224
|
| `↑` / `↓`, `PgUp` / `PgDn`, `Home` / `End` | Scroll the expanded task tree (see [Scrolling the task list](#scrolling-the-task-list)) |
|
|
211
225
|
| `Ctrl+Shift+↑` / `Ctrl+Shift+↓`, `Ctrl+Shift+PgUp` / `Ctrl+Shift+PgDn`, `Ctrl+Shift+Home` / `Ctrl+Shift+End` | Scroll the compact task list |
|
|
212
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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type AgentToolResult, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { footerStatus } from "./goal-core.ts";
|
|
3
2
|
import { FOCUS_ENTRY, STATE_ENTRY, GOAL_EVENT_ENTRY, goalDetails } from "./goal-format.ts";
|
|
4
3
|
import { loadGoalSettings, loadGoalSettingsFileConfig } from "./goal-settings.ts";
|
|
5
4
|
import {
|
|
@@ -122,6 +121,8 @@ export interface GoalCore {
|
|
|
122
121
|
archiveCurrentGoal(ctx: ExtensionContext, reason: StopReason | undefined): GoalRecord | null;
|
|
123
122
|
stopActiveGoal(status: Exclude<GoalStatus, "active">, reason: StopReason | undefined, ctx: ExtensionContext): void;
|
|
124
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;
|
|
125
126
|
queueContinuation(ctx: ExtensionContext, force?: boolean): void;
|
|
126
127
|
flushGoalTransaction(ctx: ExtensionContext): void;
|
|
127
128
|
replaceGoal(config: GoalCreationConfig, ctx: ExtensionContext, startNow?: boolean, verificationContract?: string, tokenBudget?: number): void;
|
|
@@ -669,9 +670,12 @@ export function createGoalCore(
|
|
|
669
670
|
return;
|
|
670
671
|
}
|
|
671
672
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
673
|
+
// The goal widget is the single home for goal status: the status line
|
|
674
|
+
// lives in the compact/expanded dashboard (goal: <label> [<usage>]
|
|
675
|
+
// (+N open)), so the focused-goal footer segment is cleared — a stale
|
|
676
|
+
// unfocused hint or earlier status must not linger once a goal is
|
|
677
|
+
// focused. The unfocused branch above still sets its own hint.
|
|
678
|
+
ctx.ui.setStatus("goal", undefined);
|
|
675
679
|
|
|
676
680
|
if (!widgetRegistered) {
|
|
677
681
|
ctx.ui.setWidget(
|
|
@@ -808,6 +812,38 @@ export function createGoalCore(
|
|
|
808
812
|
ctx.ui.notify("Goal paused.", "info");
|
|
809
813
|
}
|
|
810
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
|
+
|
|
811
847
|
function flushGoalTransaction(ctx: ExtensionContext): void {
|
|
812
848
|
goalService.flushTurn(ctx);
|
|
813
849
|
}
|
|
@@ -992,6 +1028,7 @@ export function createGoalCore(
|
|
|
992
1028
|
archiveCurrentGoal,
|
|
993
1029
|
stopActiveGoal,
|
|
994
1030
|
pauseActiveGoal,
|
|
1031
|
+
toggleGoalAuditor,
|
|
995
1032
|
queueContinuation,
|
|
996
1033
|
flushGoalTransaction,
|
|
997
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
|