killeros 2.0.5 → 2.0.7

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/killeros/goals.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { lstatSync } from "node:fs";
3
+ import path from "node:path";
2
4
  import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
3
5
  import { Text } from "@earendil-works/pi-tui";
4
6
  import { Type } from "typebox";
@@ -7,10 +9,11 @@ import { BoundedText } from "./bounded-text.ts";
7
9
  import { formatTime, formatTokens } from "./display.ts";
8
10
  import { reportError } from "./errors.ts";
9
11
  import { resolvePersonalInstructions } from "./personal-instructions.ts";
10
- import type { GoalBlockerAudit, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
12
+ import type { GoalBlockerAudit, GoalFileVerification, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
11
13
 
12
14
  const GOAL_ENTRY_TYPE = "killeros-goal";
13
15
  const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
16
+ const GOAL_UPDATE_TOOL = "killeros_goal_update";
14
17
  const GOAL_OBJECTIVE_LIMIT = 4_000;
15
18
  const GOAL_VERSION = 1;
16
19
 
@@ -29,7 +32,6 @@ interface GoalTransitionOptions {
29
32
 
30
33
  interface RestoredGoalState {
31
34
  state?: GoalState;
32
- recoveryProven: boolean;
33
35
  }
34
36
 
35
37
  const GoalUpdateParams = Type.Object({
@@ -52,6 +54,7 @@ const GoalUpdateParams = Type.Object({
52
54
  interface GoalUpdateDetails {
53
55
  status: "complete" | "blocked" | "blocker-audit";
54
56
  evidence: string;
57
+ verification?: "file" | "model-reported";
55
58
  blockerKey?: string;
56
59
  streak?: number;
57
60
  }
@@ -64,6 +67,41 @@ function finiteNonNegative(value: unknown): value is number {
64
67
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
65
68
  }
66
69
 
70
+ function isGoalFileVerification(value: unknown): value is GoalFileVerification {
71
+ if (!value || typeof value !== "object") return false;
72
+ const candidate = value as Partial<GoalFileVerification>;
73
+ return candidate.kind === "file"
74
+ && typeof candidate.path === "string"
75
+ && candidate.path === candidate.path.trim()
76
+ && isAbsoluteFilePath(candidate.path);
77
+ }
78
+
79
+ function isAbsoluteFilePath(value: string): boolean {
80
+ if (!value || /^(?:https?|file):\/\//iu.test(value) || /[\\\/]$/u.test(value)) return false;
81
+ return path.isAbsolute(value) || path.win32.isAbsolute(value);
82
+ }
83
+
84
+ function inferGoalVerification(objective: string): GoalFileVerification | undefined {
85
+ const destination = /\b(?:create|write|save|generate)\b[^\r\n]{0,160}?\b(?:file|document|markdown|report|spreadsheet|presentation|image)\b\s+(?:to|at|as|destination(?:\s+is)?|output(?:\s+(?:to|at))?)\b\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z]:\\[^\s,;]+|\/[^\s,;]+))/giu;
86
+ const paths = [...objective.matchAll(destination)]
87
+ .map((match) => (match[1] ?? match[2] ?? match[3] ?? match[4] ?? "").trim())
88
+ .filter(isAbsoluteFilePath);
89
+ const unique = [...new Set(paths)];
90
+ return unique.length === 1 ? { kind: "file", path: unique[0]! } : undefined;
91
+ }
92
+
93
+ function verifyGoalDeliverable(verification: GoalFileVerification): void {
94
+ let artifact: ReturnType<typeof lstatSync>;
95
+ try {
96
+ artifact = lstatSync(verification.path);
97
+ } catch {
98
+ throw new Error(`Goal deliverable is not a regular file at the required path: ${verification.path}`);
99
+ }
100
+ if (!artifact.isFile()) {
101
+ throw new Error(`Goal deliverable is not a regular file at the required path: ${verification.path}`);
102
+ }
103
+ }
104
+
67
105
  function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
68
106
  if (!value || typeof value !== "object") return false;
69
107
  const candidate = value as Partial<GoalBlockerAudit>;
@@ -92,6 +130,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
92
130
  || !finiteNonNegative(candidate.baselineTokens)
93
131
  || candidate.activeStartedAt !== undefined && !finiteNonNegative(candidate.activeStartedAt)
94
132
  || candidate.result !== undefined && typeof candidate.result !== "string"
133
+ || candidate.verification !== undefined && !isGoalFileVerification(candidate.verification)
95
134
  || candidate.blockerAudit !== undefined && !isGoalBlockerAudit(candidate.blockerAudit, candidate.turns!, candidate.status)
96
135
  || candidate.resumeAfterManualCompaction !== undefined && candidate.resumeAfterManualCompaction !== true
97
136
  || candidate.resumeAfterManualCompaction === true && candidate.status !== "paused") {
@@ -112,6 +151,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
112
151
  result: candidate.result,
113
152
  resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
114
153
  blockerAudit: candidate.blockerAudit,
154
+ verification: candidate.verification,
115
155
  };
116
156
  }
117
157
 
@@ -130,19 +170,17 @@ function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
130
170
  if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
131
171
  const data = entry.data as Partial<GoalEntryData> | undefined;
132
172
  if (!data || data.version !== GOAL_VERSION || data.state === null) {
133
- return { state: undefined, recoveryProven: false };
173
+ return { state: undefined };
134
174
  }
135
175
  const restored = parseGoalState(data.state);
136
- if (!restored) return { state: undefined, recoveryProven: false };
176
+ if (!restored) return { state: undefined };
137
177
  const state = restored.status === "active"
138
178
  ? { ...restored, activeStartedAt: Date.now() }
139
179
  : { ...restored, activeStartedAt: undefined };
140
- const recoveryProven = state.status === "paused"
141
- && state.resumeAfterManualCompaction === true
142
- && entries.slice(index + 1).some((candidate) => candidate.type === "compaction");
143
- return { state, recoveryProven };
180
+ if (state.status === "paused") state.resumeAfterManualCompaction = undefined;
181
+ return { state };
144
182
  }
145
- return { state: undefined, recoveryProven: false };
183
+ return { state: undefined };
146
184
  }
147
185
 
148
186
  export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
@@ -173,6 +211,21 @@ function sumGoalTokens(ctx: ExtensionContext): number {
173
211
  return total;
174
212
  }
175
213
 
214
+ function setGoalUpdateToolActive(pi: ExtensionAPI, active: boolean): void {
215
+ const api = pi as ExtensionAPI & { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
216
+ if (!api.getActiveTools || !api.setActiveTools) return;
217
+ const activeTools = api.getActiveTools();
218
+ const isActive = activeTools.includes(GOAL_UPDATE_TOOL);
219
+ if (active === isActive) return;
220
+ api.setActiveTools(active
221
+ ? [...activeTools, GOAL_UPDATE_TOOL]
222
+ : activeTools.filter((name) => name !== GOAL_UPDATE_TOOL));
223
+ }
224
+
225
+ function syncGoalUpdateTool(pi: ExtensionAPI, runtime: GoalRuntime): void {
226
+ setGoalUpdateToolActive(pi, runtime.state?.status === "active");
227
+ }
228
+
176
229
  function persistGoalState(
177
230
  pi: ExtensionAPI,
178
231
  runtime: GoalRuntime,
@@ -182,6 +235,7 @@ function persistGoalState(
182
235
  const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
183
236
  pi.appendEntry(GOAL_ENTRY_TYPE, data);
184
237
  runtime.state = state;
238
+ syncGoalUpdateTool(pi, runtime);
185
239
  runtime.persistenceRetryNeeded = false;
186
240
  runtime.requestRender?.();
187
241
  }
@@ -276,6 +330,7 @@ export function pauseGoalAfterFailure(
276
330
  result: reason,
277
331
  resumeAfterManualCompaction: undefined,
278
332
  } : undefined;
333
+ syncGoalUpdateTool(pi, runtime);
279
334
  runtime.persistenceRetryNeeded = true;
280
335
  runtime.continuationScheduled = false;
281
336
  runtime.requestRender?.();
@@ -301,6 +356,7 @@ function pauseGoalForPossibleManualCompaction(
301
356
  result: reason,
302
357
  resumeAfterManualCompaction: true,
303
358
  } : undefined;
359
+ syncGoalUpdateTool(pi, runtime);
304
360
  runtime.persistenceRetryNeeded = true;
305
361
  runtime.continuationScheduled = false;
306
362
  runtime.requestRender?.();
@@ -333,6 +389,34 @@ function recoverGoalAfterManualCompaction(
333
389
  return true;
334
390
  }
335
391
 
392
+ function beginGoalTurn(
393
+ pi: ExtensionAPI,
394
+ runtime: GoalRuntime,
395
+ ctx: ExtensionContext,
396
+ current: GoalState,
397
+ ): GoalState | undefined {
398
+ const now = Date.now();
399
+ const next: GoalState = {
400
+ ...current,
401
+ revision: current.revision + 1,
402
+ turns: current.turns + 1,
403
+ updatedAt: now,
404
+ activeStartedAt: current.activeStartedAt ?? now,
405
+ resumeAfterManualCompaction: undefined,
406
+ };
407
+ try {
408
+ persistGoalState(pi, runtime, "turn", next);
409
+ } catch (error) {
410
+ pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
411
+ return undefined;
412
+ }
413
+ runtime.goalTurnInFlight = true;
414
+ runtime.agentEndObserved = false;
415
+ runtime.lastStopReason = undefined;
416
+ runtime.lastError = undefined;
417
+ return next;
418
+ }
419
+
336
420
  function scheduleGoalContinuation(
337
421
  pi: ExtensionAPI,
338
422
  runtime: GoalRuntime,
@@ -348,16 +432,13 @@ function scheduleGoalContinuation(
348
432
  || initState.active
349
433
  || !ctx.isIdle()
350
434
  || ctx.hasPendingMessages()) return false;
351
- const current = runtime.state;
352
435
  runtime.continuationScheduled = true;
353
- runtime.goalTurnInFlight = false;
354
- runtime.agentEndObserved = false;
355
- runtime.lastStopReason = undefined;
356
- runtime.lastError = undefined;
436
+ const next = beginGoalTurn(pi, runtime, ctx, runtime.state);
437
+ if (!next) return false;
357
438
  try {
358
439
  pi.sendMessage({
359
440
  customType: GOAL_CONTINUATION_TYPE,
360
- content: goalContinuationMessage(current, ctx),
441
+ content: goalContinuationMessage(next, ctx),
361
442
  display: false,
362
443
  }, { triggerTurn: true, deliverAs: "followUp" });
363
444
  return true;
@@ -440,7 +521,7 @@ export function registerGoal(
440
521
  });
441
522
 
442
523
  pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
443
- name: "killeros_goal_update",
524
+ name: GOAL_UPDATE_TOOL,
444
525
  label: "Goal update",
445
526
  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.",
446
527
  parameters: GoalUpdateParams,
@@ -453,10 +534,14 @@ export function registerGoal(
453
534
  const evidence = params.evidence.trim();
454
535
  if (!evidence) throw new Error("Goal evidence must not be empty");
455
536
  if (params.status === "complete") {
537
+ if (state.verification) verifyGoalDeliverable(state.verification);
538
+ const verification = state.verification ? "file" : "model-reported";
456
539
  transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
457
540
  return {
458
- content: [{ type: "text", text: `Goal marked complete: ${evidence}` }],
459
- details: { status: "complete", evidence },
541
+ content: [{ type: "text", text: state.verification
542
+ ? `Goal verified complete at ${state.verification.path}: ${evidence}`
543
+ : `Goal marked complete (model-reported): ${evidence}` }],
544
+ details: { status: "complete", evidence, verification },
460
545
  };
461
546
  }
462
547
  if (!runtime.goalTurnInFlight) throw new Error("A blocker audit can only be recorded during an active KillerOS goal turn");
@@ -491,7 +576,12 @@ export function registerGoal(
491
576
  renderCall(args, theme) {
492
577
  return new Text(`${theme.fg("toolTitle", theme.bold("goal "))}${theme.fg("muted", args.status)}`, 0, 0);
493
578
  },
494
- renderResult(result, options, theme) {
579
+ renderResult(result, options, theme, context) {
580
+ if (context?.isError) {
581
+ const first = result.content[0];
582
+ const message = first?.type === "text" ? first.text : "Goal update failed";
583
+ return new BoundedText(theme.fg("error", message), options.expanded ? undefined : 3);
584
+ }
495
585
  const details = result.details;
496
586
  if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
497
587
  const label = details.status === "complete" ? "✓ Complete" : details.status === "blocked" ? "! Blocked" : `! Blocker audit ${details.streak}/3`;
@@ -502,7 +592,8 @@ export function registerGoal(
502
592
 
503
593
  const restoreGoal = (ctx: ExtensionContext): void => {
504
594
  const restored = restoreGoalState(ctx);
505
- runtime.state = restored.state;
595
+ runtime.state = isGoalModeSupported(ctx) ? restored.state : undefined;
596
+ syncGoalUpdateTool(pi, runtime);
506
597
  runtime.continuationScheduled = false;
507
598
  runtime.continuationHeld = false;
508
599
  runtime.goalTurnInFlight = false;
@@ -511,9 +602,7 @@ export function registerGoal(
511
602
  runtime.lastStopReason = undefined;
512
603
  runtime.lastError = undefined;
513
604
  runtime.requestRender?.();
514
- if (restored.recoveryProven) {
515
- recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
516
- } else if (runtime.state?.status === "active") {
605
+ if (runtime.state?.status === "active") {
517
606
  setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
518
607
  }
519
608
  };
@@ -536,6 +625,7 @@ export function registerGoal(
536
625
  }
537
626
  }
538
627
  runtime.state = undefined;
628
+ syncGoalUpdateTool(pi, runtime);
539
629
  runtime.continuationScheduled = false;
540
630
  runtime.continuationHeld = false;
541
631
  runtime.goalTurnInFlight = false;
@@ -550,25 +640,8 @@ export function registerGoal(
550
640
  const current = runtime.state;
551
641
  if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active" || initState.active) return;
552
642
  if (runtime.goalTurnInFlight) return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
553
- const now = Date.now();
554
- const next: GoalState = {
555
- ...current,
556
- revision: current.revision + 1,
557
- turns: current.turns + 1,
558
- updatedAt: now,
559
- activeStartedAt: current.activeStartedAt ?? now,
560
- resumeAfterManualCompaction: undefined,
561
- };
562
- try {
563
- persistGoalState(pi, runtime, "turn", next);
564
- } catch (error) {
565
- pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
566
- return;
567
- }
568
- runtime.goalTurnInFlight = true;
569
- runtime.agentEndObserved = false;
570
- runtime.lastStopReason = undefined;
571
- runtime.lastError = undefined;
643
+ const next = beginGoalTurn(pi, runtime, ctx, current);
644
+ if (!next) return;
572
645
  return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(next)}` };
573
646
  });
574
647
 
@@ -672,6 +745,7 @@ export function registerGoal(
672
745
  ctx.ui.notify("Goal pause saved. Goal remains paused. Automatic compaction recovery is off.", "info");
673
746
  } catch (error) {
674
747
  runtime.state = checkpoint;
748
+ syncGoalUpdateTool(pi, runtime);
675
749
  runtime.persistenceRetryNeeded = true;
676
750
  runtime.continuationScheduled = false;
677
751
  runtime.requestRender?.();
@@ -790,6 +864,7 @@ export function registerGoal(
790
864
  activeStartedAt: now,
791
865
  blockedAuditStartTurn: current.turns,
792
866
  blockerAudit: undefined,
867
+ verification: inferGoalVerification(objective),
793
868
  result: undefined,
794
869
  resumeAfterManualCompaction: undefined,
795
870
  };
@@ -861,6 +936,7 @@ export function registerGoal(
861
936
  turns: 0,
862
937
  blockedAuditStartTurn: 0,
863
938
  baselineTokens: sumGoalTokens(ctx),
939
+ verification: inferGoalVerification(objective),
864
940
  };
865
941
  try {
866
942
  persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
package/killeros/init.ts CHANGED
@@ -133,8 +133,10 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
133
133
 
134
134
  pi.on("session_start", () => setInitTools(pi, initState, false));
135
135
  pi.on("session_shutdown", () => {
136
+ const settle = initState.settle;
136
137
  setInitTools(pi, initState, false);
137
138
  resetInitRuntime(initState);
139
+ settle?.({ kind: "cancelled" });
138
140
  });
139
141
  pi.on("before_agent_start", () => {
140
142
  if (initState.active) setInitTools(pi, initState, true);
@@ -160,7 +162,7 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
160
162
  ctx.ui.notify("/init requires interactive TUI mode", "error");
161
163
  return;
162
164
  }
163
- if (initState.active) {
165
+ if (initState.active || initState.starting) {
164
166
  ctx.ui.notify("/init is already running", "warning");
165
167
  return;
166
168
  }
@@ -172,28 +174,45 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
172
174
  ctx.ui.notify("Trust this project before running /init", "error");
173
175
  return;
174
176
  }
175
- await ctx.waitForIdle();
177
+ const starting = Symbol();
178
+ initState.starting = starting;
179
+ try {
180
+ await ctx.waitForIdle();
181
+ } catch (error) {
182
+ if (initState.starting !== starting) return;
183
+ initState.starting = undefined;
184
+ reportError(ctx, "/init could not wait for active work", error);
185
+ return;
186
+ }
187
+ if (initState.starting !== starting) return;
176
188
 
177
189
  let projectRoot: string;
178
190
  try {
179
191
  projectRoot = await fs.realpath(ctx.cwd);
180
192
  } catch (error) {
193
+ if (initState.starting !== starting) return;
194
+ initState.starting = undefined;
181
195
  reportError(ctx, "/init could not resolve the project root", error);
182
196
  return;
183
197
  }
198
+ if (initState.starting !== starting) return;
184
199
  const targetPath = path.join(projectRoot, "AGENTS.md");
185
200
  try {
186
201
  const [{ index: evidence }, baseline] = await Promise.all([
187
202
  buildInitEvidence(projectRoot),
188
203
  captureInitTargetBaseline(targetPath),
189
204
  ]);
205
+ if (initState.starting !== starting) return;
190
206
  initState.active = true;
191
207
  initState.projectRoot = projectRoot;
192
208
  initState.targetPath = targetPath;
193
209
  initState.evidence = evidence;
194
210
  initState.baseline = baseline;
195
211
  initState.outcome = { kind: "pending" };
212
+ initState.starting = undefined;
196
213
  } catch (error) {
214
+ if (initState.starting !== starting) return;
215
+ initState.starting = undefined;
197
216
  reportError(ctx, "/init could not capture safe repository evidence", error);
198
217
  return;
199
218
  }
@@ -234,6 +253,8 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
234
253
  case "policy-conflict":
235
254
  ctx.ui.notify(`/init left AGENTS.md unchanged: ${outcome.reason}`, "warning");
236
255
  break;
256
+ case "cancelled":
257
+ break;
237
258
  default:
238
259
  reportError(ctx, "/init did not generate AGENTS.md", "the model completed without a write or policy-conflict outcome");
239
260
  }