killeros 2.1.27 → 2.1.29
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 +32 -0
- package/README.md +8 -4
- package/killeros/activity.ts +16 -7
- package/killeros/auto-compaction.ts +58 -7
- package/killeros/change-receipt.ts +37 -130
- package/killeros/commands.ts +1 -0
- package/killeros/footer.ts +48 -77
- package/killeros/goal-interface.ts +195 -48
- package/killeros/goal-runtime.ts +191 -45
- package/killeros/goal-settlement.ts +129 -30
- package/killeros/goal-state.ts +205 -32
- package/killeros/passive-git-status.ts +212 -72
- 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/killeros/footer.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { watch } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
5
6
|
import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
6
7
|
import { formatCwd, formatTime, modelDisplayName, padRight } from "./display.ts";
|
|
7
|
-
import {
|
|
8
|
+
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, type PassiveGitRunner } from "./passive-git-status.ts";
|
|
8
9
|
import { goalElapsedMilliseconds } from "./goal-state.ts";
|
|
9
10
|
import type { GoalRuntime, GoalState } from "./runtime.ts";
|
|
10
11
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
@@ -36,26 +37,16 @@ type GitStatusExecutor = (
|
|
|
36
37
|
callback: (error: Error | null, stdout: string) => void,
|
|
37
38
|
) => unknown;
|
|
38
39
|
|
|
39
|
-
/** Resolves changed-file counts
|
|
40
|
-
export function resolveGitFileChanges(
|
|
40
|
+
/** Resolves changed-file counts without asking Git to inspect worktree content. */
|
|
41
|
+
export async function resolveGitFileChanges(
|
|
41
42
|
cwd: string,
|
|
42
43
|
execute: GitStatusExecutor = execFile,
|
|
43
44
|
trusted = true,
|
|
44
45
|
): Promise<GitFileChanges | undefined> {
|
|
45
|
-
if (!trusted) return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const found = passiveGitCommand(cwd);
|
|
50
|
-
if (!found) {
|
|
51
|
-
resolve(undefined);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
gitCommand = found;
|
|
55
|
-
} catch {
|
|
56
|
-
resolve(undefined);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
46
|
+
if (!trusted) return undefined;
|
|
47
|
+
try {
|
|
48
|
+
const gitCommand = passiveGitCommand(cwd);
|
|
49
|
+
if (!gitCommand) return undefined;
|
|
59
50
|
const options = {
|
|
60
51
|
encoding: "utf8" as const,
|
|
61
52
|
env: passiveGitEnv(),
|
|
@@ -63,64 +54,42 @@ export function resolveGitFileChanges(
|
|
|
63
54
|
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
64
55
|
windowsHide: true as const,
|
|
65
56
|
};
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
} catch {
|
|
72
|
-
done(false);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
const runStatus = (config: string, args: string[]): void => {
|
|
76
|
-
try {
|
|
77
|
-
execute(gitCommand, args, options, (error, stdout) => {
|
|
78
|
-
if (error) {
|
|
79
|
-
resolve(undefined);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
verifyFiltersUnchanged(config, (unchanged) => {
|
|
83
|
-
if (!unchanged) {
|
|
84
|
-
resolve(undefined);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const changes: GitFileChanges = { modified: 0, added: 0, deleted: 0 };
|
|
89
|
-
const entries = stdout.split("\0");
|
|
90
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
91
|
-
const entry = entries[index];
|
|
92
|
-
if (!entry) continue;
|
|
93
|
-
const status = entry.slice(0, 2);
|
|
94
|
-
if (status.includes("D")) changes.deleted += 1;
|
|
95
|
-
else if (status === "??" || status.includes("A")) changes.added += 1;
|
|
96
|
-
else changes.modified += 1;
|
|
97
|
-
if (status.includes("R") || status.includes("C")) index += 1;
|
|
98
|
-
}
|
|
99
|
-
resolve(changes);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
} catch {
|
|
103
|
-
resolve(undefined);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
execute(gitCommand, ["-C", cwd, ...PASSIVE_GIT_CONFIG_ARGS], options, (error, config) => {
|
|
109
|
-
if (error) {
|
|
110
|
-
resolve(undefined);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const safetyArgs = passiveStatusSafetyArgs(config);
|
|
114
|
-
if (!safetyArgs) {
|
|
115
|
-
resolve(undefined);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
runStatus(config, ["-C", cwd, ...safetyArgs, "status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
57
|
+
const runAt = (directory: string): PassiveGitRunner => (args) => new Promise((resolve, reject) => {
|
|
58
|
+
execute(gitCommand, ["-C", directory, ...args], options, (error, stdout) => {
|
|
59
|
+
if (error) reject(error);
|
|
60
|
+
else resolve(Buffer.from(stdout));
|
|
119
61
|
});
|
|
120
|
-
}
|
|
121
|
-
|
|
62
|
+
});
|
|
63
|
+
const initialRunner = runAt(cwd);
|
|
64
|
+
const root = new TextDecoder("utf-8", { fatal: true }).decode(
|
|
65
|
+
await initialRunner(["rev-parse", "--path-format=absolute", "--show-toplevel"]),
|
|
66
|
+
).trim();
|
|
67
|
+
if (!path.isAbsolute(root)) return undefined;
|
|
68
|
+
const snapshot = await inspectWorktreeWithoutFilters(root, runAt(root));
|
|
69
|
+
const changes: GitFileChanges = { modified: 0, added: 0, deleted: 0 };
|
|
70
|
+
const deletedObjectIds: string[] = [];
|
|
71
|
+
const addedObjectIds: string[] = [];
|
|
72
|
+
for (const file of snapshot.files.values()) {
|
|
73
|
+
if (!file.mode) {
|
|
74
|
+
changes.deleted += 1;
|
|
75
|
+
if (!file.indexMode && file.headObjectId) deletedObjectIds.push(file.headObjectId);
|
|
76
|
+
} else if (!file.headMode) {
|
|
77
|
+
changes.added += 1;
|
|
78
|
+
if (file.indexObjectId) addedObjectIds.push(file.indexObjectId);
|
|
79
|
+
} else changes.modified += 1;
|
|
122
80
|
}
|
|
123
|
-
|
|
81
|
+
for (const objectId of deletedObjectIds) {
|
|
82
|
+
const match = addedObjectIds.indexOf(objectId);
|
|
83
|
+
if (match < 0) continue;
|
|
84
|
+
addedObjectIds.splice(match, 1);
|
|
85
|
+
changes.deleted -= 1;
|
|
86
|
+
changes.added -= 1;
|
|
87
|
+
changes.modified += 1;
|
|
88
|
+
}
|
|
89
|
+
return changes;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
124
93
|
}
|
|
125
94
|
|
|
126
95
|
async function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
|
|
@@ -383,11 +352,13 @@ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
|
|
|
383
352
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
384
353
|
if (!state) return "";
|
|
385
354
|
if (state.status === "active") {
|
|
386
|
-
const
|
|
387
|
-
|
|
355
|
+
const detail = state.maxTurns === undefined
|
|
356
|
+
? `${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))}`
|
|
357
|
+
: `${state.turns}/${state.maxTurns} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))}`;
|
|
358
|
+
return `${theme.fg("customMessageLabel", "/goal active")}${theme.fg("dim", ` · ${detail}`)}`;
|
|
388
359
|
}
|
|
389
|
-
if (state.status === "paused") return theme.fg("warning", "/goal
|
|
390
|
-
if (state.status === "blocked") return theme.fg("error", "/goal
|
|
360
|
+
if (state.status === "paused") return theme.fg("warning", "/goal paused");
|
|
361
|
+
if (state.status === "blocked") return theme.fg("error", "/goal blocked");
|
|
391
362
|
return "";
|
|
392
363
|
}
|
|
393
364
|
|
|
@@ -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) {
|