killeros 2.0.22 → 2.1.23

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.
@@ -0,0 +1,420 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { Type } from "typebox";
5
+ import { BoundedText } from "./bounded-text.ts";
6
+ import { formatTime, formatTokens } from "./display.ts";
7
+ import { reportError } from "./errors.ts";
8
+ import { parseGoalCommand } from "./goal-command.ts";
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, recordGoalBlockerAudit, transitionGoalState, verifyGoalDeliverable } from "./goal-state.ts";
11
+ import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
12
+ import { safeTerminalText } from "./safe-terminal-text.ts";
13
+
14
+ const GoalUpdateParams = Type.Object({
15
+ status: StringEnum(["complete", "blocked"] as const, {
16
+ description: "Mark the active goal complete or blocked",
17
+ }),
18
+ evidence: Type.String({
19
+ minLength: 1,
20
+ maxLength: 2_000,
21
+ description: "Concise evidence that the objective is complete, or the repeated blocker and attempted workarounds",
22
+ }),
23
+ blockerKey: Type.Optional(Type.String({
24
+ minLength: 1,
25
+ maxLength: 120,
26
+ pattern: "^[a-z0-9][a-z0-9._-]{0,119}$",
27
+ description: "Stable lowercase key identifying the repeated blocker",
28
+ })),
29
+ });
30
+
31
+ interface GoalUpdateDetails {
32
+ status: "complete" | "blocked" | "blocker-audit";
33
+ evidence: string;
34
+ verification?: "file" | "model-reported";
35
+ blockerKey?: string;
36
+ streak?: number;
37
+ }
38
+
39
+ function goalStatusLabel(status: GoalStatus): string {
40
+ return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
41
+ }
42
+
43
+ function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "pause" | "resume" | "clear" }> {
44
+ if (status === "active") return [{ label: "Pause automatic continuation", control: "pause" }, { label: "Clear goal", control: "clear" }];
45
+ if (status === "paused" || status === "blocked") {
46
+ return [{ label: "Resume automatic continuation", control: "resume" }, { label: "Clear goal", control: "clear" }];
47
+ }
48
+ return [{ label: "Clear goal", control: "clear" }];
49
+ }
50
+
51
+ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
52
+ const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
53
+ const turns = state.maxTurns === undefined
54
+ ? `${state.turns} turn${state.turns === 1 ? "" : "s"}`
55
+ : `${state.turns}/${state.maxTurns} turns`;
56
+ const lines = [
57
+ `Goal ${goalStatusLabel(state.status).toLowerCase()} · ${turns} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))} · ${formatTokens(usedTokens)} tokens`,
58
+ ...(state.verification === undefined ? [] : [`Deliverable: ${state.verification.path}`]),
59
+ state.objective,
60
+ ];
61
+ if (state.result) lines.push(state.result);
62
+ return safeTerminalText(lines.join("\n"));
63
+ }
64
+
65
+ export function registerGoalInterface(
66
+ pi: ExtensionAPI,
67
+ runtime: GoalRuntime,
68
+ initState: InitRuntime,
69
+ ): void {
70
+ pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry, options, theme) => {
71
+ const data = entry.data;
72
+ if (!data || data.version !== GOAL_VERSION || data.event === "turn" || data.event === "checkpoint") return undefined;
73
+ if (data.event === "clear" || data.state === null) return new Text(theme.fg("dim", "Goal cleared"), 0, 0);
74
+ const state = parseGoalState(data.state);
75
+ if (!state) return undefined;
76
+ const icon = state.status === "active" ? "✻" : state.status === "paused" ? "Ⅱ" : state.status === "blocked" ? "!" : "✓";
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}`);
79
+ const objective = safeTerminalText(state.objective);
80
+ if (!options.expanded) return new BoundedText(`${status}${theme.fg("dim", ` · ${objective}`)}`, 3);
81
+ const lines = [status, theme.fg("dim", objective)];
82
+ if (state.result) lines.push(theme.fg("muted", safeTerminalText(state.result)));
83
+ return new BoundedText(lines.join("\n"));
84
+ });
85
+
86
+ pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
87
+ name: GOAL_UPDATE_TOOL,
88
+ label: "Goal update",
89
+ description: "Mark the active KillerOS long-running goal complete after verification, or record the same blocker key on three consecutive goal turns before blocking it.",
90
+ parameters: GoalUpdateParams,
91
+ executionMode: "sequential",
92
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
93
+ if (!isGoalModeSupported(ctx)) throw new Error("KillerOS goals require TUI or RPC mode");
94
+ if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
95
+ const state = runtime.state;
96
+ if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
97
+ const evidence = params.evidence.trim();
98
+ if (!evidence) throw new Error("Goal evidence must not be empty");
99
+ if (params.status === "complete") {
100
+ if (state.verification) await verifyGoalDeliverable(state.verification);
101
+ if (runtime.state !== state) throw new Error("Goal changed while completion was being verified");
102
+ const verification = state.verification ? "file" : "model-reported";
103
+ transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
104
+ const safeEvidence = safeTerminalText(evidence);
105
+ const text = state.verification
106
+ ? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${safeEvidence}`
107
+ : `Goal marked complete (model-reported): ${safeEvidence}`;
108
+ return {
109
+ content: [{ type: "text", text }],
110
+ details: { status: "complete", evidence, verification },
111
+ };
112
+ }
113
+ if (!runtime.goalTurnInFlight) throw new Error("A blocker audit can only be recorded during an active KillerOS goal turn");
114
+ const blockerKey = params.blockerKey;
115
+ if (!blockerKey || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(blockerKey)) {
116
+ throw new Error("A blocked goal update requires a stable lowercase blockerKey");
117
+ }
118
+ const previous = state.blockerAudit;
119
+ const sameTurn = previous?.key === blockerKey && previous.lastTurn === state.turns;
120
+ const consecutive = previous?.key === blockerKey && previous.lastTurn === state.turns - 1;
121
+ const streak = sameTurn ? previous.streak : consecutive ? previous.streak + 1 : 1;
122
+ const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns, evidence };
123
+ if (streak < 3) {
124
+ const next = recordGoalBlockerAudit(state, blockerAudit, Date.now());
125
+ persistGoalState(pi, runtime, "blocker-audit", next);
126
+ return {
127
+ content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
128
+ details: { status: "blocker-audit", evidence, blockerKey, streak },
129
+ };
130
+ }
131
+ transitionGoal(pi, runtime, "blocked", "blocked", evidence, { blockerAudit });
132
+ return {
133
+ content: [{ type: "text", text: `Goal marked blocked: ${evidence}` }],
134
+ details: { status: "blocked", evidence, blockerKey, streak },
135
+ };
136
+ },
137
+ renderCall(args, theme) {
138
+ return new Text(`${theme.fg("toolTitle", theme.bold("goal "))}${theme.fg("muted", safeTerminalText(args.status))}`, 0, 0);
139
+ },
140
+ renderResult(result, options, theme, context) {
141
+ if (context?.isError) {
142
+ const first = result.content[0];
143
+ const message = first?.type === "text" ? safeTerminalText(first.text) : "Goal update failed";
144
+ return new BoundedText(theme.fg("error", message), options.expanded ? undefined : 3);
145
+ }
146
+ const details = result.details;
147
+ if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
148
+ const label = details.status === "complete" ? "✓ Complete" : details.status === "blocked" ? "! Blocked" : `! Blocker audit ${details.streak}/3`;
149
+ const text = `${theme.fg(details.status === "complete" ? "success" : "warning", label)}${theme.fg("dim", ` · ${safeTerminalText(details.evidence)}`)}`;
150
+ return new BoundedText(text, options.expanded ? undefined : 3);
151
+ },
152
+ });
153
+ const handleGoalCommand = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
154
+ const command = parseGoalCommand(args);
155
+ if (ctx.mode === "print" || ctx.mode === "json") {
156
+ ctx.ui.notify("/goal requires TUI or RPC mode", "error");
157
+ return;
158
+ }
159
+ if (command.kind === "invalid") {
160
+ ctx.ui.notify(command.message, "error");
161
+ return;
162
+ }
163
+ if (!isSavedSession(ctx)) {
164
+ ctx.ui.notify("/goal requires a saved session", "error");
165
+ return;
166
+ }
167
+
168
+ if (command.kind === "status") {
169
+ if (!runtime.state) {
170
+ ctx.ui.notify("No goal is set. Use /goal <objective> to start a long-running task.", "info");
171
+ return;
172
+ }
173
+ if (ctx.mode !== "tui") {
174
+ ctx.ui.notify(goalStatusSummary(runtime.state, ctx), "info");
175
+ return;
176
+ }
177
+ const actions = goalPanelActions(runtime.state.status);
178
+ const selected = await ctx.ui.select(goalStatusSummary(runtime.state, ctx), actions.map((action) => action.label));
179
+ const action = actions.find((candidate) => candidate.label === selected);
180
+ if (!action) return;
181
+ if (action.control === "clear" && !await ctx.ui.confirm("Clear goal?", safeTerminalText(runtime.state.objective))) return;
182
+ await handleGoalCommand(action.control, ctx);
183
+ return;
184
+ }
185
+
186
+ if (command.kind === "clear") {
187
+ if (!runtime.state) {
188
+ ctx.ui.notify("No goal is set", "info");
189
+ return;
190
+ }
191
+ const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
192
+ let saved = false;
193
+ try {
194
+ persistGoalState(pi, runtime, "clear", undefined);
195
+ saved = true;
196
+ } catch (error) {
197
+ if (runtime.state?.status === "active") {
198
+ pauseGoalAfterFailure(
199
+ pi,
200
+ runtime,
201
+ ctx,
202
+ `the requested clear could not be saved: ${error instanceof Error ? error.message : String(error)}`,
203
+ "Automatic continuation is stopped. Retry /goal clear to remove the goal.",
204
+ false,
205
+ );
206
+ } else {
207
+ reportError(ctx, "Goal could not be cleared", error);
208
+ return;
209
+ }
210
+ }
211
+ try {
212
+ await stopGoalRun(runtime, ctx, shouldStopGoalRun);
213
+ } catch (error) {
214
+ reportError(ctx, saved ? "Goal cleared, but the active goal turn could not be confirmed stopped" : "Goal paused, but the active goal turn could not be confirmed stopped", error);
215
+ return;
216
+ }
217
+ if (saved) {
218
+ ctx.ui.notify("Goal cleared", "info");
219
+ } else {
220
+ ctx.ui.notify("Goal paused: the requested clear could not be saved\nAutomatic continuation is stopped. Retry /goal clear to remove the goal.", "error");
221
+ }
222
+ return;
223
+ }
224
+
225
+ if (command.kind === "pause") {
226
+ if (!runtime.state) {
227
+ ctx.ui.notify("No goal is set", "info");
228
+ return;
229
+ }
230
+ if (runtime.state.status === "paused") {
231
+ if (!runtime.persistenceRetryNeeded
232
+ && runtime.state.resumeAfterManualCompaction !== true
233
+ && runtime.automaticCompaction === undefined) {
234
+ ctx.ui.notify("Goal is already paused", "info");
235
+ return;
236
+ }
237
+ const checkpoint = checkpointPausedGoalState(runtime.state, Date.now());
238
+ try {
239
+ persistGoalState(pi, runtime, "pause", checkpoint);
240
+ ctx.ui.notify("Goal pause saved. Goal remains paused. Automatic compaction recovery is off.", "info");
241
+ } catch (error) {
242
+ runtime.state = checkpoint;
243
+ syncGoalUpdateTool(pi, runtime);
244
+ runtime.persistenceRetryNeeded = true;
245
+ runtime.continuationScheduled = false;
246
+ runtime.requestRender?.();
247
+ reportError(ctx, "Goal pause still could not be saved", error);
248
+ }
249
+ return;
250
+ }
251
+ if (runtime.state.status !== "active") {
252
+ ctx.ui.notify(`Goal is ${runtime.state.status}; only an active goal can be paused`, "warning");
253
+ return;
254
+ }
255
+ const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
256
+ let saved = false;
257
+ let failureReason: string | undefined;
258
+ try {
259
+ transitionGoal(pi, runtime, "pause", "paused");
260
+ saved = true;
261
+ } catch (error) {
262
+ failureReason = safeTerminalText(`the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
263
+ pauseGoalAfterFailure(
264
+ pi,
265
+ runtime,
266
+ ctx,
267
+ failureReason,
268
+ "Automatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.",
269
+ false,
270
+ );
271
+ }
272
+ try {
273
+ await stopGoalRun(runtime, ctx, shouldStopGoalRun);
274
+ } catch (error) {
275
+ reportError(ctx, "Goal paused, but the active goal turn could not be confirmed stopped", error);
276
+ return;
277
+ }
278
+ if (saved) {
279
+ ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
280
+ } else {
281
+ ctx.ui.notify(`Goal paused: ${failureReason}\nAutomatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.`, "error");
282
+ }
283
+ return;
284
+ }
285
+
286
+ if (command.kind === "resume") {
287
+ if (initState.active) {
288
+ ctx.ui.notify("Wait for /init to finish before resuming a goal", "error");
289
+ return;
290
+ }
291
+ if (!runtime.state) {
292
+ ctx.ui.notify("No goal is set", "info");
293
+ return;
294
+ }
295
+ if (runtime.state.status === "complete") {
296
+ ctx.ui.notify("The goal is complete. Set a new objective.", "info");
297
+ return;
298
+ }
299
+ if (runtime.state.status === "active") {
300
+ ctx.ui.notify("Goal is already active", "info");
301
+ return;
302
+ }
303
+ const currentMax = runtime.state.maxTurns;
304
+ if (currentMax !== undefined && runtime.state.turns >= currentMax) {
305
+ if (runtime.state.turns >= GOAL_MAX_TURNS) {
306
+ ctx.ui.notify(`Goal reached the lifetime limit (${runtime.state.turns}/${GOAL_MAX_TURNS}). Set a new objective.`, "warning");
307
+ return;
308
+ }
309
+ const renewed = Math.min(Math.max(currentMax, runtime.state.turns) + DEFAULT_GOAL_MAX_TURNS, GOAL_MAX_TURNS);
310
+ try {
311
+ const base = transitionGoalState(runtime.state, "active", undefined, { resetBlockedAudit: true }, Date.now());
312
+ persistGoalState(pi, runtime, "resume", { ...base, maxTurns: renewed });
313
+ runtime.continuationScheduled = false;
314
+ if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
315
+ } catch (error) {
316
+ reportError(ctx, "Goal could not be resumed", error);
317
+ }
318
+ return;
319
+ }
320
+ try {
321
+ transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
322
+ runtime.continuationScheduled = false;
323
+ if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
324
+ } catch (error) {
325
+ reportError(ctx, "Goal could not be resumed", error);
326
+ }
327
+ return;
328
+ }
329
+
330
+ if (initState.active) {
331
+ ctx.ui.notify("Wait for /init to finish before starting a goal", "error");
332
+ return;
333
+ }
334
+ switch (command.kind) {
335
+ case "objective":
336
+ break;
337
+ default: {
338
+ const unhandled: never = command;
339
+ return unhandled;
340
+ }
341
+ }
342
+ const objective = command.objective;
343
+
344
+ const unfinished = runtime.state && runtime.state.status !== "complete";
345
+ if (unfinished) {
346
+ if (!ctx.hasUI) {
347
+ ctx.ui.notify("Clear the current goal before replacing it outside TUI mode", "error");
348
+ return;
349
+ }
350
+ const replace = await ctx.ui.confirm("Replace active goal", "Replace the current unfinished goal and discard its continuation state?");
351
+ if (!replace) return;
352
+ }
353
+
354
+ runtime.continuationHeld = true;
355
+ let waitError: unknown;
356
+ try {
357
+ await ctx.waitForIdle();
358
+ } catch (error) {
359
+ waitError = error;
360
+ } finally {
361
+ runtime.continuationHeld = false;
362
+ }
363
+ if (waitError) {
364
+ reportError(ctx, "Goal could not wait for the active turn", waitError);
365
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
366
+ return;
367
+ }
368
+ let verification: Awaited<ReturnType<typeof inferGoalVerification>>;
369
+ try {
370
+ verification = await inferGoalVerification(objective, ctx.cwd);
371
+ } catch (error) {
372
+ if (!unfinished) {
373
+ reportError(ctx, "Goal could not be started", error);
374
+ } else {
375
+ reportError(ctx, "Goal could not be replaced", error);
376
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
377
+ }
378
+ return;
379
+ }
380
+ try {
381
+ const state = createNewGoalState(objective, sumGoalTokens(ctx), verification, Date.now(), {
382
+ maxTurns: DEFAULT_GOAL_MAX_TURNS,
383
+ });
384
+ persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
385
+ if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
386
+ ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
387
+ }
388
+ } catch (error) {
389
+ if (!unfinished) {
390
+ reportError(ctx, "Goal could not be started", error);
391
+ } else if (runtime.state?.status === "active") {
392
+ pauseGoalAfterFailure(
393
+ pi,
394
+ runtime,
395
+ ctx,
396
+ `Goal could not be replaced: ${error instanceof Error ? error.message : String(error)}`,
397
+ "Automatic continuation is stopped. Retry replacement after session storage recovers.",
398
+ );
399
+ } else {
400
+ reportError(ctx, "Goal could not be replaced", error);
401
+ }
402
+ }
403
+ };
404
+
405
+ pi.registerCommand("goal", {
406
+ description: "Set a non-command objective or view the current goal",
407
+ getArgumentCompletions: (prefix) => {
408
+ const normalized = prefix.trimStart().toLowerCase();
409
+ const actions = [
410
+ { value: "clear", description: "Remove the current goal" },
411
+ { value: "pause", description: "Stop automatic continuation" },
412
+ { value: "resume", description: "Resume automatic continuation" },
413
+ ];
414
+ return actions
415
+ .filter((action) => action.value.startsWith(normalized))
416
+ .map((action) => ({ ...action, label: action.value.trimEnd() }));
417
+ },
418
+ handler: handleGoalCommand,
419
+ });
420
+ }