killeros 2.1.27 → 2.1.28
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/CHANGELOG.md +18 -0
- package/README.md +3 -3
- package/killeros/activity.ts +16 -7
- package/killeros/footer.ts +6 -4
- package/killeros/goal-interface.ts +195 -48
- package/killeros/goal-runtime.ts +190 -44
- package/killeros/goal-settlement.ts +129 -30
- package/killeros/goal-state.ts +205 -32
- package/killeros/runtime.ts +54 -0
- package/killeros/worked-for.ts +1 -1
- package/package.json +1 -1
- package/themes/killeros.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.28] - 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Simplified goal presentation without changing goal behavior: transcript history shows one iconless event row per lifecycle change (`Goal started`, `Goal replaced`, `Goal paused`, `Goal resumed`, `Goal cleared`), progress and blocker outcomes render once through the goal tool result, the footer shows compact `/goal active · 1/20 · 10s` status, the `/goal` panel leads with status then objective then `Pause goal` / `Resume goal` / `Clear goal` actions, and duplicate TUI success notifications are suppressed while RPC confirmations remain. Active-goal labels use muted teal `#6FAEB2` through the packaged theme's extension-label role.
|
|
12
|
+
- Removed the legacy `✻` marker from version-1 `Worked for …` transcript rows; stored entries are unchanged and remain readable.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Appended the selected model ID as a gray `· <model-id>` suffix to every TUI activity working message, retained for the full request cycle and falling back to `unknown model` when no usable ID exists.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Kept resumed blocked goals and goals paused by truncated provider errors readable after reload.
|
|
21
|
+
- Made goal continuation explicit: each settled turn now needs one accepted `continue`, `complete`, or blocker decision. Missing decisions, repeated continuation reports, unavailable goal tools, and runtime failures pause without scheduling another turn; compaction recovery resumes the same logical turn.
|
|
22
|
+
- Preserved accepted goal decisions across automatic compaction: a `continue` or blocker audit recorded before compaction now authorizes exactly one next turn instead of being discarded and re-decided. Duplicate compaction and settlement callbacks cannot start a second turn.
|
|
23
|
+
- Scoped goal pause reasons to the current paused state: resume and manual pause no longer surface an earlier automatic reason, terminal states keep no pause reason, and contradictory persisted states fail closed instead of starting automatic work.
|
|
24
|
+
|
|
7
25
|
## [2.1.27] - 2026-09-12
|
|
8
26
|
|
|
9
27
|
### Added
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
5
5
|
## What you get
|
|
6
6
|
|
|
7
7
|
- A custom TUI: startup masthead with versions, model, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
|
|
8
|
-
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals pause after 20 turns; `/goal resume` grants another 20.
|
|
8
|
+
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. Each turn must record `continue` with evidence and one next action, `complete`, or the existing blocker decision; otherwise the goal pauses. New goals pause after 20 turns; `/goal resume` grants another 20.
|
|
9
9
|
- `/codex-fast`: toggles the `priority` service tier on Codex requests or reports its status.
|
|
10
10
|
- `/handoff`: starts a fresh linked session carrying visible continuation context.
|
|
11
11
|
- Automatic context compaction when remaining tokens drop below 15% of the window (configurable).
|
|
@@ -32,7 +32,7 @@ Or from GitHub:
|
|
|
32
32
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Pin a release by appending its tag, for example `@v2.1.
|
|
35
|
+
Pin a release by appending its tag, for example `@v2.1.28`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
36
36
|
|
|
37
37
|
## Commands
|
|
38
38
|
|
|
@@ -85,7 +85,7 @@ A direct quoted file target binds silent file proof:
|
|
|
85
85
|
/goal Fix `killeros/footer.ts`, verified by npm test
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
KillerOS captures the file baseline at goal start and only completes when the file is created or changed. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Restored goals keep their persisted limit.
|
|
88
|
+
KillerOS captures the file baseline at goal start and only completes when the file is created or changed. A normal response never continues a goal by itself: the agent must record `continue`, `complete`, or a blocker decision through `killeros_goal_update`. Repeated continuation reports and unavailable goal tools pause the goal. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Restored goals keep their persisted limit.
|
|
89
89
|
|
|
90
90
|
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
91
91
|
|
package/killeros/activity.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { modelDisplayName } from "./display.ts";
|
|
3
4
|
|
|
4
5
|
export type ActivityMessage =
|
|
5
6
|
| { kind: "prompt" }
|
|
@@ -12,7 +13,7 @@ function safeToolName(toolName: string): string {
|
|
|
12
13
|
return truncateToWidth(normalized || "tool", 32, "…");
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function formatActivityMessage(message: ActivityMessage, theme: Theme): string {
|
|
16
|
+
export function formatActivityMessage(message: ActivityMessage, theme: Theme, modelId?: string): string {
|
|
16
17
|
let verb: string;
|
|
17
18
|
let detail: string;
|
|
18
19
|
|
|
@@ -58,38 +59,46 @@ export function formatActivityMessage(message: ActivityMessage, theme: Theme): s
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
const suffix = modelDisplayName({ id: modelId }) || "unknown model";
|
|
63
|
+
return `${theme.fg("accent", verb)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · ${detail})`)}${theme.fg("dim", ` · ${suffix}`)}`;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export function registerRequestActivity(pi: ExtensionAPI): void {
|
|
65
67
|
let active = false;
|
|
68
|
+
let activeModel: string | undefined;
|
|
69
|
+
|
|
70
|
+
const render = (message: ActivityMessage, ctx: ExtensionContext): void => {
|
|
71
|
+
ctx.ui.setWorkingMessage(formatActivityMessage(message, ctx.ui.theme, activeModel));
|
|
72
|
+
};
|
|
66
73
|
|
|
67
74
|
const clear = (ctx?: ExtensionContext): void => {
|
|
75
|
+
active = false;
|
|
76
|
+
activeModel = undefined;
|
|
68
77
|
if (ctx?.mode === "tui") {
|
|
69
78
|
ctx.ui.setWorkingMessage();
|
|
70
79
|
}
|
|
71
|
-
active = false;
|
|
72
80
|
};
|
|
73
81
|
|
|
74
82
|
pi.on("agent_start", (_event, ctx) => {
|
|
75
83
|
if (ctx.mode !== "tui") return;
|
|
84
|
+
if (!active) activeModel = ctx.model?.id;
|
|
76
85
|
active = true;
|
|
77
|
-
|
|
86
|
+
render({ kind: "prompt" }, ctx);
|
|
78
87
|
});
|
|
79
88
|
|
|
80
89
|
pi.on("tool_execution_start", (event, ctx) => {
|
|
81
90
|
if (ctx.mode !== "tui" || !active) return;
|
|
82
|
-
|
|
91
|
+
render({ kind: "tool", toolName: event.toolName }, ctx);
|
|
83
92
|
});
|
|
84
93
|
|
|
85
94
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
86
95
|
if (ctx.mode !== "tui" || !active) return;
|
|
87
|
-
|
|
96
|
+
render({ kind: "tool-result", failed: event.isError }, ctx);
|
|
88
97
|
});
|
|
89
98
|
|
|
90
99
|
pi.on("message_update", (event, ctx) => {
|
|
91
100
|
if (ctx.mode !== "tui" || !active || event.assistantMessageEvent.type !== "text_start") return;
|
|
92
|
-
|
|
101
|
+
render({ kind: "responding" }, ctx);
|
|
93
102
|
});
|
|
94
103
|
|
|
95
104
|
pi.on("agent_settled", (_event, ctx) => {
|
package/killeros/footer.ts
CHANGED
|
@@ -383,11 +383,13 @@ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
|
|
|
383
383
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
384
384
|
if (!state) return "";
|
|
385
385
|
if (state.status === "active") {
|
|
386
|
-
const
|
|
387
|
-
|
|
386
|
+
const detail = state.maxTurns === undefined
|
|
387
|
+
? `${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))}`
|
|
388
|
+
: `${state.turns}/${state.maxTurns} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))}`;
|
|
389
|
+
return `${theme.fg("customMessageLabel", "/goal active")}${theme.fg("dim", ` · ${detail}`)}`;
|
|
388
390
|
}
|
|
389
|
-
if (state.status === "paused") return theme.fg("warning", "/goal
|
|
390
|
-
if (state.status === "blocked") return theme.fg("error", "/goal
|
|
391
|
+
if (state.status === "paused") return theme.fg("warning", "/goal paused");
|
|
392
|
+
if (state.status === "blocked") return theme.fg("error", "/goal blocked");
|
|
391
393
|
return "";
|
|
392
394
|
}
|
|
393
395
|
|
|
@@ -7,19 +7,24 @@ import { formatTime, formatTokens } from "./display.ts";
|
|
|
7
7
|
import { reportError } from "./errors.ts";
|
|
8
8
|
import { parseGoalCommand } from "./goal-command.ts";
|
|
9
9
|
import { GOAL_ENTRY_TYPE, GOAL_UPDATE_TOOL, isGoalModeSupported, isSavedSession, pauseGoalAfterFailure, persistGoalState, scheduleGoalContinuation, stopGoalRun, sumGoalTokens, syncGoalUpdateTool, transitionGoal, type GoalEntryData } from "./goal-runtime.ts";
|
|
10
|
-
import { checkpointPausedGoalState, createNewGoalState, DEFAULT_GOAL_MAX_TURNS, GOAL_MAX_TURNS, goalElapsedMilliseconds, GOAL_VERSION, inferGoalVerification, parseGoalState,
|
|
10
|
+
import { checkpointPausedGoalState, createNewGoalState, DEFAULT_GOAL_MAX_TURNS, GOAL_EVIDENCE_LIMIT, GOAL_MAX_TURNS, goalElapsedMilliseconds, GOAL_VERSION, inferGoalVerification, normalizeGoalText, parseGoalState, recordGoalDecision, transitionGoalState, verifyGoalDeliverable } from "./goal-state.ts";
|
|
11
11
|
import type { GoalRuntime, GoalState, GoalStatus } from "./runtime.ts";
|
|
12
12
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
13
13
|
|
|
14
14
|
const GoalUpdateParams = Type.Object({
|
|
15
|
-
status: StringEnum(["complete", "blocked"] as const, {
|
|
16
|
-
description: "
|
|
15
|
+
status: StringEnum(["complete", "continue", "blocked"] as const, {
|
|
16
|
+
description: "Record exactly one active-goal decision: complete, continue, or blocked",
|
|
17
17
|
}),
|
|
18
18
|
evidence: Type.String({
|
|
19
19
|
minLength: 1,
|
|
20
|
-
maxLength:
|
|
21
|
-
description: "
|
|
20
|
+
maxLength: GOAL_EVIDENCE_LIMIT,
|
|
21
|
+
description: "Concrete current-turn evidence for completion, progress, or the repeated blocker",
|
|
22
22
|
}),
|
|
23
|
+
nextAction: Type.Optional(Type.String({
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: GOAL_EVIDENCE_LIMIT,
|
|
26
|
+
description: "One concrete action toward the unchanged objective when status is continue",
|
|
27
|
+
})),
|
|
23
28
|
blockerKey: Type.Optional(Type.String({
|
|
24
29
|
minLength: 1,
|
|
25
30
|
maxLength: 120,
|
|
@@ -29,21 +34,18 @@ const GoalUpdateParams = Type.Object({
|
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
interface GoalUpdateDetails {
|
|
32
|
-
status: "complete" | "blocked" | "blocker-audit";
|
|
37
|
+
status: "complete" | "continue" | "blocked" | "blocker-audit";
|
|
33
38
|
evidence: string;
|
|
39
|
+
nextAction?: string;
|
|
34
40
|
verification?: "file" | "model-reported";
|
|
35
41
|
blockerKey?: string;
|
|
36
42
|
streak?: number;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
function goalStatusLabel(status: GoalStatus): string {
|
|
40
|
-
return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
45
|
function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "pause" | "resume" | "clear" }> {
|
|
44
|
-
if (status === "active") return [{ label: "Pause
|
|
46
|
+
if (status === "active") return [{ label: "Pause goal", control: "pause" }, { label: "Clear goal", control: "clear" }];
|
|
45
47
|
if (status === "paused" || status === "blocked") {
|
|
46
|
-
return [{ label: "Resume
|
|
48
|
+
return [{ label: "Resume goal", control: "resume" }, { label: "Clear goal", control: "clear" }];
|
|
47
49
|
}
|
|
48
50
|
return [{ label: "Clear goal", control: "clear" }];
|
|
49
51
|
}
|
|
@@ -54,11 +56,33 @@ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
|
54
56
|
? `${state.turns} turn${state.turns === 1 ? "" : "s"}`
|
|
55
57
|
: `${state.turns}/${state.maxTurns} turns`;
|
|
56
58
|
const lines = [
|
|
57
|
-
`Goal ${
|
|
58
|
-
...(state.verification === undefined ? [] : [`Deliverable: ${state.verification.path}`]),
|
|
59
|
+
`Goal ${state.status} · ${turns} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))} · ${formatTokens(usedTokens)} tokens`,
|
|
59
60
|
state.objective,
|
|
61
|
+
...(state.verification === undefined ? [] : [`Deliverable · ${state.verification.path}`]),
|
|
60
62
|
];
|
|
61
|
-
|
|
63
|
+
const decision = state.turnDecision ?? state.lastDecision;
|
|
64
|
+
if (decision?.kind === "continue") {
|
|
65
|
+
lines.push(`Progress · model-reported on turn ${decision.turn}`);
|
|
66
|
+
lines.push(`Next · ${decision.nextAction}`);
|
|
67
|
+
} else if (decision?.kind === "blocker-audit") {
|
|
68
|
+
lines.push(`Blocker audit · ${decision.streak}/3 on turn ${decision.turn}`);
|
|
69
|
+
lines.push(`Evidence · ${decision.evidence}`);
|
|
70
|
+
} else if (decision?.kind === "blocked") {
|
|
71
|
+
lines.push(`Evidence · ${decision.evidence}`);
|
|
72
|
+
} else if (decision?.kind === "complete") {
|
|
73
|
+
lines.push(`Completion · ${decision.verification === "file" ? `verified file ${state.verification?.path ?? "deliverable"}` : "model-reported"}`);
|
|
74
|
+
lines.push(`Evidence · ${decision.evidence}`);
|
|
75
|
+
}
|
|
76
|
+
if (state.status === "paused") {
|
|
77
|
+
lines.push(state.turns === 0 ? "Paused at · before turn 1" : `Paused at · turn ${state.turns}`);
|
|
78
|
+
const reason = state.stopReason ?? state.result;
|
|
79
|
+
if (reason) lines.push(`Reason · ${reason}`);
|
|
80
|
+
if (state.stopReason === "repeated continue report" && state.lastContinueReport) {
|
|
81
|
+
lines.push(`Repeated turns · ${state.lastContinueReport.turn} and ${state.turns}`);
|
|
82
|
+
}
|
|
83
|
+
} else if (state.result && decision?.kind !== "complete" && decision?.kind !== "blocked") {
|
|
84
|
+
lines.push(`Result · ${state.result}`);
|
|
85
|
+
}
|
|
62
86
|
return safeTerminalText(lines.join("\n"));
|
|
63
87
|
}
|
|
64
88
|
|
|
@@ -68,24 +92,63 @@ export function registerGoalInterface(
|
|
|
68
92
|
): void {
|
|
69
93
|
pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry, options, theme) => {
|
|
70
94
|
const data = entry.data;
|
|
71
|
-
if (!data || data.version !== GOAL_VERSION
|
|
95
|
+
if (!data || data.version !== GOAL_VERSION) return undefined;
|
|
72
96
|
if (data.event === "clear" || data.state === null) return new Text(theme.fg("dim", "Goal cleared"), 0, 0);
|
|
97
|
+
const event = data.event;
|
|
98
|
+
if (event !== "set" && event !== "replace" && event !== "pause" && event !== "resume" && event !== "limit" && event !== "error") return undefined;
|
|
73
99
|
const state = parseGoalState(data.state);
|
|
74
100
|
if (!state) return undefined;
|
|
75
|
-
const
|
|
76
|
-
const color: ThemeColor = state.status === "active" ? "accent" : state.status === "paused" ? "warning" : state.status === "blocked" ? "error" : "success";
|
|
77
|
-
const status = theme.fg(color, `${icon} Goal ${state.status}`);
|
|
101
|
+
const reason = state.stopReason ?? state.result;
|
|
78
102
|
const objective = safeTerminalText(state.objective);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
let label: string;
|
|
104
|
+
let detail: string;
|
|
105
|
+
let color: ThemeColor;
|
|
106
|
+
switch (event) {
|
|
107
|
+
case "set":
|
|
108
|
+
label = "Goal started";
|
|
109
|
+
detail = objective;
|
|
110
|
+
color = "customMessageLabel";
|
|
111
|
+
break;
|
|
112
|
+
case "replace":
|
|
113
|
+
label = "Goal replaced";
|
|
114
|
+
detail = objective;
|
|
115
|
+
color = "customMessageLabel";
|
|
116
|
+
break;
|
|
117
|
+
case "resume":
|
|
118
|
+
label = "Goal resumed";
|
|
119
|
+
detail = objective;
|
|
120
|
+
color = "customMessageLabel";
|
|
121
|
+
break;
|
|
122
|
+
case "pause":
|
|
123
|
+
label = "Goal paused";
|
|
124
|
+
detail = reason ?? objective;
|
|
125
|
+
color = "warning";
|
|
126
|
+
break;
|
|
127
|
+
case "limit":
|
|
128
|
+
label = "Goal paused";
|
|
129
|
+
detail = reason ?? "";
|
|
130
|
+
color = "warning";
|
|
131
|
+
break;
|
|
132
|
+
case "error":
|
|
133
|
+
label = "Goal paused";
|
|
134
|
+
detail = reason ?? "";
|
|
135
|
+
color = "error";
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const safeDetail = safeTerminalText(detail);
|
|
139
|
+
const status = theme.fg(color, label);
|
|
140
|
+
if (!options.expanded) {
|
|
141
|
+
return new BoundedText(safeDetail ? `${status}${theme.fg("dim", " · ")}${safeDetail}` : status, 1);
|
|
142
|
+
}
|
|
143
|
+
const lines = [status, objective];
|
|
144
|
+
if (reason && reason !== objective) lines.push(theme.fg("muted", safeTerminalText(reason)));
|
|
82
145
|
return new BoundedText(lines.join("\n"));
|
|
83
146
|
});
|
|
84
147
|
|
|
85
148
|
pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
|
|
86
149
|
name: GOAL_UPDATE_TOOL,
|
|
87
150
|
label: "Goal update",
|
|
88
|
-
description: "
|
|
151
|
+
description: "Record exactly one active-goal decision: complete after verification, continue with evidence and one next action, or audit the same blocker before blocking it.",
|
|
89
152
|
parameters: GoalUpdateParams,
|
|
90
153
|
executionMode: "sequential",
|
|
91
154
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -94,42 +157,110 @@ export function registerGoalInterface(
|
|
|
94
157
|
if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
|
|
95
158
|
const state = runtime.state;
|
|
96
159
|
if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
|
|
97
|
-
|
|
98
|
-
|
|
160
|
+
if (!runtime.goalTurnInFlight
|
|
161
|
+
|| runtime.goalTurn?.turn !== state.turns
|
|
162
|
+
|| runtime.goalTurn.revision !== state.revision) {
|
|
163
|
+
throw new Error("A goal decision can only be recorded during an active KillerOS goal turn");
|
|
164
|
+
}
|
|
165
|
+
if (state.turnDecision !== undefined) {
|
|
166
|
+
throw new Error("Only one goal decision may be accepted per logical goal turn");
|
|
167
|
+
}
|
|
168
|
+
if (params.status !== "complete" && params.status !== "continue" && params.status !== "blocked") {
|
|
169
|
+
throw new Error("Goal update status is invalid");
|
|
170
|
+
}
|
|
171
|
+
if (typeof params.evidence !== "string") throw new Error("Goal evidence must be text");
|
|
172
|
+
const evidence = normalizeGoalText(params.evidence, GOAL_EVIDENCE_LIMIT, "Goal evidence");
|
|
173
|
+
|
|
99
174
|
if (params.status === "complete") {
|
|
100
175
|
if (state.verification) await verifyGoalDeliverable(state.verification);
|
|
101
176
|
signal?.throwIfAborted();
|
|
102
|
-
if (runtime.state !== state
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
177
|
+
if (runtime.state !== state
|
|
178
|
+
|| runtime.goalTurn?.turn !== state.turns
|
|
179
|
+
|| runtime.goalTurn.revision !== state.revision
|
|
180
|
+
|| state.turnDecision !== undefined
|
|
181
|
+
|| !runtime.goalTurnInFlight) throw new Error("Goal changed while completion was being verified");
|
|
182
|
+
const verification: "file" | "model-reported" = state.verification ? "file" : "model-reported";
|
|
183
|
+
const decision = { kind: "complete" as const, turn: state.turns, evidence, verification };
|
|
184
|
+
try {
|
|
185
|
+
transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true, decision });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `goal completion could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
106
190
|
const text = state.verification
|
|
107
|
-
? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${
|
|
108
|
-
: `Goal marked complete (model-reported): ${
|
|
191
|
+
? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${evidence}`
|
|
192
|
+
: `Goal marked complete (model-reported): ${evidence}`;
|
|
109
193
|
return {
|
|
110
194
|
content: [{ type: "text", text }],
|
|
111
195
|
details: { status: "complete", evidence, verification },
|
|
112
196
|
};
|
|
113
197
|
}
|
|
114
|
-
|
|
198
|
+
|
|
199
|
+
if (params.status === "continue") {
|
|
200
|
+
if (typeof params.nextAction !== "string") throw new Error("A continue decision requires a nextAction");
|
|
201
|
+
const nextAction = normalizeGoalText(params.nextAction, GOAL_EVIDENCE_LIMIT, "Goal nextAction");
|
|
202
|
+
const previous = state.lastContinueReport;
|
|
203
|
+
if (previous?.turn === state.turns - 1
|
|
204
|
+
&& previous.evidence === evidence
|
|
205
|
+
&& previous.nextAction === nextAction) {
|
|
206
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "repeated continue report", "Run /goal resume only after choosing a different next action.", false);
|
|
207
|
+
ctx.ui.notify(
|
|
208
|
+
`Goal paused: repeated continue report on turns ${previous.turn} and ${state.turns}\nRun /goal resume only after choosing a different next action.`,
|
|
209
|
+
"error",
|
|
210
|
+
);
|
|
211
|
+
throw new Error("repeated continue report");
|
|
212
|
+
}
|
|
213
|
+
const decision = { kind: "continue" as const, turn: state.turns, evidence, nextAction };
|
|
214
|
+
try {
|
|
215
|
+
const next = recordGoalDecision(state, decision, Date.now());
|
|
216
|
+
persistGoalState(pi, runtime, "continue", next);
|
|
217
|
+
runtime.goalTurn = { turn: next.turns, revision: next.revision };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `goal decision could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: `Model-reported progress recorded; the goal remains active: ${evidence}` }],
|
|
224
|
+
details: { status: "continue", evidence, nextAction },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
115
228
|
const blockerKey = params.blockerKey;
|
|
116
229
|
if (!blockerKey || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(blockerKey)) {
|
|
117
230
|
throw new Error("A blocked goal update requires a stable lowercase blockerKey");
|
|
118
231
|
}
|
|
119
232
|
const previous = state.blockerAudit;
|
|
120
|
-
|
|
233
|
+
if (previous?.lastTurn === state.turns) {
|
|
234
|
+
throw new Error("Only one goal decision may be accepted per logical goal turn");
|
|
235
|
+
}
|
|
121
236
|
const consecutive = previous?.key === blockerKey && previous.lastTurn === state.turns - 1;
|
|
122
|
-
const streak =
|
|
237
|
+
const streak = consecutive ? previous.streak + 1 : 1;
|
|
123
238
|
const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns, evidence };
|
|
124
239
|
if (streak < 3) {
|
|
125
|
-
const
|
|
126
|
-
|
|
240
|
+
const decision = { kind: "blocker-audit" as const, turn: state.turns, blockerKey, streak, evidence };
|
|
241
|
+
try {
|
|
242
|
+
const next = {
|
|
243
|
+
...recordGoalDecision(state, decision, Date.now()),
|
|
244
|
+
blockerAudit,
|
|
245
|
+
};
|
|
246
|
+
persistGoalState(pi, runtime, "blocker-audit", next);
|
|
247
|
+
runtime.goalTurn = { turn: next.turns, revision: next.revision };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `blocker decision could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
127
252
|
return {
|
|
128
253
|
content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
|
|
129
254
|
details: { status: "blocker-audit", evidence, blockerKey, streak },
|
|
130
255
|
};
|
|
131
256
|
}
|
|
132
|
-
|
|
257
|
+
const decision = { kind: "blocked" as const, turn: state.turns, blockerKey, streak: 3 as const, evidence };
|
|
258
|
+
try {
|
|
259
|
+
transitionGoal(pi, runtime, "blocked", "blocked", evidence, { blockerAudit, decision });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `blocked decision could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
133
264
|
return {
|
|
134
265
|
content: [{ type: "text", text: `Goal marked blocked: ${evidence}` }],
|
|
135
266
|
details: { status: "blocked", evidence, blockerKey, streak },
|
|
@@ -142,15 +273,29 @@ export function registerGoalInterface(
|
|
|
142
273
|
if (context?.isError) {
|
|
143
274
|
const first = result.content[0];
|
|
144
275
|
const message = first?.type === "text" ? safeTerminalText(first.text) : "Goal update failed";
|
|
145
|
-
return new BoundedText(theme.fg("error", message), options.expanded ? undefined :
|
|
276
|
+
return new BoundedText(theme.fg("error", message), options.expanded ? undefined : 1);
|
|
146
277
|
}
|
|
147
278
|
const details = result.details;
|
|
148
|
-
if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
|
|
149
|
-
const label = details.status === "complete"
|
|
150
|
-
|
|
151
|
-
|
|
279
|
+
if (!details || typeof details.evidence !== "string") return new BoundedText(theme.fg("dim", "Goal updated"), options.expanded ? undefined : 1);
|
|
280
|
+
const label = details.status === "complete"
|
|
281
|
+
? "Goal completed"
|
|
282
|
+
: details.status === "blocked"
|
|
283
|
+
? "Goal blocked"
|
|
284
|
+
: details.status === "continue"
|
|
285
|
+
? "Progress recorded"
|
|
286
|
+
: `Blocker audit ${details.streak}/3`;
|
|
287
|
+
const color: ThemeColor = details.status === "complete"
|
|
288
|
+
? "success"
|
|
289
|
+
: details.status === "blocked"
|
|
290
|
+
? "error"
|
|
291
|
+
: details.status === "continue"
|
|
292
|
+
? "customMessageLabel"
|
|
293
|
+
: "warning";
|
|
294
|
+
const text = `${theme.fg(color, label)}${theme.fg("dim", " · ")}${safeTerminalText(details.evidence)}`;
|
|
295
|
+
return new BoundedText(text, options.expanded ? undefined : 1);
|
|
152
296
|
},
|
|
153
297
|
});
|
|
298
|
+
syncGoalUpdateTool(pi, runtime);
|
|
154
299
|
const handleGoalCommand = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
155
300
|
const command = parseGoalCommand(args);
|
|
156
301
|
if (ctx.mode === "print" || ctx.mode === "json") {
|
|
@@ -216,7 +361,7 @@ export function registerGoalInterface(
|
|
|
216
361
|
return;
|
|
217
362
|
}
|
|
218
363
|
if (saved) {
|
|
219
|
-
ctx.ui.notify("Goal cleared", "info");
|
|
364
|
+
if (ctx.mode !== "tui") ctx.ui.notify("Goal cleared", "info");
|
|
220
365
|
} else {
|
|
221
366
|
ctx.ui.notify("Goal paused: the requested clear could not be saved\nAutomatic continuation is stopped. Retry /goal clear to remove the goal.", "error");
|
|
222
367
|
}
|
|
@@ -277,7 +422,7 @@ export function registerGoalInterface(
|
|
|
277
422
|
return;
|
|
278
423
|
}
|
|
279
424
|
if (saved) {
|
|
280
|
-
ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
|
|
425
|
+
if (ctx.mode !== "tui") ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
|
|
281
426
|
} else {
|
|
282
427
|
ctx.ui.notify(`Goal paused: ${failureReason}\nAutomatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.`, "error");
|
|
283
428
|
}
|
|
@@ -308,7 +453,7 @@ export function registerGoalInterface(
|
|
|
308
453
|
const base = transitionGoalState(runtime.state, "active", undefined, { resetBlockedAudit: true }, Date.now());
|
|
309
454
|
persistGoalState(pi, runtime, "resume", { ...base, maxTurns: renewed });
|
|
310
455
|
runtime.continuationScheduled = false;
|
|
311
|
-
if (scheduleGoalContinuation(pi, runtime, ctx)) ctx.ui.notify("Goal resumed", "info");
|
|
456
|
+
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") ctx.ui.notify("Goal resumed", "info");
|
|
312
457
|
} catch (error) {
|
|
313
458
|
reportError(ctx, "Goal could not be resumed", error);
|
|
314
459
|
}
|
|
@@ -317,7 +462,7 @@ export function registerGoalInterface(
|
|
|
317
462
|
try {
|
|
318
463
|
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
319
464
|
runtime.continuationScheduled = false;
|
|
320
|
-
if (scheduleGoalContinuation(pi, runtime, ctx)) ctx.ui.notify("Goal resumed", "info");
|
|
465
|
+
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") ctx.ui.notify("Goal resumed", "info");
|
|
321
466
|
} catch (error) {
|
|
322
467
|
reportError(ctx, "Goal could not be resumed", error);
|
|
323
468
|
}
|
|
@@ -375,8 +520,10 @@ export function registerGoalInterface(
|
|
|
375
520
|
maxTurns: DEFAULT_GOAL_MAX_TURNS,
|
|
376
521
|
});
|
|
377
522
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
378
|
-
|
|
379
|
-
|
|
523
|
+
// The replacement supersedes any recovery still parked after waitForIdle().
|
|
524
|
+
if (unfinished) runtime.automaticCompaction = undefined;
|
|
525
|
+
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") {
|
|
526
|
+
ctx.ui.notify(`${unfinished ? "Goal replaced" : "Goal started"}. Each turn must record continue, complete, or a blocker decision before another turn starts.`, "info");
|
|
380
527
|
}
|
|
381
528
|
} catch (error) {
|
|
382
529
|
if (!unfinished) {
|