killeros 2.0.21 → 2.1.22

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,599 @@
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 { formatGoalHistory } from "./goal-history.ts";
10
+ import { GOAL_ENTRY_TYPE, GOAL_UPDATE_TOOL, goalBranchEntries, isGoalModeSupported, isSavedSession, pauseGoalAfterFailure, persistGoalState, scheduleGoalContinuation, stopGoalRun, sumGoalTokens, syncGoalUpdateTool, transitionGoal, type GoalEntryData } from "./goal-runtime.ts";
11
+ import { checkpointPausedGoalState, createNewGoalState, DEFAULT_GOAL_MAX_TURNS, editGoalState, goalElapsedMilliseconds, GOAL_VERSION, inferGoalVerification, parseGoalState, pauseGoalState, recordGoalBlockerAudit, updateGoalControlsState, validateGoalObjective, verifyGoalDeliverable } from "./goal-state.ts";
12
+ import { listGoalCompletionChecks, resolveGoalCompletionCheck, runGoalCompletionCheck } from "./hooks.ts";
13
+ import type { GoalCompletionCheck, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
14
+ import { safeTerminalText } from "./safe-terminal-text.ts";
15
+
16
+ const GoalUpdateParams = Type.Object({
17
+ status: StringEnum(["complete", "blocked"] as const, {
18
+ description: "Mark the active goal complete or blocked",
19
+ }),
20
+ evidence: Type.String({
21
+ minLength: 1,
22
+ maxLength: 2_000,
23
+ description: "Concise evidence that the objective is complete, or the repeated blocker and attempted workarounds",
24
+ }),
25
+ blockerKey: Type.Optional(Type.String({
26
+ minLength: 1,
27
+ maxLength: 120,
28
+ pattern: "^[a-z0-9][a-z0-9._-]{0,119}$",
29
+ description: "Stable lowercase key identifying the repeated blocker",
30
+ })),
31
+ });
32
+
33
+ interface GoalUpdateDetails {
34
+ status: "complete" | "blocked" | "blocker-audit";
35
+ evidence: string;
36
+ verification?: "file" | "check" | "file-and-check" | "model-reported";
37
+ blockerKey?: string;
38
+ streak?: number;
39
+ }
40
+
41
+ function goalStatusLabel(status: GoalStatus): string {
42
+ return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
43
+ }
44
+
45
+ function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "checks" | "pause" | "resume" | "edit" | "clear" }> {
46
+ const terminal = [
47
+ { label: "List completion checks", control: "checks" as const },
48
+ { label: "Edit objective", control: "edit" as const },
49
+ { label: "Clear goal", control: "clear" as const },
50
+ ];
51
+ if (status === "active") return [{ label: "Pause automatic continuation", control: "pause" }, ...terminal];
52
+ if (status === "paused" || status === "blocked") {
53
+ return [{ label: "Resume automatic continuation", control: "resume" }, ...terminal];
54
+ }
55
+ return terminal;
56
+ }
57
+
58
+ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
59
+ const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
60
+ const turns = state.maxTurns === undefined
61
+ ? `${state.turns} turn${state.turns === 1 ? "" : "s"}`
62
+ : `${state.turns}/${state.maxTurns} turns`;
63
+ const lines = [
64
+ `Goal ${goalStatusLabel(state.status).toLowerCase()} · ${turns} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))} · ${formatTokens(usedTokens)} tokens`,
65
+ ...(state.completionCheck === undefined ? [] : [`Check: ${state.completionCheck.name}`]),
66
+ state.objective,
67
+ ];
68
+ if (state.result) lines.push(state.result);
69
+ return safeTerminalText(lines.join("\n"));
70
+ }
71
+
72
+ export function registerGoalInterface(
73
+ pi: ExtensionAPI,
74
+ runtime: GoalRuntime,
75
+ initState: InitRuntime,
76
+ ): void {
77
+ pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry, options, theme) => {
78
+ const data = entry.data;
79
+ if (!data || data.version !== GOAL_VERSION || data.event === "turn" || data.event === "checkpoint") return undefined;
80
+ if (data.event === "clear" || data.state === null) return new Text(theme.fg("dim", "Goal cleared"), 0, 0);
81
+ const state = parseGoalState(data.state);
82
+ if (!state) return undefined;
83
+ const icon = state.status === "active" ? "✻" : state.status === "paused" ? "Ⅱ" : state.status === "blocked" ? "!" : "✓";
84
+ const color: ThemeColor = state.status === "active" ? "accent" : state.status === "paused" ? "warning" : state.status === "blocked" ? "error" : "success";
85
+ const status = theme.fg(color, `${icon} Goal ${state.status}`);
86
+ const objective = safeTerminalText(state.objective);
87
+ if (!options.expanded) return new BoundedText(`${status}${theme.fg("dim", ` · ${objective}`)}`, 3);
88
+ const lines = [status, theme.fg("dim", objective)];
89
+ if (state.result) lines.push(theme.fg("muted", safeTerminalText(state.result)));
90
+ return new BoundedText(lines.join("\n"));
91
+ });
92
+
93
+ pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
94
+ name: GOAL_UPDATE_TOOL,
95
+ label: "Goal update",
96
+ 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.",
97
+ parameters: GoalUpdateParams,
98
+ executionMode: "sequential",
99
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
100
+ if (!isGoalModeSupported(ctx)) throw new Error("KillerOS goals require TUI or RPC mode");
101
+ if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
102
+ const state = runtime.state;
103
+ if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
104
+ const evidence = params.evidence.trim();
105
+ if (!evidence) throw new Error("Goal evidence must not be empty");
106
+ if (params.status === "complete") {
107
+ if (state.verification) await verifyGoalDeliverable(state.verification);
108
+ if (state.completionCheck) await runGoalCompletionCheck(ctx, state.completionCheck, signal);
109
+ if (state.verification && state.completionCheck) await verifyGoalDeliverable(state.verification);
110
+ if (runtime.state !== state) throw new Error("Goal changed while completion was being verified");
111
+ const verification = state.verification && state.completionCheck
112
+ ? "file-and-check"
113
+ : state.verification ? "file" : state.completionCheck ? "check" : "model-reported";
114
+ transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
115
+ const safeEvidence = safeTerminalText(evidence);
116
+ const text = state.verification && state.completionCheck
117
+ ? `Goal verified complete by file and ${state.completionCheck.name}: ${safeEvidence}`
118
+ : state.completionCheck
119
+ ? `Goal verified complete by ${state.completionCheck.name}: ${safeEvidence}`
120
+ : state.verification
121
+ ? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${safeEvidence}`
122
+ : `Goal marked complete (model-reported): ${safeEvidence}`;
123
+ return {
124
+ content: [{ type: "text", text }],
125
+ details: { status: "complete", evidence, verification },
126
+ };
127
+ }
128
+ if (!runtime.goalTurnInFlight) throw new Error("A blocker audit can only be recorded during an active KillerOS goal turn");
129
+ const blockerKey = params.blockerKey;
130
+ if (!blockerKey || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(blockerKey)) {
131
+ throw new Error("A blocked goal update requires a stable lowercase blockerKey");
132
+ }
133
+ const previous = state.blockerAudit;
134
+ const sameTurn = previous?.key === blockerKey && previous.lastTurn === state.turns;
135
+ const consecutive = previous?.key === blockerKey && previous.lastTurn === state.turns - 1;
136
+ const streak = sameTurn ? previous.streak : consecutive ? previous.streak + 1 : 1;
137
+ const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns, evidence };
138
+ if (streak < 3) {
139
+ const next = recordGoalBlockerAudit(state, blockerAudit, Date.now());
140
+ persistGoalState(pi, runtime, "blocker-audit", next);
141
+ return {
142
+ content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
143
+ details: { status: "blocker-audit", evidence, blockerKey, streak },
144
+ };
145
+ }
146
+ transitionGoal(pi, runtime, "blocked", "blocked", evidence, { blockerAudit });
147
+ return {
148
+ content: [{ type: "text", text: `Goal marked blocked: ${evidence}` }],
149
+ details: { status: "blocked", evidence, blockerKey, streak },
150
+ };
151
+ },
152
+ renderCall(args, theme) {
153
+ return new Text(`${theme.fg("toolTitle", theme.bold("goal "))}${theme.fg("muted", safeTerminalText(args.status))}`, 0, 0);
154
+ },
155
+ renderResult(result, options, theme, context) {
156
+ if (context?.isError) {
157
+ const first = result.content[0];
158
+ const message = first?.type === "text" ? safeTerminalText(first.text) : "Goal update failed";
159
+ return new BoundedText(theme.fg("error", message), options.expanded ? undefined : 3);
160
+ }
161
+ const details = result.details;
162
+ if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
163
+ const label = details.status === "complete" ? "✓ Complete" : details.status === "blocked" ? "! Blocked" : `! Blocker audit ${details.streak}/3`;
164
+ const text = `${theme.fg(details.status === "complete" ? "success" : "warning", label)}${theme.fg("dim", ` · ${safeTerminalText(details.evidence)}`)}`;
165
+ return new BoundedText(text, options.expanded ? undefined : 3);
166
+ },
167
+ });
168
+ const handleGoalCommand = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
169
+ const command = parseGoalCommand(args);
170
+ if (ctx.mode === "print" || ctx.mode === "json") {
171
+ ctx.ui.notify("/goal requires TUI or RPC mode", "error");
172
+ return;
173
+ }
174
+ if (command.kind === "invalid") {
175
+ ctx.ui.notify(command.message, "error");
176
+ return;
177
+ }
178
+ if (!isSavedSession(ctx)) {
179
+ ctx.ui.notify("/goal requires a saved session", "error");
180
+ return;
181
+ }
182
+
183
+ if (command.kind === "status") {
184
+ if (!runtime.state) {
185
+ ctx.ui.notify("No goal is set. Use /goal <objective> to start a long-running task.", "info");
186
+ return;
187
+ }
188
+ if (ctx.mode !== "tui") {
189
+ ctx.ui.notify(goalStatusSummary(runtime.state, ctx), "info");
190
+ return;
191
+ }
192
+ const actions = goalPanelActions(runtime.state.status);
193
+ const selected = await ctx.ui.select(goalStatusSummary(runtime.state, ctx), actions.map((action) => action.label));
194
+ const action = actions.find((candidate) => candidate.label === selected);
195
+ if (!action) return;
196
+ if (action.control === "clear" && !await ctx.ui.confirm("Clear goal?", safeTerminalText(runtime.state.objective))) return;
197
+ await handleGoalCommand(action.control, ctx);
198
+ return;
199
+ }
200
+
201
+ if (command.kind === "history") {
202
+ const history = formatGoalHistory(goalBranchEntries(ctx), command.count);
203
+ ctx.ui.notify(history ?? "No goal history on the current branch.", "info");
204
+ return;
205
+ }
206
+
207
+ if (command.kind === "checks") {
208
+ try {
209
+ const checks = listGoalCompletionChecks(ctx);
210
+ ctx.ui.notify(checks.length
211
+ ? `Goal completion checks: ${checks.join(", ")}`
212
+ : "No goal completion checks are configured.", "info");
213
+ } catch (error) {
214
+ reportError(ctx, "Goal completion checks could not be listed", error);
215
+ }
216
+ return;
217
+ }
218
+
219
+ if (command.kind === "check" || command.kind === "limit") {
220
+ if (initState.active) {
221
+ ctx.ui.notify(`Wait for /init to finish before changing goal ${command.kind}`, "error");
222
+ return;
223
+ }
224
+ const maxTurns = command.kind === "limit" && command.value.kind === "count" ? command.value.count : undefined;
225
+ const current = runtime.state;
226
+ if (!current) {
227
+ ctx.ui.notify("No goal is set", "info");
228
+ return;
229
+ }
230
+ if (current.status === "complete") {
231
+ ctx.ui.notify("The goal is complete. Set a new objective or use /goal edit.", "info");
232
+ return;
233
+ }
234
+ let completionCheck: GoalCompletionCheck | undefined = current.completionCheck;
235
+ if (command.kind === "check") {
236
+ try {
237
+ completionCheck = command.value.kind === "clear" ? undefined : resolveGoalCompletionCheck(ctx, command.value.name);
238
+ } catch (error) {
239
+ reportError(ctx, "Goal completion check could not be set", error);
240
+ return;
241
+ }
242
+ }
243
+ runtime.continuationHeld = true;
244
+ try {
245
+ await ctx.waitForIdle();
246
+ } catch (error) {
247
+ runtime.continuationHeld = false;
248
+ reportError(ctx, "Goal could not wait for the active turn", error);
249
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
250
+ return;
251
+ }
252
+ runtime.continuationHeld = false;
253
+ const latest = runtime.state;
254
+ if (!latest || latest.status === "complete") {
255
+ ctx.ui.notify(latest?.status === "complete" ? "The goal completed before its controls changed." : "No goal is set", "info");
256
+ return;
257
+ }
258
+ if (command.kind === "check" && command.value.kind === "named") {
259
+ try {
260
+ completionCheck = resolveGoalCompletionCheck(ctx, command.value.name);
261
+ } catch (error) {
262
+ reportError(ctx, "Goal completion check could not be set", error);
263
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
264
+ return;
265
+ }
266
+ } else if (command.kind === "check") {
267
+ completionCheck = undefined;
268
+ } else {
269
+ completionCheck = latest.completionCheck;
270
+ }
271
+ const nextLimit = command.kind === "limit" ? maxTurns : latest.maxTurns;
272
+ let next = updateGoalControlsState(latest, { completionCheck, ...(nextLimit === undefined ? {} : { maxTurns: nextLimit }) }, Date.now());
273
+ const exhausted = next.status === "active" && next.maxTurns !== undefined && next.turns >= next.maxTurns;
274
+ if (exhausted) next = pauseGoalState(next, `Turn limit reached (${next.turns}/${next.maxTurns}).`, Date.now());
275
+ try {
276
+ persistGoalState(pi, runtime, command.kind, next);
277
+ runtime.continuationScheduled = false;
278
+ if (exhausted) {
279
+ ctx.ui.notify(`Goal paused: turn limit reached (${next.turns}/${next.maxTurns})`, "warning");
280
+ } else {
281
+ const message = command.kind === "check"
282
+ ? completionCheck ? `Goal completion check set to ${completionCheck.name}` : "Goal completion check cleared"
283
+ : next.maxTurns === undefined ? "Goal turn limit cleared" : `Goal turn limit set to ${next.maxTurns}`;
284
+ ctx.ui.notify(message, "info");
285
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
286
+ }
287
+ } catch (error) {
288
+ reportError(ctx, `Goal ${command.kind} could not be changed`, error);
289
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
290
+ }
291
+ return;
292
+ }
293
+
294
+ if (command.kind === "clear") {
295
+ if (!runtime.state) {
296
+ ctx.ui.notify("No goal is set", "info");
297
+ return;
298
+ }
299
+ const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
300
+ let saved = false;
301
+ try {
302
+ persistGoalState(pi, runtime, "clear", undefined);
303
+ saved = true;
304
+ } catch (error) {
305
+ if (runtime.state?.status === "active") {
306
+ pauseGoalAfterFailure(
307
+ pi,
308
+ runtime,
309
+ ctx,
310
+ `the requested clear could not be saved: ${error instanceof Error ? error.message : String(error)}`,
311
+ "Automatic continuation is stopped. Retry /goal clear to remove the goal.",
312
+ false,
313
+ );
314
+ } else {
315
+ reportError(ctx, "Goal could not be cleared", error);
316
+ return;
317
+ }
318
+ }
319
+ try {
320
+ await stopGoalRun(runtime, ctx, shouldStopGoalRun);
321
+ } catch (error) {
322
+ 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);
323
+ return;
324
+ }
325
+ if (saved) {
326
+ ctx.ui.notify("Goal cleared", "info");
327
+ } else {
328
+ ctx.ui.notify("Goal paused: the requested clear could not be saved\nAutomatic continuation is stopped. Retry /goal clear to remove the goal.", "error");
329
+ }
330
+ return;
331
+ }
332
+
333
+ if (command.kind === "pause") {
334
+ if (!runtime.state) {
335
+ ctx.ui.notify("No goal is set", "info");
336
+ return;
337
+ }
338
+ if (runtime.state.status === "paused") {
339
+ if (!runtime.persistenceRetryNeeded
340
+ && runtime.state.resumeAfterManualCompaction !== true
341
+ && runtime.automaticCompaction === undefined) {
342
+ ctx.ui.notify("Goal is already paused", "info");
343
+ return;
344
+ }
345
+ const checkpoint = checkpointPausedGoalState(runtime.state, Date.now());
346
+ try {
347
+ persistGoalState(pi, runtime, "pause", checkpoint);
348
+ ctx.ui.notify("Goal pause saved. Goal remains paused. Automatic compaction recovery is off.", "info");
349
+ } catch (error) {
350
+ runtime.state = checkpoint;
351
+ syncGoalUpdateTool(pi, runtime);
352
+ runtime.persistenceRetryNeeded = true;
353
+ runtime.continuationScheduled = false;
354
+ runtime.requestRender?.();
355
+ reportError(ctx, "Goal pause still could not be saved", error);
356
+ }
357
+ return;
358
+ }
359
+ if (runtime.state.status !== "active") {
360
+ ctx.ui.notify(`Goal is ${runtime.state.status}; only an active goal can be paused`, "warning");
361
+ return;
362
+ }
363
+ const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
364
+ let saved = false;
365
+ let failureReason: string | undefined;
366
+ try {
367
+ transitionGoal(pi, runtime, "pause", "paused");
368
+ saved = true;
369
+ } catch (error) {
370
+ failureReason = safeTerminalText(`the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
371
+ pauseGoalAfterFailure(
372
+ pi,
373
+ runtime,
374
+ ctx,
375
+ failureReason,
376
+ "Automatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.",
377
+ false,
378
+ );
379
+ }
380
+ try {
381
+ await stopGoalRun(runtime, ctx, shouldStopGoalRun);
382
+ } catch (error) {
383
+ reportError(ctx, "Goal paused, but the active goal turn could not be confirmed stopped", error);
384
+ return;
385
+ }
386
+ if (saved) {
387
+ ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
388
+ } else {
389
+ ctx.ui.notify(`Goal paused: ${failureReason}\nAutomatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.`, "error");
390
+ }
391
+ return;
392
+ }
393
+
394
+ if (command.kind === "resume") {
395
+ if (initState.active) {
396
+ ctx.ui.notify("Wait for /init to finish before resuming a goal", "error");
397
+ return;
398
+ }
399
+ if (!runtime.state) {
400
+ ctx.ui.notify("No goal is set", "info");
401
+ return;
402
+ }
403
+ if (runtime.state.status === "complete") {
404
+ ctx.ui.notify("The goal is complete. Set a new objective or use /goal edit.", "info");
405
+ return;
406
+ }
407
+ if (runtime.state.maxTurns !== undefined && runtime.state.turns >= runtime.state.maxTurns) {
408
+ ctx.ui.notify(`Goal turn limit reached (${runtime.state.turns}/${runtime.state.maxTurns}). Raise or clear it before resuming.`, "warning");
409
+ return;
410
+ }
411
+ if (runtime.state.status === "active") {
412
+ ctx.ui.notify("Goal is already active", "info");
413
+ return;
414
+ }
415
+ try {
416
+ transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
417
+ runtime.continuationScheduled = false;
418
+ if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
419
+ } catch (error) {
420
+ reportError(ctx, "Goal could not be resumed", error);
421
+ }
422
+ return;
423
+ }
424
+
425
+ if (command.kind === "edit") {
426
+ if (initState.active) {
427
+ ctx.ui.notify("Wait for /init to finish before editing a goal", "error");
428
+ return;
429
+ }
430
+ if (!runtime.state) {
431
+ ctx.ui.notify("No goal is set", "info");
432
+ return;
433
+ }
434
+ if (ctx.mode !== "tui") {
435
+ ctx.ui.notify("/goal edit requires interactive TUI mode", "error");
436
+ return;
437
+ }
438
+ runtime.continuationHeld = true;
439
+ let waitError: unknown;
440
+ try {
441
+ await ctx.waitForIdle();
442
+ } catch (error) {
443
+ waitError = error;
444
+ } finally {
445
+ runtime.continuationHeld = false;
446
+ }
447
+ if (waitError) {
448
+ reportError(ctx, "Goal could not wait for the active turn", waitError);
449
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
450
+ return;
451
+ }
452
+ const edited = await ctx.ui.editor("Edit long-running goal", runtime.state.objective);
453
+ if (edited === undefined) {
454
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
455
+ return;
456
+ }
457
+ const objective = validateGoalObjective(edited);
458
+ if (!objective) {
459
+ ctx.ui.notify(edited.trim() ? "A goal objective may not exceed 4,000 characters" : "A goal objective may not be empty", "error");
460
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
461
+ return;
462
+ }
463
+ let verification: Awaited<ReturnType<typeof inferGoalVerification>>;
464
+ try {
465
+ verification = await inferGoalVerification(objective);
466
+ } catch (error) {
467
+ reportError(ctx, "Goal verification could not be inferred", error);
468
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
469
+ return;
470
+ }
471
+ const next = editGoalState(runtime.state, objective, verification, Date.now());
472
+ try {
473
+ persistGoalState(pi, runtime, "edit", next);
474
+ runtime.continuationScheduled = false;
475
+ if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal updated and active", "info");
476
+ } catch (error) {
477
+ if (runtime.state?.status === "active") {
478
+ pauseGoalAfterFailure(
479
+ pi,
480
+ runtime,
481
+ ctx,
482
+ `Goal could not be edited: ${error instanceof Error ? error.message : String(error)}`,
483
+ "Automatic continuation is stopped. Retry /goal edit after session storage recovers.",
484
+ );
485
+ } else {
486
+ reportError(ctx, "Goal could not be edited", error);
487
+ }
488
+ }
489
+ return;
490
+ }
491
+
492
+ if (initState.active) {
493
+ ctx.ui.notify("Wait for /init to finish before starting a goal", "error");
494
+ return;
495
+ }
496
+ switch (command.kind) {
497
+ case "objective":
498
+ case "start":
499
+ break;
500
+ default: {
501
+ const unhandled: never = command;
502
+ return unhandled;
503
+ }
504
+ }
505
+ const objective = command.objective;
506
+ const controlledStart = command.kind === "start" ? command : undefined;
507
+
508
+ let completionCheck: GoalCompletionCheck | undefined;
509
+ if (controlledStart?.completionCheckName) {
510
+ try {
511
+ completionCheck = resolveGoalCompletionCheck(ctx, controlledStart.completionCheckName);
512
+ } catch (error) {
513
+ reportError(ctx, "Goal completion check could not be resolved", error);
514
+ return;
515
+ }
516
+ }
517
+
518
+ const unfinished = runtime.state && runtime.state.status !== "complete";
519
+ if (unfinished) {
520
+ if (!ctx.hasUI) {
521
+ ctx.ui.notify("Clear the current goal before replacing it outside TUI mode", "error");
522
+ return;
523
+ }
524
+ const replace = await ctx.ui.confirm("Replace active goal", "Replace the current unfinished goal and discard its continuation state?");
525
+ if (!replace) return;
526
+ }
527
+
528
+ runtime.continuationHeld = true;
529
+ let waitError: unknown;
530
+ try {
531
+ await ctx.waitForIdle();
532
+ } catch (error) {
533
+ waitError = error;
534
+ } finally {
535
+ runtime.continuationHeld = false;
536
+ }
537
+ if (waitError) {
538
+ reportError(ctx, "Goal could not wait for the active turn", waitError);
539
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
540
+ return;
541
+ }
542
+ try {
543
+ if (controlledStart?.completionCheckName) {
544
+ completionCheck = resolveGoalCompletionCheck(ctx, controlledStart.completionCheckName);
545
+ }
546
+ const verification = await inferGoalVerification(objective);
547
+ const state = createNewGoalState(objective, sumGoalTokens(ctx), verification, Date.now(), {
548
+ ...(completionCheck === undefined ? {} : { completionCheck }),
549
+ maxTurns: controlledStart?.maxTurns ?? DEFAULT_GOAL_MAX_TURNS,
550
+ });
551
+ persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
552
+ if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
553
+ ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
554
+ }
555
+ } catch (error) {
556
+ if (!unfinished) {
557
+ reportError(ctx, "Goal could not be started", error);
558
+ } else if (runtime.state?.status === "active") {
559
+ pauseGoalAfterFailure(
560
+ pi,
561
+ runtime,
562
+ ctx,
563
+ `Goal could not be replaced: ${error instanceof Error ? error.message : String(error)}`,
564
+ "Automatic continuation is stopped. Retry replacement after session storage recovers.",
565
+ );
566
+ } else {
567
+ reportError(ctx, "Goal could not be replaced", error);
568
+ }
569
+ }
570
+ };
571
+
572
+ pi.registerCommand("goal", {
573
+ description: "Set a non-command objective or view the current goal",
574
+ getArgumentCompletions: (prefix) => {
575
+ const normalized = prefix.trimStart().toLowerCase();
576
+ const actions = normalized.startsWith("start ")
577
+ ? [
578
+ { value: "start --check ", description: "Bind a named completion check" },
579
+ { value: "start --turns ", description: "Set a goal turn limit" },
580
+ { value: "start -- ", description: "Start a goal with strict syntax" },
581
+ ]
582
+ : [
583
+ { value: "clear", description: "Remove the current goal" },
584
+ { value: "edit", description: "Edit and reactivate the current goal" },
585
+ { value: "pause", description: "Stop automatic continuation" },
586
+ { value: "resume", description: "Resume automatic continuation" },
587
+ { value: "start", description: "Start with optional controls" },
588
+ { value: "check", description: "Set or clear a completion check" },
589
+ { value: "checks", description: "List completion checks" },
590
+ { value: "limit", description: "Set or clear a turn limit" },
591
+ { value: "history", description: "Show goal history" },
592
+ ];
593
+ return actions
594
+ .filter((action) => action.value.startsWith(normalized))
595
+ .map((action) => ({ ...action, label: action.value.trimEnd() }));
596
+ },
597
+ handler: handleGoalCommand,
598
+ });
599
+ }