pi-goal-x 0.32.3 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/advanced-usage.md +7 -1
- package/docs/architecture.md +22 -0
- package/docs/controls.md +140 -0
- package/docs/prompt-caching.md +3 -1
- package/extensions/auditor-selector.ts +4 -2
- package/extensions/goal-auditor.ts +2 -2
- package/extensions/goal-commands.ts +85 -37
- package/extensions/goal-controls.ts +202 -0
- package/extensions/goal-core-tools.ts +6 -5
- package/extensions/goal-dialog-controls.ts +31 -0
- package/extensions/goal-draft.ts +4 -3
- package/extensions/goal-drafting.ts +10 -9
- package/extensions/goal-events.ts +57 -13
- package/extensions/goal-format.ts +3 -2
- package/extensions/goal-interruption.ts +53 -0
- package/extensions/goal-prompt-profile.ts +77 -0
- package/extensions/goal-questionnaire.ts +33 -29
- package/extensions/goal-recovery.ts +5 -2
- package/extensions/goal-scheduler-state.ts +17 -5
- package/extensions/goal-scheduler.ts +89 -13
- package/extensions/goal-session-safety.ts +21 -8
- package/extensions/goal-settings.ts +112 -79
- package/extensions/goal-state.ts +19 -9
- package/extensions/goal-status.ts +10 -3
- package/extensions/goal-task-confirmation.ts +7 -6
- package/extensions/goal-tool-names.ts +2 -4
- package/extensions/goal-widget.ts +51 -78
- package/extensions/prompts/goal-prompts.ts +7 -6
- package/extensions/widgets/goal-controls-dialog.ts +329 -0
- package/extensions/widgets/goal-dashboard-model.ts +9 -2
- package/extensions/widgets/goal-dashboard-renderer.ts +15 -12
- package/extensions/widgets/goal-escape-dialog.ts +8 -7
- package/extensions/widgets/goal-widget.ts +17 -5
- package/package.json +17 -9
package/docs/advanced-usage.md
CHANGED
|
@@ -4,7 +4,13 @@ For installation, goal workflows, commands and the settings overview, see the [R
|
|
|
4
4
|
|
|
5
5
|
## Automatic continuation and optional execution contracts
|
|
6
6
|
|
|
7
|
-
Active goals continue automatically after successful executions, including reasoning-only responses and final-task verification. No tool call, task update, scheduling declaration
|
|
7
|
+
Active goals continue automatically after successful executions, including reasoning-only responses and final-task verification. No tool call, task update, or scheduling declaration is required.
|
|
8
|
+
|
|
9
|
+
Default continuation is immediate. To slow repeated replies without recognized work, enable **slow repeated idle replies (opt-in)** under Advanced execution in `/goal-settings` (`idleContinuationBackoff: true`). Use the scope switch to choose a global default or a project override. An explicit project `false` overrides global `true`; removing the override restores inheritance.
|
|
10
|
+
|
|
11
|
+
When enabled, one execution without recognized work continues immediately. Further such executions wait 30, 60, 120, 240, then 300 seconds between attempts. Work observed anywhere in the execution or a change to the objective, requirements or tasks resets the delay. Goal inspection, echo-only calls, accounting and scheduling declarations alone do not reset it. This heuristic may delay reasoning or unfamiliar custom tools, and repeated recognized tool calls can bypass it. It reduces request rate; it does not guarantee progress or finite spending. Use a run allowance or token budget for a hard limit.
|
|
12
|
+
|
|
13
|
+
The delay survives reloads and shows a relative time, local clock time and resume command. Waiting spends no autonomous runs or active time. Turning the option off cancels a pending delay promptly without renewing spent runs or resuming a paused goal. Turning it on starts observing a subsequent execution, rather than classifying work retroactively. Old saved delay metadata has no effect without opt-in. `/goal-resume` renews the run allowance and continues now; incoming user/producer messages remain ordinary host work outside that allowance. Strict waits and provider-error recovery keep their separate timing.
|
|
8
14
|
|
|
9
15
|
Enable `strictExecutionContract: true` in `/goal-settings` or your global/project settings to require explicit ready/wait decisions. In that mode, a missing decision permits one repair prompt within the remaining allowance, then pauses. This is a user preference; agents should not enable it merely to continue.
|
|
10
16
|
|
package/docs/architecture.md
CHANGED
|
@@ -249,6 +249,28 @@ token/time intervals per turn; a goal never double-charges the same interval.
|
|
|
249
249
|
`GoalRuntime` (goal-runtime.ts) owns continuation scheduling, the stale
|
|
250
250
|
checkpoint state, the turn-stop guard, and one-shot steering reminders.
|
|
251
251
|
|
|
252
|
+
`GoalScheduler` persists dispatch intent and allowance consumption. Default
|
|
253
|
+
continuation is immediate. Explicit `idleContinuationBackoff=true` also records a
|
|
254
|
+
bounded idle streak and earliest next attempt: one immediate continuation, then
|
|
255
|
+
30 seconds doubling to five minutes. Work accumulates over the whole logical run;
|
|
256
|
+
content snapshots exclude usage, revisions and scheduler writes. Disabled pacing
|
|
257
|
+
skips those snapshots. Settings are rechecked at scheduling and admission; opting
|
|
258
|
+
out removes saved delay metadata without renewing consumption. Resume and genuine
|
|
259
|
+
host input reset pacing; strict waits and provider recovery remain independent.
|
|
260
|
+
|
|
261
|
+
`goal-prompt-profile.ts` detects Pi's native custom-turn startup protocol and
|
|
262
|
+
leaves its structured prompt/tool deltas intact on Pi 1.2 and newer. On older
|
|
263
|
+
affected structured hosts, it projects the current public prompt and resolved
|
|
264
|
+
tools through `context_with_system`, or the full `context` event where necessary.
|
|
265
|
+
That fallback fixes the first request after direct creation, drafting, resume or
|
|
266
|
+
reload without changing history or tool/result pairs. Classic hosts keep their
|
|
267
|
+
native path. Current `agent_settled.aborted` events stop continuation/recovery and
|
|
268
|
+
pause the associated goal, including cancellation before a provider request.
|
|
269
|
+
|
|
270
|
+
Audit display messages flush only at idle settlement and remain scoped to their
|
|
271
|
+
session. If Pi has invalidated a settlement context, the handler skips it before
|
|
272
|
+
consuming continuation intent or changing the replacement session's queue.
|
|
273
|
+
|
|
252
274
|
An optional `token_budget` may be set at creation. When accounted usage
|
|
253
275
|
reaches the budget, `accountProgress` transitions the goal to the distinct
|
|
254
276
|
`budget_limited` status exactly once (status leaves `active`, so accounting
|
package/docs/controls.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Commands and keys
|
|
2
|
+
|
|
3
|
+
Open `/goal-settings` and choose **Controls**, or run `/goal-settings controls`.
|
|
4
|
+
The screen has three sections: **Commands**, **Keys** and **Pausing**. Type to
|
|
5
|
+
filter, use the arrow keys and Enter to edit, and Tab to switch sections.
|
|
6
|
+
Page Up/Down moves through long lists; Home/End jumps to the first/last row.
|
|
7
|
+
Ctrl+S switches between **This project** and **All projects**. The selected row shows
|
|
8
|
+
whether its value comes from the project, global settings or the default.
|
|
9
|
+
|
|
10
|
+
Changes are saved when you choose Save or confirm a recorded key. Escape backs
|
|
11
|
+
out of an edit without saving. **Use inherited value** removes a project
|
|
12
|
+
override; **Remove global override** restores the default. **Use default** pins
|
|
13
|
+
the default in the selected scope. Each section also has a reset action that
|
|
14
|
+
removes only that section's overrides in the selected scope.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
Each action has a preferred command followed by any aliases. Choose **Edit
|
|
19
|
+
command paths** to replace them, **Add an alias** to keep the current spellings,
|
|
20
|
+
or **Disable** to remove its command invocations. For example, add `/goal edit`
|
|
21
|
+
to **Revise the goal** to use the existing confirmation workflow with that
|
|
22
|
+
spelling. Arguments after the command path are passed through to the action.
|
|
23
|
+
Subcommands appear in argument completion; the longest matching complete path
|
|
24
|
+
wins. A routing-only root lists its available subcommands when called without
|
|
25
|
+
a matching action.
|
|
26
|
+
|
|
27
|
+
Command changes need a reload. The screen shows when changes are pending;
|
|
28
|
+
Ctrl+R applies them while Pi is idle when controls were opened through a slash
|
|
29
|
+
command. If controls were opened from a shortcut, close them and run `/reload`.
|
|
30
|
+
Keys and pause order take effect immediately.
|
|
31
|
+
|
|
32
|
+
`/goal-settings` always remains available as a recovery entry. You can give the
|
|
33
|
+
settings action other spellings, but this entry cannot be removed. Pi command
|
|
34
|
+
roots such as `/reload` and `/settings` are reserved. The editor checks conflicts
|
|
35
|
+
with other commands before saving.
|
|
36
|
+
For settings, **Recovery entry only** removes additional aliases while keeping
|
|
37
|
+
`/goal-settings` available.
|
|
38
|
+
|
|
39
|
+
The settings keys below are stable action IDs, even after renaming an invocation:
|
|
40
|
+
|
|
41
|
+
| Action ID | Action |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `goal` | Guided goal draft |
|
|
44
|
+
| `sisyphus` | Guided Sisyphus draft |
|
|
45
|
+
| `goal-direct` | Immediate goal creation |
|
|
46
|
+
| `sisyphus-direct` | Immediate Sisyphus creation |
|
|
47
|
+
| `goal-tweak` | Confirmed revision |
|
|
48
|
+
| `goal-pause` | Pause goal and stop turn |
|
|
49
|
+
| `goal-resume` | Resume and renew the run allowance |
|
|
50
|
+
| `goal-clear` | Confirmed archival |
|
|
51
|
+
| `goal-list` | List open goals |
|
|
52
|
+
| `goal-status` | Status; also accepts `verbose` or `health` |
|
|
53
|
+
| `goal-refresh` | Read state and settings again |
|
|
54
|
+
| `goal-recovery` | Recovery report; also accepts `repair` |
|
|
55
|
+
| `goal-focus` | Choose a goal |
|
|
56
|
+
| `goal-unfocus` | Detach this session |
|
|
57
|
+
| `goal-settings` | Settings and controls |
|
|
58
|
+
| `goal-cancel` | Cancel a draft |
|
|
59
|
+
|
|
60
|
+
## Keys
|
|
61
|
+
|
|
62
|
+
Choose an action under **Keys**, then record a key or edit its Pi key names.
|
|
63
|
+
**Record another key** adds a second binding. **Disable** leaves it unbound.
|
|
64
|
+
Conflicts are checked before saving; errors leave your input available to fix.
|
|
65
|
+
Use a free key or change a conflicting Pi binding in its `keybindings.json`.
|
|
66
|
+
|
|
67
|
+
Every public goal shortcut is listed: pause, audit interruption, auditor toggle,
|
|
68
|
+
dashboard expansion, compact scrolling, expanded navigation, and goal dialog
|
|
69
|
+
controls. Each command action can also have a shortcut, independently of its
|
|
70
|
+
slash command names. A shortcut for direct creation or revision asks for the
|
|
71
|
+
objective or change before invoking the usual workflow.
|
|
72
|
+
|
|
73
|
+
Goal dialog controls apply to questionnaires and confirmation dialogs. They
|
|
74
|
+
have their own context, so the same key can confirm a dialog and pause a live
|
|
75
|
+
goal. Text editing and the settings screen use Pi's usual UI controls. The
|
|
76
|
+
settings screen remains usable when you change goal dialog navigation.
|
|
77
|
+
While typing an answer, letters and Pi's editing keys keep editing text. Escape
|
|
78
|
+
leaves the answer editor; free custom keys such as F7 can do the same.
|
|
79
|
+
|
|
80
|
+
Pi key names include `escape`, `f6`, `ctrl+alt+p`, `pageUp` and `super+k`. Multiple
|
|
81
|
+
keys can be separated with commas. For a comma key combined with other bindings,
|
|
82
|
+
the text editor also accepts a JSON key list, for example `["ctrl+,", "f6"]`.
|
|
83
|
+
Existing comma-key lists reopen in this form so they can be saved unchanged.
|
|
84
|
+
Terminals must report a key before Pi can use it; some modifier combinations
|
|
85
|
+
require the Kitty keyboard protocol. See [Pi's keybinding reference](https://github.com/earendil-works/pi/blob/v1.4.0/packages/coding-agent/docs/keybindings.md).
|
|
86
|
+
|
|
87
|
+
## Pausing
|
|
88
|
+
|
|
89
|
+
The default is still Escape, with **Goal and turn together**. An open dialog
|
|
90
|
+
owns its input; closing the expanded dashboard or interrupting an audit takes
|
|
91
|
+
priority over pausing in that view.
|
|
92
|
+
|
|
93
|
+
| Pause order | First press | Next press |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| Goal and turn together | Pause the goal and stop the turn | Pi's normal interrupt |
|
|
96
|
+
| Goal first | Pause the goal; let the current turn finish | Stop the remaining turn |
|
|
97
|
+
| Turn first | Stop the turn and wait for the user, keeping the goal active | Pause the goal |
|
|
98
|
+
|
|
99
|
+
Goal first finishes the current Pi turn, then stops before another model
|
|
100
|
+
request. A turn-only stop holds automatic continuation, provider recovery and wake-ups.
|
|
101
|
+
It does not spend another run allowance. Resume or send a new user message to
|
|
102
|
+
continue. Reloading an interrupted autonomous run does not replay it.
|
|
103
|
+
|
|
104
|
+
When you move the pause key away from Pi's interrupt key, Pi's interrupt stops
|
|
105
|
+
the turn and waits for you without pausing the goal. The configured pause key
|
|
106
|
+
uses the selected order. The explicit **Pause the goal** command always stops
|
|
107
|
+
both the goal and its turn, including a turn left running by **Goal first**.
|
|
108
|
+
|
|
109
|
+
## Settings files
|
|
110
|
+
|
|
111
|
+
The UI writes the existing sparse settings layers:
|
|
112
|
+
|
|
113
|
+
- Global: `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-goal-x-settings.json`.
|
|
114
|
+
- Project: `<project>/.pi/pi-goal-x-settings.json`.
|
|
115
|
+
|
|
116
|
+
Project leaves override global leaves. Missing leaves inherit; an empty list
|
|
117
|
+
explicitly disables an action. Existing `keybindings.dashboard` settings keep
|
|
118
|
+
their meaning. For example:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"commands": {
|
|
123
|
+
"goal-tweak": ["goal-tweak", "goal edit"],
|
|
124
|
+
"goal-resume": ["work continue"]
|
|
125
|
+
},
|
|
126
|
+
"pauseOrder": "turn-first",
|
|
127
|
+
"keybindings": {
|
|
128
|
+
"pause": ["f6", "ctrl+alt+p"],
|
|
129
|
+
"toggleAuditor": "ctrl+alt+a",
|
|
130
|
+
"interruptAudit": "escape",
|
|
131
|
+
"dashboard": { "toggleExpand": "ctrl+alt+t" },
|
|
132
|
+
"dialog": { "confirm": "enter", "cancel": "escape" },
|
|
133
|
+
"commands": { "goal-settings": "f10", "goal-status": [] }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `/goal-refresh` after external key or pause-order edits, and `/reload` after
|
|
139
|
+
external command edits. Verbose goal status reports the effective controls and
|
|
140
|
+
their sources. Developer-only `PI_GOAL_DEBUG` shortcuts are unchanged.
|
package/docs/prompt-caching.md
CHANGED
|
@@ -14,7 +14,9 @@ For Anthropic-style explicit caching, the extension moves Pi's existing cache_co
|
|
|
14
14
|
|
|
15
15
|
Execution tools already use an idempotent profile: goal progress, focus, and lifecycle changes do not change schemas. Explicit switches into/out of drafting and changes to disableTasks can change tools and legitimately rebuild the cache. 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.
|
|
16
16
|
|
|
17
|
-
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
|
|
17
|
+
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 declared host range is in `package.json`; development dependencies pin Pi 1.4.0, which requires Node 22.19 or newer. Older supported hosts retain the extension's Node 22.15 floor.
|
|
18
|
+
|
|
19
|
+
Pi 1.2 and newer prepare custom-triggered turns through the same public startup protocol as user prompts. The extension leaves their structured prompt and tool deltas intact, including additional sections supplied by other extensions. On older affected structured hosts, goal requests use the public full-context boundary to project the current prompt and resolved tools from the first request after drafting, direct creation, resume or reload. That fallback leaves persisted history and conversation messages intact; earlier structured hosts expose the boundary through `context`, and classic hosts retain their native path. Explicit tool-profile changes can still rebuild the prefix.
|
|
18
20
|
|
|
19
21
|
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.
|
|
20
22
|
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* current-session/default choice (which clears the explicit provider/model),
|
|
6
6
|
* authenticated models from the model registry (with a ✓ marker on the exact
|
|
7
7
|
* current selection), and an advanced manual `provider/model` entry. The
|
|
8
|
-
* thinking-level row offers
|
|
8
|
+
* thinking-level row offers Pi's seven levels or `(default)`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { GOAL_THINKING_LEVELS } from "./goal-settings.ts";
|
|
12
|
+
|
|
11
13
|
export type AuditorModelSummary = { provider: string; id: string; name?: string };
|
|
12
14
|
|
|
13
15
|
export type AuditorChoice =
|
|
@@ -15,7 +17,7 @@ export type AuditorChoice =
|
|
|
15
17
|
| { kind: "model"; provider: string; model: string; label: string }
|
|
16
18
|
| { kind: "manual"; label: string };
|
|
17
19
|
|
|
18
|
-
export const AUDITOR_THINKING_LEVELS =
|
|
20
|
+
export const AUDITOR_THINKING_LEVELS = GOAL_THINKING_LEVELS;
|
|
19
21
|
|
|
20
22
|
export function auditorModelLabel(model: AuditorModelSummary): string {
|
|
21
23
|
return `${model.provider}/${model.id}${model.name && model.name !== model.id ? ` — ${model.name}` : ""}`;
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import type { GoalRecord, GoalTask, GoalTaskList } from "./goal-record.ts";
|
|
16
16
|
import { countTaskSubtree } from "./goal-task-count.ts";
|
|
17
|
-
import { loadGoalSettings, type GoalSettings, type ThinkingLevel } from "./goal-settings.ts";
|
|
17
|
+
import { GOAL_THINKING_LEVELS, loadGoalSettings, type GoalSettings, type ThinkingLevel } from "./goal-settings.ts";
|
|
18
18
|
import { statusLabel } from "./goal-core.ts";
|
|
19
19
|
|
|
20
20
|
export interface AuditorProgress {
|
|
@@ -47,7 +47,7 @@ export interface GoalAuditorResult {
|
|
|
47
47
|
error?: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const THINKING_LEVELS = new Set(
|
|
50
|
+
const THINKING_LEVELS = new Set<string>(GOAL_THINKING_LEVELS);
|
|
51
51
|
|
|
52
52
|
function asNonEmptyString(value: unknown): string | undefined {
|
|
53
53
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { GOAL_COMMANDS, commandHint, commandPaths, commandProblems, commandRoutes, matchCommandRoute, type GoalCommandId } from "./goal-controls.ts";
|
|
2
|
+
import { showGoalControls } from "./widgets/goal-controls-dialog.ts";
|
|
1
3
|
import { goalStoragePath, goalStorageRoot } from "./storage/goal-root.ts";
|
|
2
4
|
import { existsSync } from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
@@ -75,12 +77,15 @@ import {
|
|
|
75
77
|
import type { GoalCore } from "./goal-state.ts";
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
80
|
+
* Stable command actions retain their lifecycle checks. Configured invocation
|
|
81
|
+
* paths are registered at load time; aliases and subcommands share the same
|
|
82
|
+
* handler and argument completion. /goal-settings remains the recovery entry.
|
|
81
83
|
*/
|
|
82
84
|
export function registerGoalCommands(core: GoalCore): void {
|
|
83
85
|
const { pi } = core;
|
|
86
|
+
type Definition = { description: string; handler: (args: string, ctx: ExtensionContext) => Promise<void> };
|
|
87
|
+
const actions = new Map<GoalCommandId, Definition>();
|
|
88
|
+
const define = (id: GoalCommandId, definition: Definition): void => { actions.set(id, definition); };
|
|
84
89
|
|
|
85
90
|
async function chooseOpenGoal(ctx: ExtensionContext, title: string): Promise<GoalRecord | null> {
|
|
86
91
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
@@ -117,7 +122,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
117
122
|
async function focusGoalCommand(ctx: ExtensionContext): Promise<void> {
|
|
118
123
|
const open = core.openGoals();
|
|
119
124
|
if (open.length === 0) {
|
|
120
|
-
ctx.ui.notify(
|
|
125
|
+
ctx.ui.notify(`No open goals. Use ${commandHint("goal", core.loadedCommands)} to draft one, or ${commandHint("goal-direct", core.loadedCommands)} <objective> to start immediately.`, "warning");
|
|
121
126
|
return;
|
|
122
127
|
}
|
|
123
128
|
if (open.length === 1) {
|
|
@@ -172,7 +177,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
172
177
|
}
|
|
173
178
|
if (!current) {
|
|
174
179
|
const openCount = otherOpenGoalCount(core.goalsById, null);
|
|
175
|
-
ctx.ui.notify(openCount > 0 ? buildUnfocusedOpenGoalsSummary(openCount) : detailedSummary(null), "info");
|
|
180
|
+
ctx.ui.notify(openCount > 0 ? buildUnfocusedOpenGoalsSummary(openCount) : detailedSummary(null, core.loadedCommands), "info");
|
|
176
181
|
return;
|
|
177
182
|
}
|
|
178
183
|
ctx.ui.notify(`Goal unfocused for this session. It remains open in .pi/goals: ${current.id}`, "info");
|
|
@@ -181,12 +186,12 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
181
186
|
function handleDirectGoalSet(rawObjective: string, ctx: ExtensionContext, mode: GoalMode): void {
|
|
182
187
|
const raw = rawObjective.trim();
|
|
183
188
|
if (!raw) {
|
|
184
|
-
const command = mode === "sisyphus" ? "
|
|
189
|
+
const command = `${commandHint(mode === "sisyphus" ? "sisyphus-direct" : "goal-direct", core.loadedCommands)} <objective>`;
|
|
185
190
|
ctx.ui.notify(`No objective provided. Use ${command}.`, "warning");
|
|
186
191
|
return;
|
|
187
192
|
}
|
|
188
193
|
if (mode === "sisyphus" && !sisyphusObjectiveSufficient(raw)) {
|
|
189
|
-
ctx.ui.notify(
|
|
194
|
+
ctx.ui.notify(`A Sisyphus objective needs ordered steps with per-step done criteria. Use ${commandHint("sisyphus", core.loadedCommands)} for guided drafting, or provide numbered steps (1) ..., 2) ...) in the objective.`, "warning");
|
|
190
195
|
return;
|
|
191
196
|
}
|
|
192
197
|
const settings = loadGoalSettings(ctx.cwd);
|
|
@@ -222,7 +227,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
222
227
|
? `\n\n${formatCheckpointHealthReport(checkpointHealth, sessionFile)}`
|
|
223
228
|
: "";
|
|
224
229
|
ctx.ui.notify(
|
|
225
|
-
formatRecoveryReport(report) + checkpointSection,
|
|
230
|
+
formatRecoveryReport(report, core.loadedCommands) + checkpointSection,
|
|
226
231
|
report.healthy && (checkpointHealth?.legacyFull ?? 0) === 0 ? "info" : "warning",
|
|
227
232
|
);
|
|
228
233
|
}
|
|
@@ -262,6 +267,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
262
267
|
? `goal-refresh: re-read caches from disk — ${changes.length} change(s):\n${changes.map((c) => ` - ${c}`).join("\n")}`
|
|
263
268
|
: "goal-refresh: no changes detected — caches were already current.";
|
|
264
269
|
ctx.ui.notify(text, "info");
|
|
270
|
+
core.scheduler.refreshSettings(ctx);
|
|
265
271
|
core.updateUI(ctx);
|
|
266
272
|
}
|
|
267
273
|
|
|
@@ -279,7 +285,10 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
279
285
|
const health = /^\s*health\b/i.test(rawArgs);
|
|
280
286
|
const ledger = readGoalLedger(ctx);
|
|
281
287
|
const text = buildGoalStatusText({
|
|
288
|
+
commands: core.loadedCommands,
|
|
289
|
+
keybindings: loadGoalSettings(ctx.cwd).keybindings,
|
|
282
290
|
maxAutonomousRuns: loadGoalSettings(ctx.cwd).maxAutonomousRuns,
|
|
291
|
+
idleContinuationBackoff: loadGoalSettings(ctx.cwd).idleContinuationBackoff,
|
|
283
292
|
goal: view,
|
|
284
293
|
focused: view !== null && core.focusedGoalId === view.id,
|
|
285
294
|
otherOpenGoals: otherCount,
|
|
@@ -323,10 +332,14 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
323
332
|
return;
|
|
324
333
|
}
|
|
325
334
|
if (currentGoal.status === "paused") {
|
|
326
|
-
ctx.ui.notify(
|
|
335
|
+
ctx.ui.notify(`Goal is already paused. Use ${commandHint("goal-resume", core.loadedCommands)} to continue.`, "info");
|
|
336
|
+
if (!ctx.isIdle()) ctx.abort?.();
|
|
327
337
|
return;
|
|
328
338
|
}
|
|
339
|
+
core.interruption.reset();
|
|
340
|
+
ctx.ui.setStatus("goal-interrupt", undefined);
|
|
329
341
|
core.pauseActiveGoal(ctx);
|
|
342
|
+
ctx.abort?.();
|
|
330
343
|
}
|
|
331
344
|
|
|
332
345
|
async function handleGoalResume(ctx: ExtensionContext): Promise<void> {
|
|
@@ -361,10 +374,11 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
361
374
|
type SettingRow = {
|
|
362
375
|
key: keyof GoalSettings | string;
|
|
363
376
|
label: string;
|
|
364
|
-
section: "Goal behavior" | "Task tracking" | "Completion auditor" | "Blocker Oracle";
|
|
377
|
+
section: "Goal behavior" | "Task tracking" | "Completion auditor" | "Blocker Oracle" | "Advanced execution";
|
|
365
378
|
kind: "boolean" | "modelSelector" | "thinking" | "positiveInteger";
|
|
366
379
|
/** Settings path for the mutation (defaults to [key]). */
|
|
367
380
|
path?: string[];
|
|
381
|
+
help?: string;
|
|
368
382
|
};
|
|
369
383
|
|
|
370
384
|
const SETTING_ROWS: readonly SettingRow[] = [
|
|
@@ -377,6 +391,8 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
377
391
|
{ key: "showAutonomousRuns", label: "show autonomous runs line", section: "Goal behavior", kind: "boolean" },
|
|
378
392
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
|
|
379
393
|
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
|
|
394
|
+
{ key: "idleContinuationBackoff", label: "slow repeated idle replies (opt-in)", section: "Advanced execution", kind: "boolean",
|
|
395
|
+
help: "Slows repeated replies without recognized work. May delay reasoning or custom tools. For a hard stop, set an autonomous run allowance." },
|
|
380
396
|
{ key: "disableTasks", label: "disableTasks", section: "Task tracking", kind: "boolean" },
|
|
381
397
|
{ key: "subtaskDepth", label: "subtaskDepth", section: "Task tracking", kind: "positiveInteger" },
|
|
382
398
|
{ key: "disabled", label: "auditor disabled", section: "Completion auditor", kind: "boolean" },
|
|
@@ -393,7 +409,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
393
409
|
];
|
|
394
410
|
|
|
395
411
|
function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
|
|
396
|
-
if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner" || key === "hideUnfocusedPrompt" || key === "showAutonomousRuns") {
|
|
412
|
+
if (key === "idleContinuationBackoff" || key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner" || key === "hideUnfocusedPrompt" || key === "showAutonomousRuns") {
|
|
397
413
|
return config[key] === true ? "true" : "false";
|
|
398
414
|
}
|
|
399
415
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
@@ -439,6 +455,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
439
455
|
try {
|
|
440
456
|
mutateSettingsLayer({ scope, cwd: ctx.cwd, mutation });
|
|
441
457
|
applyEffectiveSettings();
|
|
458
|
+
if (mutation.path[0] === "idleContinuationBackoff") core.scheduler.refreshSettings(ctx);
|
|
442
459
|
return true;
|
|
443
460
|
} catch (err) {
|
|
444
461
|
ctx.ui.notify(`Settings change failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
@@ -457,6 +474,8 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
457
474
|
const localLayer = snapshot[scope].layer as Record<string, unknown>;
|
|
458
475
|
const options: string[] = [];
|
|
459
476
|
options.push(`─── Editing: ${scope} (${scopePath(scope)}) ───`);
|
|
477
|
+
options.push(`Scope: ${scope} (select to switch)`);
|
|
478
|
+
options.push("Controls: commands, keys and pause order…");
|
|
460
479
|
let lastSection: string | null = null;
|
|
461
480
|
const pathOf = (row: SettingRow): string[] => row.path ?? [String(row.key)];
|
|
462
481
|
const valueAtPath = (obj: unknown, path: string[]): unknown => {
|
|
@@ -494,6 +513,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
494
513
|
options.push("Done");
|
|
495
514
|
const selected = await ctx.ui.select("Goal settings", options);
|
|
496
515
|
if (!selected || selected === "Done") break;
|
|
516
|
+
if (selected.startsWith("Controls:")) { if (await showGoalControls(core, ctx, scope)) return; continue; }
|
|
497
517
|
if (selected.startsWith("───")) continue; // headers are not rows
|
|
498
518
|
const trimmed = selected.trim();
|
|
499
519
|
if (trimmed.startsWith("Scope:")) {
|
|
@@ -534,7 +554,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
534
554
|
];
|
|
535
555
|
if (hasLocalOverride) actions.push(inheritLabel);
|
|
536
556
|
actions.push("Cancel");
|
|
537
|
-
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
557
|
+
const action = await ctx.ui.select(`${row.label} (${scope})${row.help ? `\n${row.help}` : ""}`, actions);
|
|
538
558
|
if (!action || action === "Cancel") continue;
|
|
539
559
|
if (action === inheritLabel) {
|
|
540
560
|
applyMutation(scope, { op: "unset", path: rowPath });
|
|
@@ -552,7 +572,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
552
572
|
const actions = [`Set ${scope} override...`];
|
|
553
573
|
if (hasLocalOverride) actions.push(inheritLabel);
|
|
554
574
|
actions.push("Cancel");
|
|
555
|
-
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
575
|
+
const action = await ctx.ui.select(`${row.label} (${scope})${row.help ? `\n${row.help}` : ""}`, actions);
|
|
556
576
|
if (!action || action === "Cancel") continue;
|
|
557
577
|
if (action === inheritLabel) {
|
|
558
578
|
applyMutation(scope, { op: "unset", path: rowPath });
|
|
@@ -658,7 +678,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
658
678
|
// Headless behavior is explicit: guidance without mutation. Clearing
|
|
659
679
|
// requires an interactive confirmation (follow-up Stage 2).
|
|
660
680
|
if (!ctx.hasUI) {
|
|
661
|
-
ctx.ui.notify(`Run
|
|
681
|
+
ctx.ui.notify(`Run ${commandHint("goal-clear", core.loadedCommands)} in an interactive session to confirm clearing: ${oneLineSummary(target)}`, "warning");
|
|
662
682
|
return;
|
|
663
683
|
}
|
|
664
684
|
const confirmed = await ctx.ui.confirm("Clear goal?", oneLineSummary(target));
|
|
@@ -688,19 +708,19 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
688
708
|
const selected = await chooseOpenGoal(ctx, "Tweak which open goal?");
|
|
689
709
|
if (!selected) return;
|
|
690
710
|
} else {
|
|
691
|
-
ctx.ui.notify(
|
|
711
|
+
ctx.ui.notify(`No goal is set. Use ${commandHint("goal", core.loadedCommands)} to draft one, or ${commandHint("goal-direct", core.loadedCommands)} <objective> to create one immediately.`, "warning");
|
|
692
712
|
return;
|
|
693
713
|
}
|
|
694
714
|
}
|
|
695
715
|
const currentGoal = core.state.goal;
|
|
696
716
|
if (!currentGoal) return;
|
|
697
717
|
if (currentGoal.status === "complete") {
|
|
698
|
-
ctx.ui.notify(
|
|
718
|
+
ctx.ui.notify(`Goal is complete. Use ${commandHint("goal", core.loadedCommands)} to draft a new one, or ${commandHint("goal-direct", core.loadedCommands)} <objective> to create one immediately.`, "warning");
|
|
699
719
|
return;
|
|
700
720
|
}
|
|
701
721
|
const trimmed = replacement.trim();
|
|
702
722
|
if (!trimmed) {
|
|
703
|
-
ctx.ui.notify(
|
|
723
|
+
ctx.ui.notify(`Describe the requested change: ${commandHint("goal-tweak", core.loadedCommands)} <change>, for example remove the token budget`, "info");
|
|
704
724
|
return;
|
|
705
725
|
}
|
|
706
726
|
const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
|
|
@@ -714,19 +734,19 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
714
734
|
}
|
|
715
735
|
|
|
716
736
|
// /goal and /sisyphus are the guided default. -direct commands are the explicit bypass.
|
|
717
|
-
|
|
737
|
+
define("goal", {
|
|
718
738
|
description: "Draft a regular goal with clarification, task planning, and confirmation.",
|
|
719
739
|
handler: async (rawArgs, ctx) => {
|
|
720
740
|
await startGoalDrafting(core, ctx, "goal", rawArgs);
|
|
721
741
|
},
|
|
722
742
|
});
|
|
723
|
-
|
|
743
|
+
define("sisyphus", {
|
|
724
744
|
description: "Draft a Sisyphus goal with clarification, task planning, and confirmation.",
|
|
725
745
|
handler: async (rawArgs, ctx) => {
|
|
726
746
|
await startGoalDrafting(core, ctx, "sisyphus", rawArgs);
|
|
727
747
|
},
|
|
728
748
|
});
|
|
729
|
-
|
|
749
|
+
define("goal-cancel", {
|
|
730
750
|
description: "Cancel the in-progress guided draft without creating or modifying a goal.",
|
|
731
751
|
handler: async (_rawArgs, ctx) => {
|
|
732
752
|
if (!hasActiveDraft(core)) {
|
|
@@ -738,15 +758,15 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
738
758
|
ctx.ui.notify("Draft cancelled; no goal was created. The execution profile is restored.", "info");
|
|
739
759
|
},
|
|
740
760
|
});
|
|
741
|
-
|
|
761
|
+
define("goal-direct", {
|
|
742
762
|
description: "Create and start a regular goal immediately, without drafting.",
|
|
743
763
|
handler: async (rawArgs, ctx) => { handleDirectGoalSet(rawArgs, ctx, "goal"); },
|
|
744
764
|
});
|
|
745
|
-
|
|
765
|
+
define("sisyphus-direct", {
|
|
746
766
|
description: "Create and start a Sisyphus goal immediately, without drafting.",
|
|
747
767
|
handler: async (rawArgs, ctx) => { handleDirectGoalSet(rawArgs, ctx, "sisyphus"); },
|
|
748
768
|
});
|
|
749
|
-
|
|
769
|
+
define("goal-list", {
|
|
750
770
|
description: "List all open goals and the current focus.",
|
|
751
771
|
handler: async (_rawArgs, ctx) => {
|
|
752
772
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
@@ -754,64 +774,92 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
754
774
|
core.updateUI(ctx);
|
|
755
775
|
},
|
|
756
776
|
});
|
|
757
|
-
|
|
777
|
+
define("goal-status", {
|
|
758
778
|
description: "Show the unified goal dashboard (read-only). Append \"verbose\" for full detail or \"health\" for storage/runtime checks.",
|
|
759
779
|
handler: async (rawArgs, ctx) => {
|
|
760
780
|
await showGoalStatus(rawArgs ?? "", ctx);
|
|
761
781
|
},
|
|
762
782
|
});
|
|
763
|
-
|
|
783
|
+
define("goal-refresh", {
|
|
764
784
|
description: "Re-read goal storage caches (pool, ledger, settings) from disk and report what changed. Picks up external edits to .pi files — no file watchers needed.",
|
|
765
785
|
handler: async (_rawArgs, ctx) => {
|
|
766
786
|
await runGoalRefresh(ctx);
|
|
767
787
|
},
|
|
768
788
|
});
|
|
769
|
-
|
|
789
|
+
define("goal-recovery", {
|
|
770
790
|
description: "Read-only storage/recovery report (malformed goal files, malformed ledger lines, stale locks, orphaned snapshot data). Append \"repair\" to remove stale locks and refresh the pool snapshot after confirmation (with backup).",
|
|
771
791
|
handler: async (rawArgs, ctx) => {
|
|
772
792
|
await runGoalRecovery((rawArgs ?? "").trim(), ctx);
|
|
773
793
|
},
|
|
774
794
|
});
|
|
775
|
-
|
|
795
|
+
define("goal-focus", {
|
|
776
796
|
description: "Choose which open goal this session focuses on.",
|
|
777
797
|
handler: async (_rawArgs, ctx) => {
|
|
778
798
|
await focusGoalCommand(ctx);
|
|
779
799
|
},
|
|
780
800
|
});
|
|
781
|
-
|
|
801
|
+
define("goal-unfocus", {
|
|
782
802
|
description: "Stop focusing the current goal (session only; goal stays open).",
|
|
783
803
|
handler: async (_rawArgs, ctx) => {
|
|
784
804
|
unfocusGoalCommand(ctx);
|
|
785
805
|
},
|
|
786
806
|
});
|
|
787
|
-
|
|
788
|
-
description: "
|
|
789
|
-
handler: async (
|
|
790
|
-
await
|
|
807
|
+
define("goal-settings", {
|
|
808
|
+
description: "Goal settings and controls; accepts controls for commands, keys and pause order.",
|
|
809
|
+
handler: async (rawArgs, ctx) => {
|
|
810
|
+
if (rawArgs.trim() === "controls") await showGoalControls(core, ctx);
|
|
811
|
+
else await handleSettingsMenu(ctx);
|
|
791
812
|
},
|
|
792
813
|
});
|
|
793
|
-
|
|
814
|
+
define("goal-tweak", {
|
|
794
815
|
description: "Revise the current goal or its token budget with confirmation.",
|
|
795
816
|
handler: async (rawArgs, ctx) => {
|
|
796
817
|
await runGoalTweak(rawArgs, ctx);
|
|
797
818
|
},
|
|
798
819
|
});
|
|
799
|
-
|
|
820
|
+
define("goal-clear", {
|
|
800
821
|
description: "Archive the current goal after confirmation (user-owned abandonment).",
|
|
801
822
|
handler: async (_rawArgs, ctx) => {
|
|
802
823
|
await handleGoalClear(ctx);
|
|
803
824
|
},
|
|
804
825
|
});
|
|
805
|
-
|
|
806
|
-
description: "Pause the currently running goal
|
|
826
|
+
define("goal-pause", {
|
|
827
|
+
description: "Pause the currently running goal and stop its turn.",
|
|
807
828
|
handler: async (_rawArgs, ctx) => {
|
|
808
829
|
await handleGoalPause(ctx);
|
|
809
830
|
},
|
|
810
831
|
});
|
|
811
|
-
|
|
832
|
+
define("goal-resume", {
|
|
812
833
|
description: "Continue now and renew the configured autonomous-run allowance.",
|
|
813
834
|
handler: async (_rawArgs, ctx) => {
|
|
814
835
|
await handleGoalResume(ctx);
|
|
815
836
|
},
|
|
816
837
|
});
|
|
838
|
+
const configured = loadGoalSettings(process.cwd()).commands;
|
|
839
|
+
const problems = commandProblems(configured);
|
|
840
|
+
core.loadedCommands = problems.length ? undefined : configured;
|
|
841
|
+
core.controlWarnings = problems;
|
|
842
|
+
const routes = commandRoutes(core.loadedCommands);
|
|
843
|
+
core.invokeGoalCommand = async (id, args, ctx) => {
|
|
844
|
+
const action = actions.get(id);
|
|
845
|
+
if (!action) return;
|
|
846
|
+
core.enterGoalModal();
|
|
847
|
+
try { await action.handler(args, ctx); }
|
|
848
|
+
finally { core.exitGoalModal(); }
|
|
849
|
+
};
|
|
850
|
+
const roots = [...actions.keys()].flatMap(id => commandPaths(core.loadedCommands, id).map(route => route.split(" ")[0]!));
|
|
851
|
+
for (const root of new Set([...roots, "goal-settings"])) {
|
|
852
|
+
const rootRoutes = routes.filter(route => route.path.split(" ")[0] === root);
|
|
853
|
+
const direct = rootRoutes.find(route => route.path === root);
|
|
854
|
+
pi.registerCommand(root, {
|
|
855
|
+
description: direct ? actions.get(direct.id)!.description : `Goal actions: ${rootRoutes.map(route => route.path.slice(root.length + 1)).join(", ")}`,
|
|
856
|
+
getArgumentCompletions: prefix => rootRoutes.filter(route => route.path !== root && route.path.slice(root.length + 1).startsWith(prefix)).map(route => ({ value: route.path.slice(root.length + 1), label: route.path.slice(root.length + 1), description: GOAL_COMMANDS.find(([id]) => id === route.id)![1] })),
|
|
857
|
+
handler: async (args, ctx) => {
|
|
858
|
+
const match = matchCommandRoute(routes, root, args);
|
|
859
|
+
if (match) await core.invokeGoalCommand!(match.id, match.args, ctx);
|
|
860
|
+
else ctx.ui.notify(`Choose a goal command: ${rootRoutes.map(route => `/${route.path}`).join(", ")}`, "info");
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
817
865
|
}
|