killeros 2.1.26 → 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 +38 -0
- package/Killeros.ts +6 -13
- package/README.md +8 -10
- package/killeros/activity.ts +16 -7
- package/killeros/auto-compaction.ts +1 -2
- package/killeros/change-receipt.ts +109 -37
- package/killeros/codex-fast.ts +8 -3
- package/killeros/footer.ts +82 -31
- package/killeros/goal-interface.ts +198 -60
- package/killeros/goal-runtime.ts +192 -49
- package/killeros/goal-settlement.ts +140 -49
- package/killeros/goal-state.ts +250 -34
- package/killeros/handoff.ts +36 -4
- package/killeros/passive-git-status.ts +206 -0
- package/killeros/personal-instructions.ts +2 -3
- package/killeros/runtime.ts +51 -35
- package/killeros/shell-ui.ts +13 -4
- package/killeros/worked-for.ts +4 -3
- package/package.json +1 -1
- package/themes/killeros.json +3 -2
- package/killeros/init-evidence.ts +0 -291
- package/killeros/init-target.ts +0 -309
- package/killeros/init.ts +0 -285
package/killeros/footer.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor }
|
|
|
4
4
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
5
5
|
import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
6
6
|
import { formatCwd, formatTime, modelDisplayName, padRight } from "./display.ts";
|
|
7
|
+
import { PASSIVE_GIT_CONFIG_ARGS, passiveGitCommand, passiveGitEnv, passiveStatusSafetyArgs, samePassiveFilters } from "./passive-git-status.ts";
|
|
7
8
|
import { goalElapsedMilliseconds } from "./goal-state.ts";
|
|
8
9
|
import type { GoalRuntime, GoalState } from "./runtime.ts";
|
|
9
10
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
@@ -35,42 +36,90 @@ type GitStatusExecutor = (
|
|
|
35
36
|
callback: (error: Error | null, stdout: string) => void,
|
|
36
37
|
) => unknown;
|
|
37
38
|
|
|
38
|
-
/** Resolves changed-file counts with a bounded asynchronous Git status process. */
|
|
39
|
+
/** Resolves changed-file counts with a bounded asynchronous Git status process in a trusted project. */
|
|
39
40
|
export function resolveGitFileChanges(
|
|
40
41
|
cwd: string,
|
|
41
42
|
execute: GitStatusExecutor = execFile,
|
|
43
|
+
trusted = true,
|
|
42
44
|
): Promise<GitFileChanges | undefined> {
|
|
45
|
+
if (!trusted) return Promise.resolve(undefined);
|
|
43
46
|
return new Promise((resolve) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
let gitCommand: string;
|
|
48
|
+
try {
|
|
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
|
+
}
|
|
59
|
+
const options = {
|
|
60
|
+
encoding: "utf8" as const,
|
|
61
|
+
env: passiveGitEnv(),
|
|
62
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
63
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
64
|
+
windowsHide: true as const,
|
|
65
|
+
};
|
|
66
|
+
const verifyFiltersUnchanged = (before: string, done: (unchanged: boolean) => void): void => {
|
|
67
|
+
try {
|
|
68
|
+
execute(gitCommand, ["-C", cwd, ...PASSIVE_GIT_CONFIG_ARGS], options, (error, after) => {
|
|
69
|
+
done(!error && samePassiveFilters(before, after));
|
|
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) => {
|
|
55
109
|
if (error) {
|
|
56
110
|
resolve(undefined);
|
|
57
111
|
return;
|
|
58
112
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const entry = entries[index];
|
|
64
|
-
if (!entry) continue;
|
|
65
|
-
const status = entry.slice(0, 2);
|
|
66
|
-
if (status.includes("D")) changes.deleted += 1;
|
|
67
|
-
else if (status === "??" || status.includes("A")) changes.added += 1;
|
|
68
|
-
else changes.modified += 1;
|
|
69
|
-
if (status.includes("R") || status.includes("C")) index += 1;
|
|
113
|
+
const safetyArgs = passiveStatusSafetyArgs(config);
|
|
114
|
+
if (!safetyArgs) {
|
|
115
|
+
resolve(undefined);
|
|
116
|
+
return;
|
|
70
117
|
}
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
118
|
+
runStatus(config, ["-C", cwd, ...safetyArgs, "status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
resolve(undefined);
|
|
122
|
+
}
|
|
74
123
|
});
|
|
75
124
|
}
|
|
76
125
|
|
|
@@ -334,11 +383,13 @@ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
|
|
|
334
383
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
335
384
|
if (!state) return "";
|
|
336
385
|
if (state.status === "active") {
|
|
337
|
-
const
|
|
338
|
-
|
|
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}`)}`;
|
|
339
390
|
}
|
|
340
|
-
if (state.status === "paused") return theme.fg("warning", "/goal
|
|
341
|
-
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");
|
|
342
393
|
return "";
|
|
343
394
|
}
|
|
344
395
|
|
|
@@ -389,7 +440,7 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
389
440
|
if (JSON.stringify(changes) === JSON.stringify(gitFileChanges)) return;
|
|
390
441
|
gitFileChanges = changes;
|
|
391
442
|
tui.requestRender();
|
|
392
|
-
});
|
|
443
|
+
}, (cwd) => resolveGitFileChanges(cwd, execFile, ctx.isProjectTrusted()));
|
|
393
444
|
const unsubscribe = footerData.onBranchChange(() => {
|
|
394
445
|
gitStatus.request();
|
|
395
446
|
tui.requestRender();
|
|
@@ -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,
|
|
11
|
-
import type { GoalRuntime, GoalState, GoalStatus
|
|
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
|
+
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,39 +56,99 @@ 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
|
|
|
65
89
|
export function registerGoalInterface(
|
|
66
90
|
pi: ExtensionAPI,
|
|
67
91
|
runtime: GoalRuntime,
|
|
68
|
-
initState: InitRuntime,
|
|
69
92
|
): void {
|
|
70
93
|
pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry, options, theme) => {
|
|
71
94
|
const data = entry.data;
|
|
72
|
-
if (!data || data.version !== GOAL_VERSION
|
|
95
|
+
if (!data || data.version !== GOAL_VERSION) return undefined;
|
|
73
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;
|
|
74
99
|
const state = parseGoalState(data.state);
|
|
75
100
|
if (!state) return undefined;
|
|
76
|
-
const
|
|
77
|
-
const color: ThemeColor = state.status === "active" ? "accent" : state.status === "paused" ? "warning" : state.status === "blocked" ? "error" : "success";
|
|
78
|
-
const status = theme.fg(color, `${icon} Goal ${state.status}`);
|
|
101
|
+
const reason = state.stopReason ?? state.result;
|
|
79
102
|
const objective = safeTerminalText(state.objective);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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)));
|
|
83
145
|
return new BoundedText(lines.join("\n"));
|
|
84
146
|
});
|
|
85
147
|
|
|
86
148
|
pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
|
|
87
149
|
name: GOAL_UPDATE_TOOL,
|
|
88
150
|
label: "Goal update",
|
|
89
|
-
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.",
|
|
90
152
|
parameters: GoalUpdateParams,
|
|
91
153
|
executionMode: "sequential",
|
|
92
154
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -95,42 +157,110 @@ export function registerGoalInterface(
|
|
|
95
157
|
if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
|
|
96
158
|
const state = runtime.state;
|
|
97
159
|
if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
|
|
100
174
|
if (params.status === "complete") {
|
|
101
175
|
if (state.verification) await verifyGoalDeliverable(state.verification);
|
|
102
176
|
signal?.throwIfAborted();
|
|
103
|
-
if (runtime.state !== state
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
190
|
const text = state.verification
|
|
108
|
-
? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${
|
|
109
|
-
: `Goal marked complete (model-reported): ${
|
|
191
|
+
? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${evidence}`
|
|
192
|
+
: `Goal marked complete (model-reported): ${evidence}`;
|
|
110
193
|
return {
|
|
111
194
|
content: [{ type: "text", text }],
|
|
112
195
|
details: { status: "complete", evidence, verification },
|
|
113
196
|
};
|
|
114
197
|
}
|
|
115
|
-
|
|
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
|
+
|
|
116
228
|
const blockerKey = params.blockerKey;
|
|
117
229
|
if (!blockerKey || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(blockerKey)) {
|
|
118
230
|
throw new Error("A blocked goal update requires a stable lowercase blockerKey");
|
|
119
231
|
}
|
|
120
232
|
const previous = state.blockerAudit;
|
|
121
|
-
|
|
233
|
+
if (previous?.lastTurn === state.turns) {
|
|
234
|
+
throw new Error("Only one goal decision may be accepted per logical goal turn");
|
|
235
|
+
}
|
|
122
236
|
const consecutive = previous?.key === blockerKey && previous.lastTurn === state.turns - 1;
|
|
123
|
-
const streak =
|
|
237
|
+
const streak = consecutive ? previous.streak + 1 : 1;
|
|
124
238
|
const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns, evidence };
|
|
125
239
|
if (streak < 3) {
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
+
}
|
|
128
252
|
return {
|
|
129
253
|
content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
|
|
130
254
|
details: { status: "blocker-audit", evidence, blockerKey, streak },
|
|
131
255
|
};
|
|
132
256
|
}
|
|
133
|
-
|
|
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
|
+
}
|
|
134
264
|
return {
|
|
135
265
|
content: [{ type: "text", text: `Goal marked blocked: ${evidence}` }],
|
|
136
266
|
details: { status: "blocked", evidence, blockerKey, streak },
|
|
@@ -143,15 +273,29 @@ export function registerGoalInterface(
|
|
|
143
273
|
if (context?.isError) {
|
|
144
274
|
const first = result.content[0];
|
|
145
275
|
const message = first?.type === "text" ? safeTerminalText(first.text) : "Goal update failed";
|
|
146
|
-
return new BoundedText(theme.fg("error", message), options.expanded ? undefined :
|
|
276
|
+
return new BoundedText(theme.fg("error", message), options.expanded ? undefined : 1);
|
|
147
277
|
}
|
|
148
278
|
const details = result.details;
|
|
149
|
-
if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
|
|
150
|
-
const label = details.status === "complete"
|
|
151
|
-
|
|
152
|
-
|
|
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);
|
|
153
296
|
},
|
|
154
297
|
});
|
|
298
|
+
syncGoalUpdateTool(pi, runtime);
|
|
155
299
|
const handleGoalCommand = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
156
300
|
const command = parseGoalCommand(args);
|
|
157
301
|
if (ctx.mode === "print" || ctx.mode === "json") {
|
|
@@ -217,7 +361,7 @@ export function registerGoalInterface(
|
|
|
217
361
|
return;
|
|
218
362
|
}
|
|
219
363
|
if (saved) {
|
|
220
|
-
ctx.ui.notify("Goal cleared", "info");
|
|
364
|
+
if (ctx.mode !== "tui") ctx.ui.notify("Goal cleared", "info");
|
|
221
365
|
} else {
|
|
222
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");
|
|
223
367
|
}
|
|
@@ -278,7 +422,7 @@ export function registerGoalInterface(
|
|
|
278
422
|
return;
|
|
279
423
|
}
|
|
280
424
|
if (saved) {
|
|
281
|
-
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");
|
|
282
426
|
} else {
|
|
283
427
|
ctx.ui.notify(`Goal paused: ${failureReason}\nAutomatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.`, "error");
|
|
284
428
|
}
|
|
@@ -286,10 +430,6 @@ export function registerGoalInterface(
|
|
|
286
430
|
}
|
|
287
431
|
|
|
288
432
|
if (command.kind === "resume") {
|
|
289
|
-
if (initState.active) {
|
|
290
|
-
ctx.ui.notify("Wait for /init to finish before resuming a goal", "error");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
433
|
if (!runtime.state) {
|
|
294
434
|
ctx.ui.notify("No goal is set", "info");
|
|
295
435
|
return;
|
|
@@ -313,7 +453,7 @@ export function registerGoalInterface(
|
|
|
313
453
|
const base = transitionGoalState(runtime.state, "active", undefined, { resetBlockedAudit: true }, Date.now());
|
|
314
454
|
persistGoalState(pi, runtime, "resume", { ...base, maxTurns: renewed });
|
|
315
455
|
runtime.continuationScheduled = false;
|
|
316
|
-
if (scheduleGoalContinuation(pi, runtime,
|
|
456
|
+
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") ctx.ui.notify("Goal resumed", "info");
|
|
317
457
|
} catch (error) {
|
|
318
458
|
reportError(ctx, "Goal could not be resumed", error);
|
|
319
459
|
}
|
|
@@ -322,17 +462,13 @@ export function registerGoalInterface(
|
|
|
322
462
|
try {
|
|
323
463
|
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
324
464
|
runtime.continuationScheduled = false;
|
|
325
|
-
if (scheduleGoalContinuation(pi, runtime,
|
|
465
|
+
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") ctx.ui.notify("Goal resumed", "info");
|
|
326
466
|
} catch (error) {
|
|
327
467
|
reportError(ctx, "Goal could not be resumed", error);
|
|
328
468
|
}
|
|
329
469
|
return;
|
|
330
470
|
}
|
|
331
471
|
|
|
332
|
-
if (initState.active) {
|
|
333
|
-
ctx.ui.notify("Wait for /init to finish before starting a goal", "error");
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
472
|
switch (command.kind) {
|
|
337
473
|
case "objective":
|
|
338
474
|
break;
|
|
@@ -364,7 +500,7 @@ export function registerGoalInterface(
|
|
|
364
500
|
}
|
|
365
501
|
if (waitError) {
|
|
366
502
|
reportError(ctx, "Goal could not wait for the active turn", waitError);
|
|
367
|
-
scheduleGoalContinuation(pi, runtime,
|
|
503
|
+
scheduleGoalContinuation(pi, runtime, ctx);
|
|
368
504
|
return;
|
|
369
505
|
}
|
|
370
506
|
let verification: Awaited<ReturnType<typeof inferGoalVerification>>;
|
|
@@ -375,7 +511,7 @@ export function registerGoalInterface(
|
|
|
375
511
|
reportError(ctx, "Goal could not be started", error);
|
|
376
512
|
} else {
|
|
377
513
|
reportError(ctx, "Goal could not be replaced", error);
|
|
378
|
-
scheduleGoalContinuation(pi, runtime,
|
|
514
|
+
scheduleGoalContinuation(pi, runtime, ctx);
|
|
379
515
|
}
|
|
380
516
|
return;
|
|
381
517
|
}
|
|
@@ -384,8 +520,10 @@ export function registerGoalInterface(
|
|
|
384
520
|
maxTurns: DEFAULT_GOAL_MAX_TURNS,
|
|
385
521
|
});
|
|
386
522
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
387
|
-
|
|
388
|
-
|
|
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");
|
|
389
527
|
}
|
|
390
528
|
} catch (error) {
|
|
391
529
|
if (!unfinished) {
|