killeros 2.1.22 → 2.1.24
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 +23 -0
- package/README.md +18 -23
- package/killeros/change-receipt.ts +1 -1
- package/killeros/commands.ts +1 -1
- package/killeros/display.ts +16 -7
- package/killeros/footer.ts +2 -7
- package/killeros/goal-command.ts +10 -107
- package/killeros/goal-interface.ts +48 -227
- package/killeros/goal-runtime.ts +1 -3
- package/killeros/goal-state.ts +29 -83
- package/killeros/hooks.ts +2 -82
- package/killeros/runtime.ts +0 -7
- package/killeros/worked-for.ts +59 -8
- package/package.json +8 -7
- package/killeros/goal-history.ts +0 -71
|
@@ -6,11 +6,9 @@ import { BoundedText } from "./bounded-text.ts";
|
|
|
6
6
|
import { formatTime, formatTokens } from "./display.ts";
|
|
7
7
|
import { reportError } from "./errors.ts";
|
|
8
8
|
import { parseGoalCommand } from "./goal-command.ts";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { listGoalCompletionChecks, resolveGoalCompletionCheck, runGoalCompletionCheck } from "./hooks.ts";
|
|
13
|
-
import type { GoalCompletionCheck, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.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";
|
|
14
12
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
15
13
|
|
|
16
14
|
const GoalUpdateParams = Type.Object({
|
|
@@ -33,7 +31,7 @@ const GoalUpdateParams = Type.Object({
|
|
|
33
31
|
interface GoalUpdateDetails {
|
|
34
32
|
status: "complete" | "blocked" | "blocker-audit";
|
|
35
33
|
evidence: string;
|
|
36
|
-
verification?: "file" | "
|
|
34
|
+
verification?: "file" | "model-reported";
|
|
37
35
|
blockerKey?: string;
|
|
38
36
|
streak?: number;
|
|
39
37
|
}
|
|
@@ -42,17 +40,12 @@ function goalStatusLabel(status: GoalStatus): string {
|
|
|
42
40
|
return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "
|
|
46
|
-
|
|
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];
|
|
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" }];
|
|
52
45
|
if (status === "paused" || status === "blocked") {
|
|
53
|
-
return [{ label: "Resume automatic continuation", control: "resume" },
|
|
46
|
+
return [{ label: "Resume automatic continuation", control: "resume" }, { label: "Clear goal", control: "clear" }];
|
|
54
47
|
}
|
|
55
|
-
return
|
|
48
|
+
return [{ label: "Clear goal", control: "clear" }];
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
@@ -62,7 +55,7 @@ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
|
62
55
|
: `${state.turns}/${state.maxTurns} turns`;
|
|
63
56
|
const lines = [
|
|
64
57
|
`Goal ${goalStatusLabel(state.status).toLowerCase()} · ${turns} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))} · ${formatTokens(usedTokens)} tokens`,
|
|
65
|
-
...(state.
|
|
58
|
+
...(state.verification === undefined ? [] : [`Deliverable: ${state.verification.path}`]),
|
|
66
59
|
state.objective,
|
|
67
60
|
];
|
|
68
61
|
if (state.result) lines.push(state.result);
|
|
@@ -105,21 +98,13 @@ export function registerGoalInterface(
|
|
|
105
98
|
if (!evidence) throw new Error("Goal evidence must not be empty");
|
|
106
99
|
if (params.status === "complete") {
|
|
107
100
|
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
101
|
if (runtime.state !== state) throw new Error("Goal changed while completion was being verified");
|
|
111
|
-
const verification = state.verification
|
|
112
|
-
? "file-and-check"
|
|
113
|
-
: state.verification ? "file" : state.completionCheck ? "check" : "model-reported";
|
|
102
|
+
const verification = state.verification ? "file" : "model-reported";
|
|
114
103
|
transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
|
|
115
104
|
const safeEvidence = safeTerminalText(evidence);
|
|
116
|
-
const text = state.verification
|
|
117
|
-
? `Goal verified complete
|
|
118
|
-
:
|
|
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}`;
|
|
105
|
+
const text = state.verification
|
|
106
|
+
? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${safeEvidence}`
|
|
107
|
+
: `Goal marked complete (model-reported): ${safeEvidence}`;
|
|
123
108
|
return {
|
|
124
109
|
content: [{ type: "text", text }],
|
|
125
110
|
details: { status: "complete", evidence, verification },
|
|
@@ -198,99 +183,6 @@ export function registerGoalInterface(
|
|
|
198
183
|
return;
|
|
199
184
|
}
|
|
200
185
|
|
|
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
186
|
if (command.kind === "clear") {
|
|
295
187
|
if (!runtime.state) {
|
|
296
188
|
ctx.ui.notify("No goal is set", "info");
|
|
@@ -401,17 +293,30 @@ export function registerGoalInterface(
|
|
|
401
293
|
return;
|
|
402
294
|
}
|
|
403
295
|
if (runtime.state.status === "complete") {
|
|
404
|
-
ctx.ui.notify("The goal is complete. Set a new objective
|
|
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");
|
|
296
|
+
ctx.ui.notify("The goal is complete. Set a new objective.", "info");
|
|
409
297
|
return;
|
|
410
298
|
}
|
|
411
299
|
if (runtime.state.status === "active") {
|
|
412
300
|
ctx.ui.notify("Goal is already active", "info");
|
|
413
301
|
return;
|
|
414
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
|
+
}
|
|
415
320
|
try {
|
|
416
321
|
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
417
322
|
runtime.continuationScheduled = false;
|
|
@@ -422,80 +327,12 @@ export function registerGoalInterface(
|
|
|
422
327
|
return;
|
|
423
328
|
}
|
|
424
329
|
|
|
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
330
|
if (initState.active) {
|
|
493
331
|
ctx.ui.notify("Wait for /init to finish before starting a goal", "error");
|
|
494
332
|
return;
|
|
495
333
|
}
|
|
496
334
|
switch (command.kind) {
|
|
497
335
|
case "objective":
|
|
498
|
-
case "start":
|
|
499
336
|
break;
|
|
500
337
|
default: {
|
|
501
338
|
const unhandled: never = command;
|
|
@@ -503,17 +340,6 @@ export function registerGoalInterface(
|
|
|
503
340
|
}
|
|
504
341
|
}
|
|
505
342
|
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
343
|
|
|
518
344
|
const unfinished = runtime.state && runtime.state.status !== "complete";
|
|
519
345
|
if (unfinished) {
|
|
@@ -539,14 +365,21 @@ export function registerGoalInterface(
|
|
|
539
365
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
540
366
|
return;
|
|
541
367
|
}
|
|
368
|
+
let verification: Awaited<ReturnType<typeof inferGoalVerification>>;
|
|
542
369
|
try {
|
|
543
|
-
|
|
544
|
-
|
|
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);
|
|
545
377
|
}
|
|
546
|
-
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
547
381
|
const state = createNewGoalState(objective, sumGoalTokens(ctx), verification, Date.now(), {
|
|
548
|
-
|
|
549
|
-
maxTurns: controlledStart?.maxTurns ?? DEFAULT_GOAL_MAX_TURNS,
|
|
382
|
+
maxTurns: DEFAULT_GOAL_MAX_TURNS,
|
|
550
383
|
});
|
|
551
384
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
552
385
|
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
|
|
@@ -573,23 +406,11 @@ export function registerGoalInterface(
|
|
|
573
406
|
description: "Set a non-command objective or view the current goal",
|
|
574
407
|
getArgumentCompletions: (prefix) => {
|
|
575
408
|
const normalized = prefix.trimStart().toLowerCase();
|
|
576
|
-
const actions =
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
];
|
|
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
|
+
];
|
|
593
414
|
return actions
|
|
594
415
|
.filter((action) => action.value.startsWith(normalized))
|
|
595
416
|
.map((action) => ({ ...action, label: action.value.trimEnd() }));
|
package/killeros/goal-runtime.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
|
9
9
|
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
10
10
|
export const GOAL_UPDATE_TOOL = "killeros_goal_update";
|
|
11
11
|
|
|
12
|
-
export type GoalEntryEvent = "set" | "replace" | "
|
|
12
|
+
export type GoalEntryEvent = "set" | "replace" | "limit" | "turn" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint" | "blocker-audit";
|
|
13
13
|
export interface GoalEntryData {
|
|
14
14
|
version: 1;
|
|
15
15
|
event: GoalEntryEvent;
|
|
@@ -125,14 +125,12 @@ export function pauseGoalAtTurnLimit(
|
|
|
125
125
|
pi: ExtensionAPI,
|
|
126
126
|
runtime: GoalRuntime,
|
|
127
127
|
ctx: ExtensionContext,
|
|
128
|
-
notify = true,
|
|
129
128
|
): boolean {
|
|
130
129
|
const state = runtime.state;
|
|
131
130
|
if (state?.status !== "active" || state.maxTurns === undefined || state.turns < state.maxTurns) return false;
|
|
132
131
|
const result = `Turn limit reached (${state.turns}/${state.maxTurns}).`;
|
|
133
132
|
try {
|
|
134
133
|
transitionGoal(pi, runtime, "limit", "paused", result);
|
|
135
|
-
if (notify) ctx.ui.notify(`Goal paused: turn limit reached (${state.turns}/${state.maxTurns})`, "warning");
|
|
136
134
|
} catch (error) {
|
|
137
135
|
pauseGoalAfterFailure(pi, runtime, ctx, `turn limit pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
136
|
}
|
package/killeros/goal-state.ts
CHANGED
|
@@ -2,12 +2,11 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { Stats } from "node:fs";
|
|
3
3
|
import { lstat, open, type FileHandle } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import type { GoalBlockerAudit,
|
|
5
|
+
import type { GoalBlockerAudit, GoalFileBaseline, GoalFileVerification, GoalState, GoalStateCommon, GoalStatus } from "./runtime.ts";
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_GOAL_MAX_TURNS = 20;
|
|
8
8
|
export const GOAL_OBJECTIVE_LIMIT = 4_000;
|
|
9
9
|
export const GOAL_MAX_TURNS = 10_000;
|
|
10
|
-
export const GOAL_CHECK_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
11
10
|
export const GOAL_VERSION = 1;
|
|
12
11
|
const FILE_HASH_CHUNK_SIZE = 64 * 1024;
|
|
13
12
|
export const FILE_HASH_LIMIT = 64 * 1024 * 1024;
|
|
@@ -64,6 +63,14 @@ function isAbsoluteFilePath(value: string): boolean {
|
|
|
64
63
|
return path.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
function stripUnquotedPathPunctuation(value: string): string {
|
|
67
|
+
const pathWithoutMarks = value.replace(/[.!?]+$/u, "");
|
|
68
|
+
const trailingClosers = pathWithoutMarks.match(/\)+$/u)?.[0].length ?? 0;
|
|
69
|
+
const unmatchedClosers = Math.max(0, pathWithoutMarks.split(")").length - pathWithoutMarks.split("(").length);
|
|
70
|
+
const punctuationLength = Math.min(trailingClosers, unmatchedClosers);
|
|
71
|
+
return pathWithoutMarks.slice(0, punctuationLength ? -punctuationLength : undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
68
75
|
return isUnknownRecord(value)
|
|
69
76
|
&& value.kind === "file"
|
|
@@ -73,15 +80,6 @@ function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
|
73
80
|
&& isGoalFileBaseline(value.baseline);
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
function isGoalCompletionCheck(value: unknown): value is GoalCompletionCheck {
|
|
77
|
-
return isUnknownRecord(value)
|
|
78
|
-
&& value.kind === "named-command"
|
|
79
|
-
&& typeof value.name === "string"
|
|
80
|
-
&& GOAL_CHECK_NAME_PATTERN.test(value.name)
|
|
81
|
-
&& typeof value.configHash === "string"
|
|
82
|
-
&& /^[a-f0-9]{64}$/u.test(value.configHash);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
83
|
function isMaxTurns(value: unknown): value is number {
|
|
86
84
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= GOAL_MAX_TURNS;
|
|
87
85
|
}
|
|
@@ -118,7 +116,6 @@ export function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
118
116
|
resumeAfterManualCompaction,
|
|
119
117
|
blockerAudit,
|
|
120
118
|
verification,
|
|
121
|
-
completionCheck,
|
|
122
119
|
maxTurns,
|
|
123
120
|
} = value;
|
|
124
121
|
if (version !== GOAL_VERSION
|
|
@@ -134,7 +131,6 @@ export function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
134
131
|
|| !safeNonNegativeInteger(baselineTokens)
|
|
135
132
|
|| result !== undefined && typeof result !== "string"
|
|
136
133
|
|| verification !== undefined && !isGoalFileVerification(verification)
|
|
137
|
-
|| completionCheck !== undefined && !isGoalCompletionCheck(completionCheck)
|
|
138
134
|
|| maxTurns !== undefined && !isMaxTurns(maxTurns)
|
|
139
135
|
|| resumeAfterManualCompaction !== undefined && resumeAfterManualCompaction !== true
|
|
140
136
|
|| blockerAudit !== undefined && !isGoalBlockerAudit(blockerAudit, turns, status)) {
|
|
@@ -152,7 +148,6 @@ export function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
152
148
|
blockedAuditStartTurn: blockedAuditStartTurn ?? 0,
|
|
153
149
|
baselineTokens,
|
|
154
150
|
...(verification === undefined ? {} : { verification }),
|
|
155
|
-
...(completionCheck === undefined ? {} : { completionCheck }),
|
|
156
151
|
...(maxTurns === undefined ? {} : { maxTurns }),
|
|
157
152
|
};
|
|
158
153
|
switch (status) {
|
|
@@ -238,13 +233,26 @@ export async function captureGoalFileBaseline(
|
|
|
238
233
|
}
|
|
239
234
|
}
|
|
240
235
|
|
|
241
|
-
/** Captures one explicit
|
|
242
|
-
export async function inferGoalVerification(objective: string): Promise<GoalFileVerification | undefined> {
|
|
236
|
+
/** Captures one explicit output path so goal completion can verify its creation or modification. */
|
|
237
|
+
export async function inferGoalVerification(objective: string, cwd: string): Promise<GoalFileVerification | undefined> {
|
|
238
|
+
const candidates: string[] = [];
|
|
243
239
|
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;
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
.
|
|
247
|
-
|
|
240
|
+
for (const match of objective.matchAll(destination)) {
|
|
241
|
+
const quoted = match[1] ?? match[2] ?? match[3];
|
|
242
|
+
candidates.push(quoted !== undefined ? quoted.trim() : stripUnquotedPathPunctuation((match[4] ?? "").trim()));
|
|
243
|
+
}
|
|
244
|
+
const direct = /\b(?:update|edit|fix|refactor|migrate)\s+(`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)')/giu;
|
|
245
|
+
for (const match of objective.matchAll(direct)) {
|
|
246
|
+
const quoted = match[2] ?? match[3] ?? match[4];
|
|
247
|
+
if (quoted !== undefined) candidates.push(quoted.trim());
|
|
248
|
+
}
|
|
249
|
+
const resolved: string[] = [];
|
|
250
|
+
for (const raw of candidates) {
|
|
251
|
+
if (!raw || /^(?:https?|file):\/\//iu.test(raw) || /[\\\/]$/u.test(raw)) continue;
|
|
252
|
+
const absolute = path.isAbsolute(raw) || path.win32.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
253
|
+
if (isAbsoluteFilePath(absolute)) resolved.push(absolute);
|
|
254
|
+
}
|
|
255
|
+
const unique = [...new Set(resolved)];
|
|
248
256
|
const filePath = unique.length === 1 ? unique[0] : undefined;
|
|
249
257
|
return filePath ? { kind: "file", path: filePath, baseline: await captureGoalFileBaseline(filePath) } : undefined;
|
|
250
258
|
}
|
|
@@ -302,7 +310,6 @@ export function commonGoalState(state: GoalState): GoalStateCommon {
|
|
|
302
310
|
blockedAuditStartTurn: state.blockedAuditStartTurn,
|
|
303
311
|
baselineTokens: state.baselineTokens,
|
|
304
312
|
...(state.verification === undefined ? {} : { verification: state.verification }),
|
|
305
|
-
...(state.completionCheck === undefined ? {} : { completionCheck: state.completionCheck }),
|
|
306
313
|
...(state.maxTurns === undefined ? {} : { maxTurns: state.maxTurns }),
|
|
307
314
|
};
|
|
308
315
|
}
|
|
@@ -319,7 +326,7 @@ export function createNewGoalState(
|
|
|
319
326
|
baselineTokens: number,
|
|
320
327
|
verification: GoalFileVerification | undefined,
|
|
321
328
|
now: number,
|
|
322
|
-
controls: {
|
|
329
|
+
controls: { maxTurns?: number } = {},
|
|
323
330
|
): GoalState {
|
|
324
331
|
return {
|
|
325
332
|
version: GOAL_VERSION,
|
|
@@ -334,71 +341,10 @@ export function createNewGoalState(
|
|
|
334
341
|
blockedAuditStartTurn: 0,
|
|
335
342
|
baselineTokens,
|
|
336
343
|
...(verification === undefined ? {} : { verification }),
|
|
337
|
-
...(controls.completionCheck === undefined ? {} : { completionCheck: controls.completionCheck }),
|
|
338
344
|
...(controls.maxTurns === undefined ? {} : { maxTurns: controls.maxTurns }),
|
|
339
345
|
};
|
|
340
346
|
}
|
|
341
347
|
|
|
342
|
-
export function editGoalState(
|
|
343
|
-
state: GoalState,
|
|
344
|
-
objective: string,
|
|
345
|
-
verification: GoalFileVerification | undefined,
|
|
346
|
-
now: number,
|
|
347
|
-
): GoalState {
|
|
348
|
-
const current = stopGoalClock(state, now);
|
|
349
|
-
const { verification: _previousVerification, ...common } = current;
|
|
350
|
-
return {
|
|
351
|
-
...common,
|
|
352
|
-
revision: current.revision + 1,
|
|
353
|
-
objective,
|
|
354
|
-
status: "active",
|
|
355
|
-
updatedAt: now,
|
|
356
|
-
activeStartedAt: now,
|
|
357
|
-
blockedAuditStartTurn: current.turns,
|
|
358
|
-
...(verification === undefined ? {} : { verification }),
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export function updateGoalControlsState(
|
|
363
|
-
state: Exclude<GoalState, { status: "complete" }>,
|
|
364
|
-
controls: { completionCheck?: GoalCompletionCheck; maxTurns?: number },
|
|
365
|
-
now: number,
|
|
366
|
-
): GoalState {
|
|
367
|
-
const stopped = stopGoalClock(state, now);
|
|
368
|
-
const { completionCheck: _completionCheck, maxTurns: _maxTurns, ...common } = stopped;
|
|
369
|
-
const nextCommon: GoalStateCommon = {
|
|
370
|
-
...common,
|
|
371
|
-
revision: common.revision + 1,
|
|
372
|
-
updatedAt: now,
|
|
373
|
-
...(controls.completionCheck === undefined ? {} : { completionCheck: controls.completionCheck }),
|
|
374
|
-
...(controls.maxTurns === undefined ? {} : { maxTurns: controls.maxTurns }),
|
|
375
|
-
};
|
|
376
|
-
if (state.status === "active") {
|
|
377
|
-
return {
|
|
378
|
-
...nextCommon,
|
|
379
|
-
status: "active",
|
|
380
|
-
activeStartedAt: now,
|
|
381
|
-
...(state.result === undefined ? {} : { result: state.result }),
|
|
382
|
-
...(state.blockerAudit === undefined ? {} : { blockerAudit: state.blockerAudit }),
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
if (state.status === "paused") {
|
|
386
|
-
return {
|
|
387
|
-
...nextCommon,
|
|
388
|
-
status: "paused",
|
|
389
|
-
...(state.result === undefined ? {} : { result: state.result }),
|
|
390
|
-
...(state.blockerAudit === undefined ? {} : { blockerAudit: state.blockerAudit }),
|
|
391
|
-
...(state.resumeAfterManualCompaction === undefined ? {} : { resumeAfterManualCompaction: true }),
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
...nextCommon,
|
|
396
|
-
status: "blocked",
|
|
397
|
-
result: state.result,
|
|
398
|
-
...(state.blockerAudit === undefined ? {} : { blockerAudit: state.blockerAudit }),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
348
|
export function beginGoalTurnState(
|
|
403
349
|
current: Extract<GoalState, { status: "active" }>,
|
|
404
350
|
now: number,
|