pi-goal-x 0.31.5 → 0.31.7
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 +48 -1
- package/assets/badge-dark.svg +2 -2
- package/assets/badge-light.svg +2 -2
- package/docs/architecture.md +3 -1
- package/docs/prompt-caching.md +27 -0
- package/extensions/goal-accounting.ts +15 -0
- package/extensions/goal-auditor.ts +2 -2
- package/extensions/goal-commands.ts +10 -8
- package/extensions/goal-compaction.ts +1 -1
- package/extensions/goal-completion.ts +5 -2
- package/extensions/goal-core-tools.ts +5 -5
- package/extensions/goal-drafting.ts +47 -15
- package/extensions/goal-events.ts +75 -86
- package/extensions/goal-ledger.ts +13 -6
- package/extensions/goal-live-retention.ts +219 -0
- package/extensions/goal-policy.ts +5 -1
- package/extensions/goal-pool.ts +1 -1
- package/extensions/goal-prompt-cache.ts +80 -0
- package/extensions/goal-questionnaire.ts +24 -2
- package/extensions/goal-record.ts +2 -1
- package/extensions/goal-recovery.ts +21 -20
- package/extensions/goal-scheduler-state.ts +22 -9
- package/extensions/goal-scheduler.ts +0 -6
- package/extensions/goal-service.ts +2 -2
- package/extensions/goal-settings.ts +25 -2
- package/extensions/goal-state.ts +34 -28
- package/extensions/prompts/goal-prompts.ts +24 -11
- package/extensions/storage/goal-files.ts +34 -42
- package/extensions/storage/goal-lock.ts +3 -2
- package/extensions/storage/goal-root.ts +95 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<a href="https://pi.dev/packages?type=extension" target="_blank" rel="noopener noreferrer">
|
|
7
7
|
<picture>
|
|
8
8
|
<source media="(prefers-color-scheme: dark)" srcset="assets/badge-dark.svg">
|
|
9
|
-
<img src="assets/badge-light.svg" alt="TOP 0.3% of Pi coding agent extensions: #7 of 3,
|
|
9
|
+
<img src="assets/badge-light.svg" alt="TOP 0.3% of Pi coding agent extensions: #7 of 3,275 by downloads · Sep 22, 2026 (best recorded rank)" width="480">
|
|
10
10
|
</picture>
|
|
11
11
|
</a>
|
|
12
12
|
</div>
|
|
@@ -23,6 +23,8 @@ The extension saves goal objectives, tasks, and progress across sessions. You ca
|
|
|
23
23
|
pi install npm:pi-goal-x
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Supported Pi versions: **0.83–0.87**. The extension supports Node 22.15+; newer Pi releases require Node 22.19+.
|
|
27
|
+
|
|
26
28
|
## Create a goal
|
|
27
29
|
|
|
28
30
|
```text
|
|
@@ -120,6 +122,7 @@ Open `/goal-settings` to change these options. You can save defaults for all pro
|
|
|
120
122
|
| Task tracking (`disableTasks`) | Turn task lists on or off. Set to `true` to disable them. |
|
|
121
123
|
| Subtask depth (`subtaskDepth`) | Limit how many levels of subtasks the agent can create. |
|
|
122
124
|
| Completion requirements (`disableContracts`) | Turn explicit goal and task completion requirements on or off. Set to `true` to disable them. |
|
|
125
|
+
| Unfocused reminder (`hideUnfocusedPrompt`) | When a session has no focused goal but open goals exist in the selected pool, the agent receives a `[PI GOAL UNFOCUSED]` reminder on every request. Set to `true` to stop that reminder. Defaults to `false`. This is independent of `hideUnfocusedBanner`, which hides only the unfocused widget and status hint. Neither setting selects or resumes a goal; the separate `autoSelectSingleGoal` setting still applies. |
|
|
123
126
|
| Auditor disabled | Turn off independent completion review. |
|
|
124
127
|
| Auditor provider, model, and thinking level | Choose which model reviews completed work and its reasoning effort. |
|
|
125
128
|
|
|
@@ -165,6 +168,50 @@ pi.events.emit("pi-goal:wake", { goalId, waitToken });
|
|
|
165
168
|
|
|
166
169
|
Existing producers that directly send `triggerTurn`/`followUp` messages still run as ordinary host work and supersede old pending decisions. Those independently started turns are **outside this extension's allowance**; use `pi-goal:wake` to put them through its spending gate. The allowance also does not limit Pi's own within-run tool loop or native retries. It bounds the goal extension's kickoff, continuation, check, signal, repair and recovery dispatches.
|
|
167
170
|
|
|
171
|
+
### Prompt caching
|
|
172
|
+
|
|
173
|
+
Goal state is refreshed at the request tail while the system prompt and conversation prefix stay stable. Pi retains control of provider cache settings. See [prompt caching](docs/prompt-caching.md) for explicit-cache handling, validation, and cache invalidation boundaries.
|
|
174
|
+
|
|
175
|
+
### Changing a token budget
|
|
176
|
+
|
|
177
|
+
Use the existing tweak flow: `/goal-tweak remove the token budget` or `/goal-tweak set the token budget to 50000`. The proposal shows the current and proposed limits before confirmation. A budget is a total lifetime limit, not an additional allocation; consumed tokens and completed work are preserved. Omitting a budget change retains the current limit.
|
|
178
|
+
|
|
179
|
+
After confirmation, a goal stopped only by its budget can continue if the revised limit allows it and scheduling permits. A still-exhausted budget keeps it stopped. Other-session ownership, interrupted execution and exhausted autonomous-run allowances still require their existing recovery steps. Creation and tweak results always show the effective budget.
|
|
180
|
+
|
|
181
|
+
### Optional arguments on Responses-compatible providers
|
|
182
|
+
|
|
183
|
+
Some Pi `openai-responses` configurations omit the wire-level `strict` flag. OpenAI Responses may normalize schemas into strict mode when that flag is omitted. This can conflict with optional goal arguments; it is separate from pi-goal-x's `strictExecutionContract` scheduling setting.
|
|
184
|
+
|
|
185
|
+
For the reported OpenCode model, a narrowly scoped Pi `models.json` override makes the supported Pi 0.84.1 adapter send `strict: false` for ordinary tools:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"providers": {
|
|
190
|
+
"opencode": {
|
|
191
|
+
"modelOverrides": {
|
|
192
|
+
"gpt-6-astra": { "compat": { "supportsStrictMode": true } }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The capability flag permits the adapter to send the explicit non-strict opt-out; it does not request strict sampling for ordinary goal tools. Merge this into existing model configuration and reload Pi. This is a provider-specific workaround, not a guarantee about third-party model behavior. The local reproduction inspects requests before transmission; the issue's live OpenCode A/B result has not been independently reproduced. See [issue #59](https://github.com/tmonk/pi-goal-x/issues/59) and [OpenAI's function-calling documentation](https://developers.openai.com/api/docs/guides/function-calling).
|
|
200
|
+
|
|
201
|
+
### Sharing a goal pool across worktrees
|
|
202
|
+
|
|
203
|
+
By default goals stay in `<cwd>/.pi/goals`. Set `goalsRoot` in your project or global settings to an absolute directory (or `~/path`), or set `PI_GOAL_ROOT` for the session. Precedence is environment, project, global, then the existing default. For example:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{ "goalsRoot": "~/work/project-goals" }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Worktrees pointing to the same root share goals, archives, ledger and locks; focus remains session-local and execution ownership still requires explicit resume. The working directory for tools and audits is unchanged. Existing goals are not moved automatically. Reload/reopen the session after changing roots; `/goal-refresh` refreshes the selected pool. `/goal-status verbose` shows the effective location. Saved goal paths remain logical `.pi/goals/...` paths within that selected pool.
|
|
210
|
+
|
|
211
|
+
`hideUnfocusedPrompt: true` suppresses ordinary unfocused reminders to the model independently of `hideUnfocusedBanner`; neither changes focus or bypasses stale-checkpoint checks.
|
|
212
|
+
|
|
213
|
+
Token usage shown in the dashboard is cumulative across goal turns. The model receives a separate context snapshot when Pi can supply one; unavailable context is never reported as zero. Retained snapshots are bounded and newer snapshots supersede older ones.
|
|
214
|
+
|
|
168
215
|
## License
|
|
169
216
|
|
|
170
217
|
MIT
|
package/assets/badge-dark.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,275 by downloads · Sep 22, 2026 (best recorded rank)">
|
|
2
2
|
<!-- Dark mode -->
|
|
3
3
|
<style>
|
|
4
4
|
.frame { fill: #14181f; stroke: #2a3040; stroke-width: 1; }
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
<text class="rank" x="18" y="34">TOP 0.3%</text>
|
|
14
14
|
<line class="divider" x1="128" y1="13" x2="128" y2="41"/>
|
|
15
15
|
<text class="label" x="143" y="25">of Pi coding agent extensions</text>
|
|
16
|
-
<text class="meta" x="143" y="41">#7 of 3,
|
|
16
|
+
<text class="meta" x="143" y="41">#7 of 3,275 by downloads · Sep 22, 2026</text>
|
|
17
17
|
</a>
|
|
18
18
|
</svg>
|
package/assets/badge-light.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.3% of Pi coding agent extensions: #7 of 3,275 by downloads · Sep 22, 2026 (best recorded rank)">
|
|
2
2
|
<!-- Light mode -->
|
|
3
3
|
<style>
|
|
4
4
|
.frame { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1; }
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
<text class="rank" x="18" y="34">TOP 0.3%</text>
|
|
14
14
|
<line class="divider" x1="128" y1="13" x2="128" y2="41"/>
|
|
15
15
|
<text class="label" x="143" y="25">of Pi coding agent extensions</text>
|
|
16
|
-
<text class="meta" x="143" y="41">#7 of 3,
|
|
16
|
+
<text class="meta" x="143" y="41">#7 of 3,275 by downloads · Sep 22, 2026</text>
|
|
17
17
|
</a>
|
|
18
18
|
</svg>
|
package/docs/architecture.md
CHANGED
|
@@ -19,7 +19,7 @@ handlers from their dedicated modules:
|
|
|
19
19
|
| `goal-task-tools.ts` | `set_goal_tasks` / `update_goal_task` executors plus flat parent-linked conversion, id-stable merge, `countTasks` |
|
|
20
20
|
| `goal-task-confirmation.ts` | Task-only result boundary (`{decision}`, no auditor toggle) with neutral Confirm task list / Keep current tasks labels |
|
|
21
21
|
| `goal-commands.ts` | The curated fourteen-command palette and its handlers |
|
|
22
|
-
| `goal-events.ts` |
|
|
22
|
+
| `goal-events.ts` | Lifecycle and provider event handlers (`context`, `before_provider_request`, `turn_start`, `tool_call`, `tool_execution_end`, `turn_end`, `message_end`, `session_start`, `session_before_compact`, `session_compact`, `session_tree`, `before_agent_start`, `agent_end`, `agent_settled`, `session_shutdown`) |
|
|
23
23
|
| `goal-widget.ts` | Terminal input keybindings (Esc pause / abort-audit, Ctrl+Shift+T overlay) and the hidden debug helpers |
|
|
24
24
|
| `goal-format.ts` | Pure formatting/message-introspection helpers and renderers |
|
|
25
25
|
| `goal-service.ts` | `GoalService` — the sole mutation boundary: ordered reconcile → id/focus-revision validation → clone-mutate → write/archive → ledger → memory commit → returned effects |
|
|
@@ -341,3 +341,5 @@ operable; `/goal-clear` confirms; task confirmation uses neutral labels;
|
|
|
341
341
|
completion commits are failure-checked; and cross-process mutations are
|
|
342
342
|
serialized with persisted revisions plus per-goal filesystem locks that
|
|
343
343
|
return typed conflicts to stale writers instead of overwriting blindly.
|
|
344
|
+
|
|
345
|
+
Prompt layout and explicit provider cache handling are documented in [prompt caching](prompt-caching.md). Live goal state is constructed in the context hook; before_agent_start performs lifecycle preflight without modifying the system prompt.
|
|
@@ -0,0 +1,27 @@
|
|
|
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 plus compact counter/context snapshots, replayed at verified history anchors when safe. Current policy includes the objective, tasks, lifecycle rules, limits, scheduling state and applicable recovery/auditor reminders.
|
|
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
|
+
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.
|
|
14
|
+
|
|
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
|
+
|
|
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 supported Pi range is 0.83 through 0.87. Validation covers each supported minor; development dependencies pin 0.87.0.
|
|
18
|
+
|
|
19
|
+
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
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
Active requests split policy from compact counter/context snapshots. Already-sent tails are replayed at verified history anchors so implicit-cache requests extend their previous prefix. Policy changes (including budget/run-limit removal), rewritten history and unsafe tool-result boundaries reset retention. At most 32 tails and 4 KiB of counter text are retained per session, across at most 16 session/model identities; no full-state tails are persisted. Unchanged requests add no tail. Older snapshots are historical, and current policy takes precedence over cache reuse.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
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.
|
|
@@ -90,3 +90,18 @@ export function budgetLine(goal: BudgetLike): string | null {
|
|
|
90
90
|
const used = Math.floor(goal.usage.tokensUsed);
|
|
91
91
|
return `token budget: ${used}/${Math.floor(goal.tokenBudget ?? 0)} used, ${remaining} remaining`;
|
|
92
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
|
+
}
|
|
@@ -160,7 +160,7 @@ function minimalGoalMetadata(goal: GoalRecord): string {
|
|
|
160
160
|
`Goal id: ${goal.id}`,
|
|
161
161
|
`Status: ${statusLabel(goal)}`,
|
|
162
162
|
`Mode: ${goal.sisyphus ? "sisyphus" : "regular"}`,
|
|
163
|
-
goal.tokenBudget ? `
|
|
163
|
+
goal.tokenBudget ? `Lifetime spending cap: ${goal.tokenBudget} tokens (${goal.usage.tokensUsed} cumulatively used; not context occupancy)` : undefined,
|
|
164
164
|
].filter(Boolean).join("\n");
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -177,7 +177,7 @@ export function buildGoalAuditorPrompt(args: {
|
|
|
177
177
|
"You are the independent completion auditor for pi-goal. Decide whether the user's objective is actually satisfied.",
|
|
178
178
|
"Audit checklist:",
|
|
179
179
|
"1. Extract the real success criteria, including every explicit requirement and quality/reader outcome. Disapprove missing, contradicted, weakly verified or uninspectable requirements.",
|
|
180
|
-
"2. Inspect real artifacts with read/grep/find/ls/bash as needed.
|
|
180
|
+
"2. Inspect real artifacts with read/grep/find/ls/bash as needed. Never mutate files or clean runtime metadata (.pi goals/subagents). Report environment failures separately. Paperwork, counts and build success alone are not proof.",
|
|
181
181
|
...(!args.settings?.disableContracts && args.goal.verificationContract?.trim()
|
|
182
182
|
? ["3. Verify that the executor has satisfied every item in the <verification_contract>. If any item is missing or weakly addressed, disapprove."] : []),
|
|
183
183
|
"4. Explain missing or weak evidence concisely. Disapprove alpha scaffold, generated template, shallow draft or proxy milestones lacking the user-facing value requested.",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { goalStoragePath, goalStorageRoot } from "./storage/goal-root.ts";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -197,9 +198,9 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
async function runGoalRecovery(rawArgs: string, ctx: ExtensionContext): Promise<void> {
|
|
200
|
-
const report = runRecoveryReport(
|
|
201
|
+
const report = runRecoveryReport(ctx);
|
|
201
202
|
if (/^repair$/i.test(rawArgs)) {
|
|
202
|
-
const result = await runRecoveryRepair(
|
|
203
|
+
const result = await runRecoveryRepair(ctx, report, async () => {
|
|
203
204
|
const confirmed = await ctx.ui.confirm(`Remove ${report.staleLocks.length} stale lock(s) and refresh the pool snapshot?`, `Files are backed up to .pi/goals/.recovery-backup first.`);
|
|
204
205
|
return confirmed === true;
|
|
205
206
|
});
|
|
@@ -287,7 +288,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
287
288
|
verbose,
|
|
288
289
|
health,
|
|
289
290
|
activeFilePresent: health && view?.activePath
|
|
290
|
-
? existsSync(
|
|
291
|
+
? existsSync(goalStoragePath(ctx, view.activePath))
|
|
291
292
|
: undefined,
|
|
292
293
|
// Issue #30: checkpoint growth report (health view only, read-only).
|
|
293
294
|
checkpointHealth: health
|
|
@@ -298,7 +299,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
298
299
|
checkpointSessionFile: (ctx.sessionManager as { getSessionFile?: () => string | undefined } | undefined)?.getSessionFile?.(),
|
|
299
300
|
// §13.2: effective settings with provenance appear only in verbose mode;
|
|
300
301
|
// the standard mode stays free of settings noise (§13.1).
|
|
301
|
-
settingsReport: verbose ? effectiveSettingsReport(ctx.cwd) : [],
|
|
302
|
+
settingsReport: verbose ? [...effectiveSettingsReport(ctx.cwd), `effective goal pool: ${goalStorageRoot(ctx)}`] : [],
|
|
302
303
|
});
|
|
303
304
|
ctx.ui.notify(text, "info");
|
|
304
305
|
core.updateUI(ctx);
|
|
@@ -369,6 +370,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
369
370
|
const SETTING_ROWS: readonly SettingRow[] = [
|
|
370
371
|
{ key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
|
|
371
372
|
{ key: "hideUnfocusedBanner", label: "hideUnfocusedBanner", section: "Goal behavior", kind: "boolean" },
|
|
373
|
+
{ key: "hideUnfocusedPrompt", label: "hideUnfocusedPrompt", section: "Goal behavior", kind: "boolean" },
|
|
372
374
|
{ key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
|
|
373
375
|
{ key: "strictExecutionContract", label: "explicit execution contracts (opt-in)", section: "Goal behavior", kind: "boolean" },
|
|
374
376
|
{ key: "maxAutonomousRuns", label: "autonomous run allowance", section: "Goal behavior", kind: "positiveInteger" },
|
|
@@ -390,7 +392,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
390
392
|
];
|
|
391
393
|
|
|
392
394
|
function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
|
|
393
|
-
if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
|
|
395
|
+
if (key === "strictExecutionContract" || key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner" || key === "hideUnfocusedPrompt") {
|
|
394
396
|
return config[key] === true ? "true" : "false";
|
|
395
397
|
}
|
|
396
398
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
@@ -697,13 +699,13 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
697
699
|
}
|
|
698
700
|
const trimmed = replacement.trim();
|
|
699
701
|
if (!trimmed) {
|
|
700
|
-
ctx.ui.notify("
|
|
702
|
+
ctx.ui.notify("Describe the requested change: /goal-tweak <change>, for example remove the token budget", "info");
|
|
701
703
|
return;
|
|
702
704
|
}
|
|
703
705
|
const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
|
|
704
706
|
if (trimmed.length > (max ?? 0)) {
|
|
705
707
|
if (max !== undefined && max > 0) {
|
|
706
|
-
ctx.ui.notify(`
|
|
708
|
+
ctx.ui.notify(`Requested change exceeds ${max} characters (${trimmed.length}).`, "warning");
|
|
707
709
|
return;
|
|
708
710
|
}
|
|
709
711
|
}
|
|
@@ -788,7 +790,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
788
790
|
},
|
|
789
791
|
});
|
|
790
792
|
pi.registerCommand("goal-tweak", {
|
|
791
|
-
description: "
|
|
793
|
+
description: "Revise the current goal or its token budget with confirmation.",
|
|
792
794
|
handler: async (rawArgs, ctx) => {
|
|
793
795
|
await runGoalTweak(rawArgs, ctx);
|
|
794
796
|
},
|
|
@@ -23,7 +23,7 @@ export function buildGoalCompactSummary(
|
|
|
23
23
|
lines.push(`Goal ${goal.id} — ${statusLabel(goal)}`);
|
|
24
24
|
lines.push(` Objective: ${truncateText(goal.objective, 200)}`);
|
|
25
25
|
if (goal.usage.tokensUsed > 0) {
|
|
26
|
-
lines.push(`
|
|
26
|
+
lines.push(` Cumulative goal usage (not context occupancy): ${formatTokenValue(goal.usage.tokensUsed)}`);
|
|
27
27
|
}
|
|
28
28
|
if (goal.usage.activeSeconds > 0) {
|
|
29
29
|
lines.push(` Time: ${formatDuration(goal.usage.activeSeconds)}`);
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { loadGoalSettings, loadGoalSettingsFileConfig } from "./goal-settings.ts";
|
|
10
10
|
import { runGoalCompletionAuditor } from "./goal-auditor.ts";
|
|
11
11
|
import { nowIso, type GoalRecord } from "./goal-record.ts";
|
|
12
|
-
import { latestEventsForGoal, goalRuntimeEvents } from "./goal-ledger.ts";
|
|
12
|
+
import { latestEventsForGoal, latestAuditorResultForGoal, goalRuntimeEvents } from "./goal-ledger.ts";
|
|
13
13
|
import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
|
|
14
14
|
import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
|
|
15
15
|
import type { GoalCore } from "./goal-state.ts";
|
|
@@ -242,10 +242,13 @@ if (settings.disabled === true) {
|
|
|
242
242
|
// (recent lifecycle + task evidence) so it does not re-derive session facts.
|
|
243
243
|
const ledger = goalRuntimeEvents(ctx, auditTarget.id);
|
|
244
244
|
const warmTail = latestEventsForGoal(ledger, auditTarget.id, 8);
|
|
245
|
-
const
|
|
245
|
+
const previousAudit = latestAuditorResultForGoal(ledger, auditTarget.id);
|
|
246
|
+
let warmContext = warmTail.length > 0
|
|
246
247
|
? `Recent goal events (from the shared ledger):\n${warmTail.map((e) => `- ${e.at} ${e.type}${"taskId" in e ? ` (task ${e.taskId})` : ""}${"evidence" in e && e.evidence ? ` evidence: ${e.evidence}` : ""}`).join("\n")}`
|
|
247
248
|
: null;
|
|
248
249
|
|
|
250
|
+
if (previousAudit?.verdict === "disapproved") warmContext = `${warmContext ?? ""}\nPrevious rejection (verify whether resolved): ${previousAudit.report.slice(0, 600)}`;
|
|
251
|
+
|
|
249
252
|
const auditor = await (core.dependencies.runCompletionAuditor ?? runGoalCompletionAuditor)({
|
|
250
253
|
ctx,
|
|
251
254
|
goal: auditTarget,
|
|
@@ -7,7 +7,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { formatDuration, formatTokenValue, statusLabel, truncateText } from "./goal-core.ts";
|
|
8
8
|
import { extractVerificationContract } from "./goal-contract.ts";
|
|
9
9
|
import { detailedSummary, goalDetails, renderGoalResult } from "./goal-format.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { modelBudgetLine } from "./goal-accounting.ts";
|
|
11
11
|
import { buildGoalCreatedReport, buildTaskSummary, findTaskInTree, validateGoalAgentPause, validateGoalBlock } from "./goal-policy.ts";
|
|
12
12
|
import { buildUnfocusedOpenGoalsSummary, otherOpenGoalCount } from "./goal-pool.ts";
|
|
13
13
|
import { readGoalLedger, goalOracleState } from "./goal-ledger.ts";
|
|
@@ -99,8 +99,8 @@ pi.registerTool(defineTool({
|
|
|
99
99
|
const usageBits: string[] = [];
|
|
100
100
|
if (view.usage.activeSeconds > 0) usageBits.push(formatDuration(view.usage.activeSeconds));
|
|
101
101
|
if (view.usage.tokensUsed > 0) usageBits.push(formatTokenValue(view.usage.tokensUsed));
|
|
102
|
-
lines.push(`
|
|
103
|
-
const budget =
|
|
102
|
+
lines.push(`Cumulative goal usage (not context occupancy): ${usageBits.length > 0 ? usageBits.join(" · ") : "none"}`);
|
|
103
|
+
const budget = modelBudgetLine(view);
|
|
104
104
|
if (budget) lines.push(`Budget: ${budget}`);
|
|
105
105
|
if (view.taskList) {
|
|
106
106
|
lines.push(`Tasks: ${buildTaskSummary(view.taskList)}`);
|
|
@@ -145,7 +145,7 @@ pi.registerTool(defineTool({
|
|
|
145
145
|
} else if (view.currentTaskId) {
|
|
146
146
|
lines.push(`Current task: ${view.currentTaskId}`);
|
|
147
147
|
}
|
|
148
|
-
const budget =
|
|
148
|
+
const budget = modelBudgetLine(view);
|
|
149
149
|
if (budget) lines.push(`Budget: ${budget}`);
|
|
150
150
|
if ((view.status === "paused" || view.status === "blocked") && view.pauseReason) {
|
|
151
151
|
lines.push(`Blocker: ${view.pauseReason}`);
|
|
@@ -223,7 +223,7 @@ pi.registerTool(defineTool({
|
|
|
223
223
|
? `\n\nThe objective contains ${derived.length} ordered step${derived.length === 1 ? "" : "s"}; propose them as the task tree with set_goal_tasks if the user wants tracked milestones.`
|
|
224
224
|
: "";
|
|
225
225
|
return {
|
|
226
|
-
content: [{ type: "text", text: `${buildGoalCreatedReport({ objective: created?.objective ?? objective, detailedSummary: detailedSummary(created) })}${bootstrapLine}${otherLine}` }],
|
|
226
|
+
content: [{ type: "text", text: `${buildGoalCreatedReport({ objective: created?.objective ?? objective, detailedSummary: detailedSummary(created), tokenBudget: created?.tokenBudget })}${bootstrapLine}${otherLine}` }],
|
|
227
227
|
details: goalDetails(created),
|
|
228
228
|
terminate: true,
|
|
229
229
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Type } from "@earendil-works/pi-ai";
|
|
2
3
|
import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -6,10 +7,11 @@ import { buildDraftConfirmationText, buildProposalSummary, buildTweakConfirmatio
|
|
|
6
7
|
import { renderConfirmationTasks } from "./goal-task-confirmation.ts";
|
|
7
8
|
import { deriveTasksFromObjective } from "./goal-task-derive.ts";
|
|
8
9
|
import { goalDetails, renderGoalResult } from "./goal-format.ts";
|
|
9
|
-
import {
|
|
10
|
+
import { budgetReached } from "./goal-accounting.ts";
|
|
11
|
+
import { buildGoalCreatedReport, formatGoalBudget } from "./goal-policy.ts";
|
|
10
12
|
import { loadGoalSettings } from "./goal-settings.ts";
|
|
11
13
|
import { DIALOG_UNAVAILABLE_HINT, proposalDialogFailureMessage, formatQuestionnaireAnswers, runGoalQuestionnaire, shouldAutoConfirmProposal, showProposalDialog, type GoalQuestionnaireQuestion, type ProposalDecision } from "./goal-questionnaire.ts";
|
|
12
|
-
import { currentTaskIdIsPending, nowIso, type GoalRecord, type GoalTaskList } from "./goal-record.ts";
|
|
14
|
+
import { currentTaskIdIsPending, nowIso, validateTokenBudgetInput, type GoalRecord, type GoalTaskList } from "./goal-record.ts";
|
|
13
15
|
import type { GoalCore } from "./goal-state.ts";
|
|
14
16
|
import { convertFlatTasks, countTasks, mergeTasksWithExisting, type FlatTaskInput } from "./goal-task-tools.ts";
|
|
15
17
|
import { PROPOSE_DRAFT_TOOL_NAME, QUESTIONNAIRE_TOOL_NAME, QUESTION_TOOL_NAME } from "./goal-tool-names.ts";
|
|
@@ -171,6 +173,8 @@ export async function startGoalDrafting(core: GoalCore, ctx: ExtensionContext, m
|
|
|
171
173
|
"[GOAL TWEAK DRAFT]",
|
|
172
174
|
"The user wants to revise the focused persistent goal. Discuss requirements as needed; do not edit files or start substantive work.",
|
|
173
175
|
"Propose the complete revised objective with propose_goal_draft. Preserve the current goal mode. Include a complete flat task list when the revision changes a decomposable plan; omit tasks to retain the current list.",
|
|
176
|
+
"For budget-only changes preserve the objective, tasks, mode and auditor. token_budget: positive integer sets the total lifetime limit; null removes it; omit to retain it. Change budgets only when requested. Do not add clarification steps when the request is clear.",
|
|
177
|
+
formatGoalBudget(targetGoal?.tokenBudget),
|
|
174
178
|
"Current objective:", targetGoal?.objective ?? "(goal no longer available)",
|
|
175
179
|
"User request:", trimmed || "(ask what they want to change)",
|
|
176
180
|
].join("\n") : goalDraftingPrompt(trimmed, mode);
|
|
@@ -190,7 +194,7 @@ function proposedTaskList(core: GoalCore, ctx: ExtensionContext, tasks: FlatTask
|
|
|
190
194
|
return { ok: true, value: { tasks: converted.tasks, blockCompletion: blockCompletion === true, proposedAt: nowIso() } };
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
export function proposalText(draft: ActiveGoalDraft, objective: string, autoContinue: boolean, taskList: GoalTaskList | undefined, current?: GoalRecord): string {
|
|
197
|
+
export function proposalText(draft: ActiveGoalDraft, objective: string, autoContinue: boolean, taskList: GoalTaskList | undefined, current?: GoalRecord, tokenBudget?: number | null): string {
|
|
194
198
|
const base = draft.mode === "tweak" && current
|
|
195
199
|
? buildTweakConfirmationText({ currentObjective: current.objective, newObjective: objective, changeSummary: draft.originalTopic || "Goal revised through guided drafting.", sisyphus: current.sisyphus, tasks: taskList?.tasks })
|
|
196
200
|
: buildDraftConfirmationText({ focus: draft.mode === "sisyphus" ? "sisyphus" : "goal", originalTopic: draft.originalTopic, objective, autoContinue });
|
|
@@ -219,7 +223,7 @@ export function proposalText(draft: ActiveGoalDraft, objective: string, autoCont
|
|
|
219
223
|
tasksText = "\n\nTasks derived from the objective (confirm or ask the agent to adjust):\n" + renderConfirmationTasks(derived, 0).join("\n");
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
|
-
return
|
|
226
|
+
return `${draft.mode === "tweak" ? `Current ${formatGoalBudget(current?.tokenBudget)}\nProposed ${formatGoalBudget(tokenBudget === undefined ? current?.tokenBudget : tokenBudget ?? undefined)}` : formatGoalBudget(tokenBudget ?? undefined)}\n\n${base}${tasksText}`;
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
function flatTaskSchema() {
|
|
@@ -310,7 +314,8 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
310
314
|
"Confirmation creates or revises the goal atomically. Continue Chatting leaves drafting active for refinement.",
|
|
311
315
|
],
|
|
312
316
|
parameters: Type.Object({
|
|
313
|
-
objective: Type.String({ description: "Complete proposed objective
|
|
317
|
+
objective: Type.String({ description: "Complete proposed objective; preserve it exactly for budget-only tweaks." }),
|
|
318
|
+
token_budget: Type.Optional(Type.Union([Type.Integer({minimum: 1}), Type.Null()], {description: "Only on user request: positive whole-token lifetime limit; null removes it. Omit to retain the current tweak budget or create without one."})),
|
|
314
319
|
auto_continue: Type.Optional(Type.Boolean({ description: "Defaults to true." })),
|
|
315
320
|
sisyphus: Type.Optional(Type.Boolean({ description: "Must match the user-selected goal mode." })),
|
|
316
321
|
tasks: Type.Optional(flatTaskSchema()),
|
|
@@ -320,6 +325,10 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
320
325
|
async execute(_id, params, _signal, _update, ctx) {
|
|
321
326
|
const draft = activeDraft(core);
|
|
322
327
|
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) };
|
|
328
|
+
if (params.token_budget !== undefined && params.token_budget !== null) {
|
|
329
|
+
const validation = validateTokenBudgetInput(params.token_budget);
|
|
330
|
+
if (!validation.ok) return {content: [{type: "text", text: validation.message}], details: goalDetails(core.state.goal)};
|
|
331
|
+
}
|
|
323
332
|
const objective = params.objective.trim();
|
|
324
333
|
if (!objective) return { content: [{ type: "text", text: "The proposed objective must be at least 1 character." }], details: goalDetails(core.state.goal) };
|
|
325
334
|
const objectiveMaxChars = loadGoalSettings(ctx.cwd).objectiveMaxChars ?? 0;
|
|
@@ -328,9 +337,16 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
328
337
|
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) };
|
|
329
338
|
const taskResult = proposedTaskList(core, ctx, params.tasks as FlatTaskInput[] | undefined, params.block_completion);
|
|
330
339
|
if (!taskResult.ok) return { content: [{ type: "text", text: taskResult.message }], details: goalDetails(core.state.goal) };
|
|
340
|
+
const flushError = core.goalService.flushForAudit(ctx);
|
|
341
|
+
if (flushError || core.goalService.isTurnBuffered()) return {content: [{type: "text", text: "Proposal not saved: " + (flushError ?? "goal changes are still buffered")}], details: goalDetails(core.state.goal)};
|
|
331
342
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
332
343
|
const target = draft.mode === "tweak" ? core.state.goal : undefined;
|
|
333
344
|
if (draft.mode === "tweak" && (!target || target.id !== draft.targetGoalId)) return { content: [{ type: "text", text: "The goal changed while drafting; review it and start /goal-tweak again." }], details: goalDetails(core.state.goal) };
|
|
345
|
+
if (target?.status === "complete") return {content: [{type: "text", text: "Completed goals cannot be tweaked."}], details: goalDetails(target)};
|
|
346
|
+
const proposalToken = target ? core.focusedOperationToken(target.id) : undefined;
|
|
347
|
+
const proposalRevision = target?.revision ?? 0;
|
|
348
|
+
const proposedBudget = params.token_budget === undefined ? target?.tokenBudget : params.token_budget ?? undefined;
|
|
349
|
+
const budgetSummary = target ? `Current ${formatGoalBudget(target.tokenBudget)}\nProposed ${formatGoalBudget(proposedBudget)}` : formatGoalBudget(proposedBudget);
|
|
334
350
|
if (draft.mode === "sisyphus" && !sisyphusObjectiveSufficient(objective)) return { content: [{ type: "text", text: "A Sisyphus goal needs ordered steps with explicit per-step done criteria. Refine the objective with numbered steps (1) ..., 2) ...) or Step N: blocks before proposing again." }], details: goalDetails(core.state.goal) };
|
|
335
351
|
let confirmation: { decision: ProposalDecision; auditorEnabled: boolean; unavailable: boolean };
|
|
336
352
|
if (shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM })) {
|
|
@@ -338,7 +354,7 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
338
354
|
} else {
|
|
339
355
|
core.enterGoalModal();
|
|
340
356
|
try {
|
|
341
|
-
confirmation = await showProposalDialog(ctx, proposalText(draft, objective, params.auto_continue !== false, taskResult.value, target ?? undefined), draft.mode === "sisyphus" ? "sisyphus" : "goal", draft.auditorEnabled);
|
|
357
|
+
confirmation = await showProposalDialog(ctx, proposalText(draft, objective, params.auto_continue !== false, taskResult.value, target ?? undefined, params.token_budget), draft.mode === "sisyphus" ? "sisyphus" : "goal", draft.auditorEnabled);
|
|
342
358
|
} catch (error) {
|
|
343
359
|
return { content: [{ type: "text", text: `${proposalDialogFailureMessage(error)} Do not retry the dialog until the host issue is resolved.` }], details: goalDetails(core.state.goal) };
|
|
344
360
|
} finally {
|
|
@@ -350,7 +366,7 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
350
366
|
// §14: the durable proposal summary is part of the transcript for
|
|
351
367
|
// EVERY outcome (confirm / continue refining / cancel), so the user
|
|
352
368
|
// can review the proposal after the dialog closes.
|
|
353
|
-
const summary = buildProposalSummary({
|
|
369
|
+
const summary = budgetSummary + "\n\n" + buildProposalSummary({
|
|
354
370
|
objective: extracted.objective,
|
|
355
371
|
taskList: taskResult.value,
|
|
356
372
|
verificationContract: extracted.verificationContract,
|
|
@@ -385,7 +401,7 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
385
401
|
const derived = deriveTasksFromObjective(extracted.objective);
|
|
386
402
|
return derived && derived.length > 0 ? { tasks: derived, blockCompletion: false, proposedAt: nowIso() } : undefined;
|
|
387
403
|
})();
|
|
388
|
-
core.replaceGoal({ objective: extracted.objective, autoContinue: params.auto_continue !== false, sisyphus: expectedSisyphus, taskList: effectiveTaskList, skipAuditor }, ctx, true, extracted.verificationContract);
|
|
404
|
+
core.replaceGoal({ objective: extracted.objective, autoContinue: params.auto_continue !== false, sisyphus: expectedSisyphus, taskList: effectiveTaskList, skipAuditor }, ctx, true, extracted.verificationContract, proposedBudget);
|
|
389
405
|
clearGoalDrafting(core, ctx);
|
|
390
406
|
const created = core.state.goal;
|
|
391
407
|
return { content: [{ type: "text", text: `${summary}\n\n${buildGoalCreatedReport({
|
|
@@ -401,7 +417,10 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
401
417
|
})}` }], details: goalDetails(core.state.goal), terminate: true };
|
|
402
418
|
}
|
|
403
419
|
if (!target) return { content: [{ type: "text", text: "The goal changed while drafting; review it and start /goal-tweak again." }], details: goalDetails(core.state.goal) };
|
|
404
|
-
const token =
|
|
420
|
+
const token = proposalToken!;
|
|
421
|
+
const finalFlushError = core.goalService.flushForAudit(ctx);
|
|
422
|
+
core.reconcileFocusedGoalFromDisk(ctx);
|
|
423
|
+
if (finalFlushError || core.goalService.isTurnBuffered() || !core.isFocusedOperationCurrent(token) || core.state.goal?.revision !== proposalRevision || core.state.goal?.status === "complete") return {content: [{type: "text", text: "Goal tweak was not applied: the goal changed during confirmation. Review and propose again."}], details: goalDetails(core.state.goal)};
|
|
405
424
|
const now = nowIso();
|
|
406
425
|
// §7.5 merge: a goal tweak must never change the status of steps that
|
|
407
426
|
// persist across the tweak. The proposed list merges into the goal's
|
|
@@ -427,16 +446,24 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
427
446
|
: goal.currentTaskId;
|
|
428
447
|
// §tweak-resume: resume a paused/blocked goal (mirror the
|
|
429
448
|
// /goal-resume transition: status active + pause metadata
|
|
430
|
-
// cleared)
|
|
431
|
-
//
|
|
432
|
-
const
|
|
449
|
+
// cleared). A budget-limited goal needs an explicit budget
|
|
450
|
+
// change that clears the resource gate; preserve scheduler limits.
|
|
451
|
+
const exhausted = budgetReached({...goal, tokenBudget: proposedBudget});
|
|
452
|
+
const wantsResume = goal.status === "paused" || goal.status === "blocked" || (goal.status === "budget_limited" && params.token_budget !== undefined && !exhausted);
|
|
453
|
+
const scheduler = goal.scheduler;
|
|
454
|
+
const owned = !scheduler || (scheduler.owner === (ctx.sessionManager.getSessionId() || "unknown-session") && !["interrupted", "claimed"].includes(scheduler.phase));
|
|
455
|
+
const limit = loadGoalSettings(ctx.cwd).maxAutonomousRuns;
|
|
456
|
+
const allowance = limit === undefined || (scheduler?.used ?? 0) < limit;
|
|
457
|
+
const stalled = wantsResume && !exhausted && owned && allowance;
|
|
433
458
|
if (stalled) resumed = true;
|
|
434
459
|
return {
|
|
435
460
|
...goal,
|
|
461
|
+
tokenBudget: proposedBudget,
|
|
462
|
+
...(exhausted && owned && scheduler ? {scheduler: {...scheduler, generation: randomUUID(), phase: "idle" as const, decision: undefined, dispatch: undefined, wait: undefined}} : {}),
|
|
436
463
|
objective: extracted.objective,
|
|
437
464
|
verificationContract: extracted.verificationContract ?? goal.verificationContract,
|
|
438
465
|
taskList, currentTaskId, skipAuditor,
|
|
439
|
-
status: stalled ? "active" : goal.status,
|
|
466
|
+
status: exhausted && (goal.status === "active" || goal.status === "budget_limited" || wantsResume) ? "budget_limited" : stalled ? "active" : wantsResume ? "paused" : goal.status,
|
|
440
467
|
autoContinue: stalled ? true : goal.autoContinue,
|
|
441
468
|
stopReason: stalled ? undefined : goal.stopReason,
|
|
442
469
|
pauseReason: stalled ? undefined : goal.pauseReason,
|
|
@@ -444,7 +471,7 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
444
471
|
updatedAt: now,
|
|
445
472
|
};
|
|
446
473
|
},
|
|
447
|
-
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 }] : [])],
|
|
474
|
+
ledger: (written) => [...(target.tokenBudget !== written.tokenBudget ? [{type: "goal_budget_changed" as const, goalId: written.id, oldBudget: target.tokenBudget ?? null, newBudget: written.tokenBudget ?? null, tokensUsed: written.usage.tokensUsed, at: written.updatedAt}] : []), ...(target.status !== "budget_limited" && written.status === "budget_limited" ? [{type: "goal_budget_limited" as const, goalId: written.id, budget: written.tokenBudget!, tokensUsed: written.usage.tokensUsed, at: written.updatedAt}] : []), { 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 }] : [])],
|
|
448
475
|
});
|
|
449
476
|
if (!result.ok) return { content: [{ type: "text", text: "Goal tweak was not applied: " + result.message }], details: goalDetails(core.state.goal) };
|
|
450
477
|
if (resumed) {
|
|
@@ -455,15 +482,20 @@ export function registerDraftingTools(core: GoalCore): void {
|
|
|
455
482
|
}
|
|
456
483
|
}
|
|
457
484
|
core.clearContinuationState();
|
|
485
|
+
if (!budgetReached(result.goal)) core.runtime.consumePostBudgetReminder();
|
|
486
|
+
else { core.runtime.armPostBudgetReminder(); core.clearActiveAccounting(); core.scheduler.cancelTimer(); }
|
|
458
487
|
core.updateUI(ctx);
|
|
488
|
+
let schedulingNote = "";
|
|
459
489
|
if (resumed) {
|
|
460
490
|
// Resume glue mirrors replaceGoal: restart accounting and queue
|
|
461
491
|
// the auto-continuation so the revived goal keeps going.
|
|
462
492
|
core.beginAccounting();
|
|
493
|
+
const scheduling = core.scheduler.declare(ctx, {kind: "ready", next_action: "Continue the goal after the confirmed tweak."});
|
|
494
|
+
if (!scheduling.terminate) schedulingNote = "\n" + scheduling.content.filter(item => item.type === "text").map(item => item.text).join("\n");
|
|
463
495
|
core.queueContinuation(ctx, true);
|
|
464
496
|
}
|
|
465
497
|
clearGoalDrafting(core, ctx);
|
|
466
|
-
return { content: [{ type: "text", text: `${summary}\n\nGoal tweak confirmed and applied
|
|
498
|
+
return { content: [{ type: "text", text: `${summary}\n\nGoal tweak confirmed and applied.\n${formatGoalBudget(result.goal.tokenBudget)}${schedulingNote}${result.goal.status === "budget_limited" ? "\nConsumed tokens already meet this budget; the goal remains budget-limited." : result.goal.status === "paused" ? "\nGoal remains paused. Use /goal-resume to continue when scheduling permits." : ""}` }], details: goalDetails(result.goal), terminate: true };
|
|
467
499
|
},
|
|
468
500
|
renderCall(args, theme) {
|
|
469
501
|
// §proposal-presentation: the tool-call display lands in the terminal
|