@quantmind-br/pi-goal-x 0.0.0-stage → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/assets/badge-dark.svg +18 -0
  4. package/assets/badge-light.svg +18 -0
  5. package/docs/advanced-usage.md +106 -0
  6. package/docs/agent-flow-design.md +306 -0
  7. package/docs/agentic-runtime-prd.md +773 -0
  8. package/docs/architecture.md +368 -0
  9. package/docs/controls.md +140 -0
  10. package/docs/pi-autoresearch-survey.md +50 -0
  11. package/docs/pi-goal-x-unified-dashboard-plan.md +1667 -0
  12. package/docs/prompt-caching.md +31 -0
  13. package/docs/unified-dashboard.md +286 -0
  14. package/extensions/auditor-selector.ts +74 -0
  15. package/extensions/goal-accounting.ts +107 -0
  16. package/extensions/goal-activity.ts +202 -0
  17. package/extensions/goal-auditor.ts +480 -0
  18. package/extensions/goal-commands.ts +866 -0
  19. package/extensions/goal-compaction.ts +197 -0
  20. package/extensions/goal-completion.ts +173 -0
  21. package/extensions/goal-context.ts +12 -0
  22. package/extensions/goal-contract.ts +58 -0
  23. package/extensions/goal-controls.ts +202 -0
  24. package/extensions/goal-core-tools.ts +549 -0
  25. package/extensions/goal-core.ts +78 -0
  26. package/extensions/goal-detail.ts +65 -0
  27. package/extensions/goal-dialog-controls.ts +31 -0
  28. package/extensions/goal-draft.ts +244 -0
  29. package/extensions/goal-drafting.ts +535 -0
  30. package/extensions/goal-events.ts +692 -0
  31. package/extensions/goal-format.ts +376 -0
  32. package/extensions/goal-interruption.ts +53 -0
  33. package/extensions/goal-ledger-index.ts +68 -0
  34. package/extensions/goal-ledger.ts +1007 -0
  35. package/extensions/goal-live-retention.ts +239 -0
  36. package/extensions/goal-oracle.ts +346 -0
  37. package/extensions/goal-policy.ts +370 -0
  38. package/extensions/goal-pool.ts +103 -0
  39. package/extensions/goal-prompt-cache.ts +80 -0
  40. package/extensions/goal-questionnaire.ts +1073 -0
  41. package/extensions/goal-record.ts +388 -0
  42. package/extensions/goal-recovery.ts +256 -0
  43. package/extensions/goal-runtime.ts +277 -0
  44. package/extensions/goal-scheduler-state.ts +100 -0
  45. package/extensions/goal-scheduler.ts +436 -0
  46. package/extensions/goal-service.ts +843 -0
  47. package/extensions/goal-session-health.ts +130 -0
  48. package/extensions/goal-session-safety.ts +65 -0
  49. package/extensions/goal-settings.ts +1370 -0
  50. package/extensions/goal-state.ts +1069 -0
  51. package/extensions/goal-status.ts +305 -0
  52. package/extensions/goal-task-confirmation.ts +174 -0
  53. package/extensions/goal-task-count.ts +59 -0
  54. package/extensions/goal-task-derive.ts +46 -0
  55. package/extensions/goal-task-index.ts +57 -0
  56. package/extensions/goal-task-tools.ts +595 -0
  57. package/extensions/goal-tool-inputs.ts +102 -0
  58. package/extensions/goal-tool-names.ts +73 -0
  59. package/extensions/goal-tools.ts +16 -0
  60. package/extensions/goal-widget.ts +455 -0
  61. package/extensions/goal.ts +42 -0
  62. package/extensions/network-error-backoff.ts +55 -0
  63. package/extensions/prompts/goal-prompts.ts +330 -0
  64. package/extensions/storage/atomic-rename.ts +52 -0
  65. package/extensions/storage/goal-files.ts +695 -0
  66. package/extensions/storage/goal-lock.ts +131 -0
  67. package/extensions/storage/goal-root.ts +95 -0
  68. package/extensions/widgets/auditor-dashboard-model.ts +174 -0
  69. package/extensions/widgets/dialog-scaffold.ts +26 -0
  70. package/extensions/widgets/goal-controls-dialog.ts +329 -0
  71. package/extensions/widgets/goal-dashboard-model.ts +530 -0
  72. package/extensions/widgets/goal-dashboard-renderer.ts +674 -0
  73. package/extensions/widgets/goal-escape-dialog.ts +148 -0
  74. package/extensions/widgets/goal-notifications.ts +9 -0
  75. package/extensions/widgets/goal-widget.ts +723 -0
  76. package/extensions/widgets/task-list-overlay.ts +409 -0
  77. package/extensions/widgets/text-cache.ts +30 -0
  78. package/package.json +117 -4
  79. package/scripts/recover-session-checkpoints.mjs +289 -0
@@ -0,0 +1,31 @@
1
+ # Prompt caching
2
+
3
+ pi-goal-x preserves the provider's reusable prompt prefix during normal user turns, autonomous checkpoints, and tool loops. No extra extension setting is required. Pi's provider configuration still determines whether caching is enabled, its retention period, and model support.
4
+
5
+ The request layout is:
6
+
7
+ 1. Host system prompt and tool schemas.
8
+ 2. Conversation history, with each goal checkpoint normalized to a tiny, stable trigger in its original position.
9
+ 3. Request-only goal policy, then a task snapshot, then compact counter/context snapshots, replayed at verified history anchors when safe. Current policy includes the objective, lifecycle rules, the task gate, limits, scheduling state and applicable recovery/auditor reminders. The task snapshot carries task progress: the `[TASK LIST]` header with completed/total counts, the current task with its contract, pending tasks, and the count of omitted pending tasks.
10
+
11
+ Changing goal state never rewrites the system prompt or historical messages. The full goal-state message is not persisted into the session. Old checkpoints remain until Pi compacts history; retaining small markers avoids shifting the prefix on every continuation. Display-only auditor messages are consistently excluded, and tool calls stay adjacent to their results.
12
+
13
+ Goal context filtering and live-state tails use Pi's `context_with_system` event, preserving system and tool deltas in their original positions. Parent and delegated sessions use this hook from their first request.
14
+
15
+ For Anthropic-style explicit caching, the extension moves Pi's existing cache_control breakpoint from the transient state message onto the preceding cacheable history block. The Bedrock equivalent moves the cachePoint. Both preserve the configured TTL and do not add breakpoints. Payloads without an existing explicit marker—including caching disabled and implicit-cache providers—receive no marker changes. The extension does not set cache keys, enable paid extended retention, or replace provider defaults.
16
+
17
+ Goal and drafting tools share a fixed, idempotent profile: goal progress, focus, lifecycle changes, and draft start/confirm/cancel do not change tool declarations or their prompt rules. Drafting tools reject calls without an active user-started draft. While a draft is active, an execution guard blocks the five ordinary goal tools until confirmation or cancellation. Only changes to disableTasks alter this extension's tool selection and legitimately rebuild the cache. Host tools remain untouched. Drafting messages are appended to conversation history. Isolated auditor and Oracle sessions already use fixed instructions/tool profiles and place goal-specific evidence in their user messages; their tool loops retain history. They have separate sessions and use Pi's cache defaults. Opting these sessions into project resources also opts into any behavior from those resources.
18
+
19
+ Cache reuse can still be interrupted by Pi compaction, branch/session changes, model/provider switches, other extensions changing earlier content, changed tool schemas, provider expiry, minimum-length or breakpoint-lookback limits, or routing. The extension cannot guarantee a hit rate or a particular cost reduction. The supported host range is Pi 1.4+ within 1.x, with Node 22.19 or newer. Development dependencies pin Pi 1.4.0.
20
+
21
+ Pi 1.4 prepares custom-triggered turns through the same public startup protocol as user prompts. The extension leaves structured prompt and tool deltas intact, including additional sections supplied by other extensions. Direct creation, drafting, resume and reload use the host's native prompt preparation from the first request. Explicit tool-profile changes can still rebuild the prefix.
22
+
23
+ Validation includes consecutive request-prefix regressions; actual Anthropic, Anthropic-compatible Chat Completions, OpenAI Chat Completions and Responses serialization with short/long/disabled retention; Bedrock payload-shape checks; lifecycle/tool-pair safety tests; and full SDK parent/auditor/Oracle payload captures. These tests stop before HTTP dispatch. They verify request construction, not live provider hit rates.
24
+
25
+ Provider references: [Anthropic prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) describes exact-prefix matching and explicit breakpoints; [DeepSeek context caching](https://api-docs.deepseek.com/guides/kv_cache/) describes its automatic prefix cache and usage counters.
26
+
27
+ Active requests split policy, task snapshots and compact counter/context snapshots into separate tails, always in that order. Already-sent tails are replayed at verified history anchors so implicit-cache requests extend their previous prefix. Each kind is appended only when it differs from its previous copy, so starting, completing or skipping a task, or changing the current task, appends one task snapshot instead of resetting retention (#108 measured four prefix breaks across one three-task run; now only the task setup that enables the completion gate resets). With `blockCompletion` enabled, the policy carries a fixed TASK GATE line that defers to the latest task snapshot, so the pending count never changes policy. Policy changes (including budget/run-limit removal and enabling or disabling the task gate), dropping the task list, rewritten history and unsafe tool-result boundaries reset retention. At most 32 tails, 16 KiB of task snapshots and 4 KiB of counter text are retained per session, across at most 16 session/model identities; exceeding a bound performs one full reset. No full-state tails are persisted. Unchanged requests add no tail. Older task and counter snapshots are historical: the latest snapshot supersedes earlier ones, and current policy takes precedence over cache reuse.
28
+
29
+ Explicit-marker relocation skips all retained live text and tolerates Pi's trailing effort-only messages. Another extension's content-bearing suffix is left alone. Changing models/providers starts a separate retention identity. Unknown session identities do not retain history between requests.
30
+
31
+ Validation now includes advancing SDK request chains, combined effort-marker/retention regressions, 1,000-turn growth bounds and a 64 KiB–4 MiB history-cost gate. These are request-construction and CPU measurements, not live provider hit-rate claims.
@@ -0,0 +1,286 @@
1
+ # Unified Dashboard
2
+
3
+ `pi-goal-x` renders one dashboard component in two presentation modes. The
4
+ above-editor widget, `/goal-status`, and the completion flow all derive from
5
+ the same presentation model (`extensions/widgets/goal-dashboard-model.ts`),
6
+ so they can never drift apart on data or terminology.
7
+
8
+ - **Compact mode** — a persistent summary above the editor while a goal is
9
+ focused.
10
+ - **Expanded mode** — the full dashboard: task tree, current-task details,
11
+ verification, and recent activity. This replaces the former task-list
12
+ overlay; `Ctrl+Shift+T` now toggles between compact and expanded instead of
13
+ opening a separate overlay.
14
+
15
+ Everything shown is derived from persisted goal state and the durable ledger:
16
+ progress, the current task, verification status, the audit result, and recent
17
+ activity are never fabricated for display.
18
+
19
+ ## Compact mode
20
+
21
+ Always visible above the editor while a goal is focused:
22
+
23
+ ```text
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: toggle auditor ● ─╯
36
+ ```
37
+
38
+ Compact rows, in order:
39
+
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.
44
+ 3. Token budget (when configured): `⛽ Budget 18.2K / 50K · 36%`.
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
56
+ amber** (`mdHeading`) with colour-coded markers and ids (✓ complete muted
57
+ green, ▸ current teal, ~ skipped gray, · pending amber), an aligned id
58
+ column, truncated amber titles, and a `… +N more` overflow line when the
59
+ list is longer than the row budget (5 rows at wide, 4 medium, 3 narrow, 2
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.
66
+ 6. Current task: `Current t3 · Add the download button` (id and title in the
67
+ accent color).
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,
72
+ shortcut hint and trailing fill — is drawn in one frame tone (the
73
+ neutral-gray `muted`), so the color spans the full width with no two-tone
74
+ split; the header line is likewise one tone with only the `pi-goal-x`
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. In wide/medium
78
+ layouts the footer also right-aligns a `Ctrl+Shift+A: toggle auditor`
79
+ note beside the dot, making explicit that the shortcut turns the auditor
80
+ on and off; narrow and minimal keep just the dot. `Ctrl+Shift+A` toggles
81
+ the auditor per-goal (persisted, `auditor_toggled` ledger event,
82
+ dashboard refresh, notification; inert with no focused goal, a goal modal
83
+ open, or a complete goal). The expanded dashboard does not render the
84
+ auditor indicator.
85
+
86
+ When every top-level task is done, the current line reads
87
+ `Current All tasks complete`. With `disableTasks` enabled the task rows are
88
+ omitted entirely.
89
+
90
+ ## Expanded mode
91
+
92
+ `Ctrl+Shift+T` expands the same component:
93
+
94
+ ```text
95
+ ╭─ pi-goal-x ─ Add CSV export to reports ─────────────── 12m47s · 18.2K tok ─╮
96
+ │ Status: ● In progress · Focused: yes · Other goals: 2 │
97
+ │ File: .pi/goals/active_goal_... │
98
+ ├─ Progress ──────────────────────────────────────────────────────────────────┤
99
+ │ [██████░░░░] 3/5 tasks · 60% │
100
+ ├─ Tasks ─────────────────────────────────────────────────────────────────────┤
101
+ │ ✓ t1 Review reports page and data source │
102
+ │ ✓ t2 Implement filtered CSV export │
103
+ │ ▸ t3 Add the download button ☑ │
104
+ │ ✓ t3.1 Add loading state │
105
+ │ ✓ t3.2 Generate timestamped filename │
106
+ │ · t3.3 Add error handling │
107
+ │ · t4 Add documentation │
108
+ │ ~ t5 Add and run tests │
109
+ ├─ Current task ──────────────────────────────────────────────────────────────┤
110
+ │ t3 · Add the download button │
111
+ │ Subtasks [███████░░░] 2/3 · 67% │
112
+ │ Contract: The button downloads a CSV using the active filters. │
113
+ ├─ Verification ──────────────────────────────────────────────────────────────┤
114
+ │ Run npm test with zero failures. │
115
+ ├─ Recent activity ───────────────────────────────────────────────────────────┤
116
+ │ ✓ Completed “Implement filtered CSV export”. — Done │
117
+ │ ▸ Started “Add error handling”. │
118
+ ╰─ Esc/Ctrl+Shift+T: collapse ────────────────────────────────────────────────╯
119
+ ```
120
+
121
+ Task markers (§9.2): `✓` complete, `~` skipped, `▸` current, `·` pending.
122
+ A `☑` suffix marks a task that carries its own verification contract; the full
123
+ contract is shown in the current-task block.
124
+
125
+ When no persisted current task exists, the dashboard falls back to the first
126
+ pending task for display and marks it as inferred — the fallback is never
127
+ persisted.
128
+
129
+ ## Scrolling the task list
130
+
131
+ Both views show the task list as a **window** over the plan-ordered list, and
132
+ by default the window is **anchored to the most recently completed task** —
133
+ the viewport is "scrolled down" so recent completions (by `completedAt`) are
134
+ visible instead of always showing the earliest tasks. With nothing completed
135
+ the window starts at the top; when a new task completes the window re-anchors
136
+ to it. `↑ N more tasks` / `… +N more tasks` indicator rows mark rows hidden
137
+ above and below the window.
138
+
139
+ pi has no separate focus — the prompt editor owns the plain arrow keys — so
140
+ the two views scroll differently:
141
+
142
+ | View | Key | Action |
143
+ | --- | --- | --- |
144
+ | Expanded (modal) | `↑` / `↓` | Scroll one task row |
145
+ | Expanded (modal) | `PgUp` / `PgDn` | Scroll one page of task rows |
146
+ | Expanded (modal) | `Home` / `End` | Jump to the top / bottom of the list |
147
+ | Compact (inline) | `Ctrl+Shift+↑` / `Ctrl+Shift+↓` | Scroll one task row |
148
+ | Compact (inline) | `Ctrl+Shift+PgUp` / `Ctrl+Shift+PgDn` | Scroll one page |
149
+ | Compact (inline) | `Ctrl+Shift+Home` / `Ctrl+Shift+End` | Jump to the top / bottom |
150
+
151
+ While the expanded dashboard is open it is modal and owns the plain arrow
152
+ keys (like the session tree or model selector); `Esc` collapses it. The
153
+ compact widget never touches the editor's arrows — it scrolls with the
154
+ `Ctrl+Shift` chords, which pi leaves unbound, so typing and navigation are
155
+ never hijacked. The chords are consumed only when the compact list actually
156
+ overflows its viewport; on a short list they pass through unused. When the
157
+ list overflows, the compact footer advertises the chords
158
+ (`Ctrl+Shift+↑↓: scroll`). `/goal-status` renders the same anchored window as
159
+ a static snapshot (it is not interactive). The window and its indicators are
160
+ width-safe at every layout width.
161
+
162
+ ## Status states
163
+
164
+ | Symbol | Status | Shown for |
165
+ | --- | --- | --- |
166
+ | `●` | In progress | active goal with auto-continue on |
167
+ | `○` | Idle | active goal with auto-continue off |
168
+ | `◐` | Paused (user/agent) | paused goal, with reason |
169
+ | `⊘` | Blocked | blocked goal, with blocker and suggested action |
170
+ | `⛽` | Budget limited | goal at its token budget, with usage detail |
171
+ | `✓` | Complete | completed goal, before/after archival |
172
+
173
+ Status labels are always explicit words, so the dashboard stays readable
174
+ without the symbols.
175
+
176
+ ## Unfocused state
177
+
178
+ When open goals exist but none is focused:
179
+
180
+ ```text
181
+ ╭─ pi-goal-x ─ Goal focus required ───────────────────────────────────────────╮
182
+ │ 3 open goals are available. │
183
+ │ Run /goal-focus to choose the goal for this session. │
184
+ ╰─────────────────────────────────────────────────────────────────────────────╯
185
+ ```
186
+
187
+ ## Audit view
188
+
189
+ During an independent completion audit the widget switches to a structured
190
+ audit dashboard with the same visual system:
191
+
192
+ ```text
193
+ ╭─ Independent completion audit ─ anthropic/claude-sonnet:high ─── 2m18s ─╮
194
+ │ ✓ Objective and success criteria │
195
+ │ ✓ Verification contracts │
196
+ │ ✓ Tasks and recorded evidence │
197
+ │ ◌ Workspace inspection │
198
+ │ · Final decision │
199
+ │ [███████░░░] 72% │
200
+ ╰─ Esc: stop audit ─────────────────────────────────────────────────────────╯
201
+ ```
202
+
203
+ Raw auditor tools and output are hidden by default; they appear only in
204
+ expanded/debug audit mode or when the audit failed and diagnostics are needed.
205
+
206
+ After the audit, a result card shows the outcome:
207
+
208
+ ```text
209
+ ╭─ Audit result ─ APPROVED ─────────────────────────────────────────────────╮
210
+ │ ✓ Objective satisfied. │
211
+ │ ✓ Verification requirements satisfied. │
212
+ │ ✓ Required tasks and evidence accepted. │
213
+ ╰────────────────────────────────────────────────────────────────────────────╯
214
+ ```
215
+
216
+ A rejected audit keeps the goal open, shows `CHANGES REQUIRED` with the
217
+ auditor's findings, and returns to the normal dashboard so work can continue.
218
+
219
+ ## Keybindings
220
+
221
+ | Key | Action |
222
+ | --- | --- |
223
+ | `Ctrl+Shift+T` | Toggle the dashboard between compact and expanded |
224
+ | `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) |
225
+ | `↑` / `↓`, `PgUp` / `PgDn`, `Home` / `End` | Scroll the expanded task tree (see [Scrolling the task list](#scrolling-the-task-list)) |
226
+ | `Ctrl+Shift+↑` / `Ctrl+Shift+↓`, `Ctrl+Shift+PgUp` / `Ctrl+Shift+PgDn`, `Ctrl+Shift+Home` / `Ctrl+Shift+End` | Scroll the compact task list |
227
+ | `Esc` | Collapse the expanded dashboard; otherwise pause the goal |
228
+ | `Esc` (during audit) | Stop the audit and choose to continue working or complete without audit |
229
+
230
+ Configure the three task shortcuts with `keybindings.dashboard.toggleExpand`,
231
+ `keybindings.dashboard.scrollUp`, and `keybindings.dashboard.scrollDown` in
232
+ `.pi/pi-goal-x-settings.json`. The defaults are `ctrl+shift+t`,
233
+ `ctrl+shift+up`, and `ctrl+shift+down`.
234
+
235
+ ## Width behavior
236
+
237
+ Layout modes (§5.5):
238
+
239
+ - **Wide (≥100 columns):** full border, multiple fields per line, full task
240
+ titles, wider progress bars, current-task details.
241
+ - **Medium (70–99):** mostly one field per line, shorter bars, truncated
242
+ paths and contracts.
243
+ - **Narrow (50–69):** compact border, short labels, reduced metadata, one
244
+ task line at a time.
245
+ - **Very narrow (<50):** the essential summary only — status, task progress,
246
+ the current task, and the verification contract.
247
+
248
+ The renderer never emits a line wider than the terminal width: all alignment
249
+ and truncation is visible-width aware (ANSI colors, Unicode, and double-width
250
+ characters included). Golden tests assert `visibleWidth(line) <= width` for
251
+ every rendered line at 40, 50, 60, 80, 100, and 140 columns.
252
+
253
+ ## `/goal-status`
254
+
255
+ `/goal-status` renders the same model as the widget.
256
+
257
+ - Standard mode: a static compact-dashboard rendering, current-task details,
258
+ recent activity, and the last audit result. No effective-settings noise.
259
+ - `/goal-status verbose`: goal id, revision, full objective, the complete
260
+ task tree with full evidence and contracts, recent ledger history, token
261
+ budget detail, pause/blocker detail, active and archived paths, the last
262
+ audit report, and effective settings with provenance.
263
+
264
+ ## Migration behavior
265
+
266
+ The current-task focus is a new **optional** persisted field
267
+ (`currentTaskId`) on goal records.
268
+
269
+ - Legacy goal files load without it and are **never rewritten** just because
270
+ the field is absent.
271
+ - A persisted `currentTaskId` is accepted only when it references an existing
272
+ *pending* task; it is cleared when that task is completed, skipped, or
273
+ removed during normalization.
274
+ - Task-list restructuring preserves the current task only while its id
275
+ remains pending; otherwise the focus clears and the dashboard recomputes.
276
+ - For display only, the dashboard may infer the first pending task as the
277
+ current task and marks it as inferred.
278
+
279
+ ## Compatibility
280
+
281
+ - Existing goal-file formats, archived goals, settings, slash commands, and
282
+ direct-goal behavior are preserved.
283
+ - The task-overlay shortcut is retained but now expands the unified
284
+ dashboard; the separate overlay registration is removed.
285
+ - Headless behavior remains functional without TUI rendering, and
286
+ audit-disabled completion remains explicit and distinct from approval.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Auditor model/thinking selector helpers (ll01 cb6760b pattern).
3
+ *
4
+ * `/goal-settings` provider/model rows open a searchable selector: the
5
+ * current-session/default choice (which clears the explicit provider/model),
6
+ * authenticated models from the model registry (with a ✓ marker on the exact
7
+ * current selection), and an advanced manual `provider/model` entry. The
8
+ * thinking-level row offers Pi's seven levels or `(default)`.
9
+ */
10
+
11
+ import { GOAL_THINKING_LEVELS } from "./goal-settings.ts";
12
+
13
+ export type AuditorModelSummary = { provider: string; id: string; name?: string };
14
+
15
+ export type AuditorChoice =
16
+ | { kind: "default"; label: string }
17
+ | { kind: "model"; provider: string; model: string; label: string }
18
+ | { kind: "manual"; label: string };
19
+
20
+ export const AUDITOR_THINKING_LEVELS = GOAL_THINKING_LEVELS;
21
+
22
+ export function auditorModelLabel(model: AuditorModelSummary): string {
23
+ return `${model.provider}/${model.id}${model.name && model.name !== model.id ? ` — ${model.name}` : ""}`;
24
+ }
25
+
26
+ /** `${provider}/${model}` key for the current configured model, if explicit. */
27
+ export function configuredAuditorModelKey(config: { provider?: string; model?: string }): string | undefined {
28
+ return config.provider && config.model ? `${config.provider}/${config.model}` : undefined;
29
+ }
30
+
31
+ export function buildAuditorModelChoices(
32
+ available: AuditorModelSummary[],
33
+ configured: string | undefined,
34
+ session: string | undefined,
35
+ ): AuditorChoice[] {
36
+ const models = available.slice().sort((a, b) => auditorModelLabel(a).localeCompare(auditorModelLabel(b)));
37
+ return [
38
+ {
39
+ kind: "default",
40
+ label: `${configured ? " " : session ? "✓ " : " "}Current session / default${session ? ` (${session})` : " (system default)"}`,
41
+ },
42
+ ...models.map((model) => {
43
+ const key = `${model.provider}/${model.id}`;
44
+ return {
45
+ kind: "model" as const,
46
+ provider: model.provider,
47
+ model: model.id,
48
+ label: `${configured === key ? "✓ " : " "}${auditorModelLabel(model)}`,
49
+ };
50
+ }),
51
+ { kind: "manual", label: "✎ Enter provider/model manually (advanced)" },
52
+ ];
53
+ }
54
+
55
+ export function filterAuditorModelChoices(choices: AuditorChoice[], filter: string): AuditorChoice[] {
56
+ const normalized = filter.trim().toLowerCase();
57
+ if (!normalized) return choices;
58
+ return choices.filter(
59
+ (choice) => choice.kind === "default" || choice.kind === "manual" || choice.label.toLowerCase().includes(normalized),
60
+ );
61
+ }
62
+
63
+ export function parseManualAuditorModel(input: string): { provider: string; model: string } | { error: string } {
64
+ const value = input.trim();
65
+ const slash = value.indexOf("/");
66
+ if (slash <= 0 || slash === value.length - 1) {
67
+ return { error: "Auditor model must use provider/model format; unavailable models will fail clearly at audit time." };
68
+ }
69
+ return { provider: value.slice(0, slash), model: value.slice(slash + 1) };
70
+ }
71
+
72
+ export function thinkingLevelChoices(current: string | undefined): string[] {
73
+ return ["(default)", ...AUDITOR_THINKING_LEVELS].map((level) => (level === current ? `✓ ${level}` : ` ${level}`));
74
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * GoalAccounting — serialized, idempotent token/time accounting.
3
+ *
4
+ * One instance per extension. `begin(goalId)` starts a session for an active
5
+ * goal; `charge()` computes the elapsed seconds since the last charge and the
6
+ * completed-turn tokens, then advances the baseline so repeated calls never
7
+ * double-charge the same wall-clock interval. `liveSeconds()` is a read-only
8
+ * display helper that does not mutate state.
9
+ *
10
+ * Also exposes the token-budget helpers used by the runtime to detect the
11
+ * one-time `budget_limited` transition.
12
+ */
13
+
14
+ export interface AccountingCharge {
15
+ tokens: number;
16
+ seconds: number;
17
+ }
18
+
19
+ export interface BudgetLike {
20
+ tokenBudget?: number;
21
+ usage: { tokensUsed: number };
22
+ }
23
+
24
+ export class GoalAccounting {
25
+ private activeGoalId: string | null = null;
26
+ private lastAccountedAt: number | null = null;
27
+
28
+ /** Start accounting for a goal id (or clear when null). */
29
+ begin(goalId: string | null): void {
30
+ this.activeGoalId = goalId;
31
+ this.lastAccountedAt = goalId ? Date.now() : null;
32
+ }
33
+
34
+ clear(): void {
35
+ this.activeGoalId = null;
36
+ this.lastAccountedAt = null;
37
+ }
38
+
39
+ get goalId(): string | null {
40
+ return this.activeGoalId;
41
+ }
42
+
43
+ /** True when accounting is active for exactly this goal. */
44
+ isActiveFor(goalId: string): boolean {
45
+ return this.activeGoalId === goalId && this.lastAccountedAt !== null;
46
+ }
47
+
48
+ /**
49
+ * Serialized idempotent charge: returns the tokens + whole elapsed seconds
50
+ * accumulated since the previous charge, then advances the baseline to
51
+ * `now`. Calling again immediately afterwards yields 0 seconds (never
52
+ * double-charges the same interval).
53
+ */
54
+ charge(opts: { now?: number; completedTurnTokens?: number } = {}): AccountingCharge {
55
+ const now = opts.now ?? Date.now();
56
+ const elapsedSeconds = this.lastAccountedAt === null
57
+ ? 0
58
+ : Math.max(0, Math.floor((now - this.lastAccountedAt) / 1000));
59
+ this.lastAccountedAt = now;
60
+ const tokens = Math.max(0, Math.trunc(opts.completedTurnTokens ?? 0));
61
+ return { tokens, seconds: elapsedSeconds };
62
+ }
63
+
64
+ /** Live elapsed whole seconds for display; does not advance the baseline. */
65
+ liveSeconds(now = Date.now()): number {
66
+ if (this.lastAccountedAt === null) return 0;
67
+ return Math.max(0, Math.floor((now - this.lastAccountedAt) / 1000));
68
+ }
69
+ }
70
+
71
+ /** Remaining token budget, or null when no budget is set. */
72
+ export function budgetRemaining(goal: BudgetLike): number | null {
73
+ if (typeof goal.tokenBudget !== "number" || !Number.isFinite(goal.tokenBudget) || goal.tokenBudget <= 0) {
74
+ return null;
75
+ }
76
+ const used = Math.max(0, Math.floor(goal.usage.tokensUsed));
77
+ return Math.max(0, Math.floor(goal.tokenBudget - used));
78
+ }
79
+
80
+ /** True when a budget is set and accounted usage has reached it. */
81
+ export function budgetReached(goal: BudgetLike): boolean {
82
+ const remaining = budgetRemaining(goal);
83
+ return remaining !== null && remaining === 0;
84
+ }
85
+
86
+ /** Token-budget description for display/steering text. */
87
+ export function budgetLine(goal: BudgetLike): string | null {
88
+ const remaining = budgetRemaining(goal);
89
+ if (remaining === null) return null;
90
+ const used = Math.floor(goal.usage.tokensUsed);
91
+ return `token budget: ${used}/${Math.floor(goal.tokenBudget ?? 0)} used, ${remaining} remaining`;
92
+ }
93
+
94
+ /** Agent-facing accounting must never read as current context occupancy. */
95
+ export function modelBudgetLine(goal: BudgetLike): string | null {
96
+ const line = budgetLine(goal);
97
+ return line ? `Lifetime ${line} (spending cap, not context capacity)` : null;
98
+ }
99
+
100
+ export interface GoalContextUsage { tokens: number | null; contextWindow: number }
101
+
102
+ /** A point-in-time observation; older retained observations are historical. */
103
+ export function contextUsageLine(usage?: GoalContextUsage): string {
104
+ if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens) || usage.tokens < 0
105
+ || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) return "Context snapshot: unavailable";
106
+ return `Context snapshot: ${Math.round(usage.tokens)}/${Math.round(usage.contextWindow)} tokens (${Math.round(100 * usage.tokens / usage.contextWindow)}%)`;
107
+ }