killeros 2.1.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.
- package/CHANGELOG.md +12 -0
- package/README.md +17 -22
- package/killeros/change-receipt.ts +1 -1
- package/killeros/commands.ts +1 -1
- package/killeros/display.ts +9 -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 +1 -1
- package/package.json +1 -1
- package/killeros/goal-history.ts +0 -71
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.23] - 2026-09-04
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Simplified `/goal` to five forms with proof-by-prose and silent file proof, removing configured completion checks.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Kept the automatic 20-turn goal pause silent as documented.
|
|
16
|
+
- Stripped sentence punctuation from unquoted inferred goal paths and picked token units after rounding so 999.6 shows as 1k and 999999 as 1M.
|
|
17
|
+
- Increased change-receipt Git timeouts to five seconds and stopped timeout failures from showing warning notifications.
|
|
18
|
+
|
|
7
19
|
## [2.1.22] - 2026-09-03
|
|
8
20
|
|
|
9
21
|
### Added
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
5
5
|
## What you get
|
|
6
6
|
|
|
7
7
|
- A custom TUI: startup card with version, model, provider, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
|
|
8
|
-
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals
|
|
8
|
+
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals pause after 20 turns; `/goal resume` grants another 20.
|
|
9
9
|
- `/init`: generates a root `AGENTS.md` from repository evidence, preserving compatible existing rules.
|
|
10
10
|
- `/variants`: pick a reasoning level supported by the active model.
|
|
11
11
|
- `/codex-fast`: toggles the `priority` service tier on Codex requests.
|
|
@@ -34,19 +34,17 @@ Or from GitHub:
|
|
|
34
34
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Pin a release by appending its tag, for example `@v2.1.
|
|
37
|
+
Pin a release by appending its tag, for example `@v2.1.23`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
|
41
41
|
```text
|
|
42
42
|
/init Generate root AGENTS.md from repository evidence
|
|
43
|
-
/goal
|
|
44
|
-
/goal
|
|
45
|
-
/goal
|
|
46
|
-
/goal
|
|
47
|
-
/goal
|
|
48
|
-
/goal history [count] Show transitions and blocker evidence on this branch
|
|
49
|
-
/goal edit|pause|resume|clear
|
|
43
|
+
/goal View the current goal
|
|
44
|
+
/goal <objective> Set an objective
|
|
45
|
+
/goal pause Stop automatic continuation
|
|
46
|
+
/goal resume Resume automatic continuation
|
|
47
|
+
/goal clear Remove the current goal
|
|
50
48
|
/variants Reasoning-level selector (/variants high sets directly)
|
|
51
49
|
/codex-fast Toggle Codex fast mode
|
|
52
50
|
/notification Configure the completion sound
|
|
@@ -60,7 +58,7 @@ Pin a release by appending its tag, for example `@v2.1.22`. Add `-l` to install
|
|
|
60
58
|
| Mode | What works |
|
|
61
59
|
| --- | --- |
|
|
62
60
|
| TUI | Everything |
|
|
63
|
-
| RPC | Goals, proactive compaction; no TUI components, `/
|
|
61
|
+
| RPC | Goals, proactive compaction; no TUI components, `/init`, sounds, title indicator |
|
|
64
62
|
| Print/JSON | No interactive questions, `/goal`, `/init`, or proactive compaction |
|
|
65
63
|
|
|
66
64
|
## Configuration
|
|
@@ -79,22 +77,19 @@ The packaged `killeros` theme activates on TUI start. Compaction triggers by def
|
|
|
79
77
|
|
|
80
78
|
`handoffMaxTokens` caps the `/handoff` summary output at 8192 tokens by default; raise it when long sessions truncate the summary.
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
State proof in the objective so the agent can verify it with its normal tools:
|
|
83
81
|
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
"goalChecks": {
|
|
87
|
-
"quality": {
|
|
88
|
-
"command": "npm run check && npm test",
|
|
89
|
-
"timeoutMs": 300000
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
82
|
+
```text
|
|
83
|
+
/goal Reduce p95 checkout latency below 120 ms, verified by the checkout benchmark, while keeping the correctness suite green
|
|
93
84
|
```
|
|
94
85
|
|
|
95
|
-
|
|
86
|
+
A direct quoted file target binds silent file proof:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
/goal Fix `killeros/footer.ts`, verified by npm test
|
|
90
|
+
```
|
|
96
91
|
|
|
97
|
-
|
|
92
|
+
KillerOS captures the file baseline at goal start and only completes when the file is created or changed. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Restored goals keep their persisted limit.
|
|
98
93
|
|
|
99
94
|
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
100
95
|
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { inflate } from "node:zlib";
|
|
7
7
|
|
|
8
|
-
const GIT_TIMEOUT_MS =
|
|
8
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
9
9
|
const GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;
|
|
10
10
|
const SNAPSHOT_CONTENT_LIMIT = 128 * 1024 * 1024;
|
|
11
11
|
const MAX_DIFF_OPERATIONS = 500_000;
|
package/killeros/commands.ts
CHANGED
|
@@ -70,7 +70,7 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
|
|
|
70
70
|
];
|
|
71
71
|
|
|
72
72
|
const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
|
|
73
|
-
goal: "/goal [objective|
|
|
73
|
+
goal: "/goal [objective|pause|resume|clear]",
|
|
74
74
|
handoff: "/handoff [next-session focus]",
|
|
75
75
|
variants: "/variants [level]",
|
|
76
76
|
model: "/model [provider/model]",
|
package/killeros/display.ts
CHANGED
|
@@ -40,12 +40,14 @@ export function formatTime(milliseconds: number): string {
|
|
|
40
40
|
|
|
41
41
|
export function formatTokens(value: number): string {
|
|
42
42
|
if (!Number.isFinite(value)) return "0";
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
if (
|
|
46
|
-
const precision =
|
|
47
|
-
return `${Number((
|
|
43
|
+
const rounded = Math.round(Math.max(0, value));
|
|
44
|
+
if (rounded < 1_000) return `${rounded}`;
|
|
45
|
+
if (rounded >= 1_000_000) {
|
|
46
|
+
const precision = rounded >= 10_000_000 ? 0 : 1;
|
|
47
|
+
return `${Number((rounded / 1_000_000).toFixed(precision))}M`;
|
|
48
48
|
}
|
|
49
|
-
const precision =
|
|
50
|
-
|
|
49
|
+
const precision = rounded >= 100_000 ? 0 : 1;
|
|
50
|
+
const inK = Number((rounded / 1_000).toFixed(precision));
|
|
51
|
+
if (inK >= 1_000) return `${Number((rounded / 1_000_000).toFixed(1))}M`;
|
|
52
|
+
return `${inK}k`;
|
|
51
53
|
}
|
package/killeros/goal-command.ts
CHANGED
|
@@ -1,120 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { GOAL_CHECK_NAME_PATTERN, GOAL_MAX_TURNS, validateGoalObjective } from "./goal-state.ts";
|
|
3
|
-
|
|
4
|
-
const GOAL_START_USAGE = "Usage: /goal start [--check <name>] [--turns <count>] -- <objective>";
|
|
5
|
-
const GOAL_CHECK_USAGE = "Usage: /goal check <name|clear>";
|
|
6
|
-
const GOAL_CHECKS_USAGE = "Usage: /goal checks";
|
|
7
|
-
const GOAL_LIMIT_USAGE = "Usage: /goal limit <count|clear>";
|
|
8
|
-
const GOAL_HISTORY_USAGE = "Usage: /goal history [count]";
|
|
9
|
-
const RESERVED_OBJECTIVE_USAGE = "Objective begins with a reserved goal command. Use /goal start -- <objective>.";
|
|
10
|
-
|
|
11
|
-
interface ControlledGoalStart {
|
|
12
|
-
objective: string;
|
|
13
|
-
completionCheckName?: string;
|
|
14
|
-
maxTurns?: number;
|
|
15
|
-
}
|
|
1
|
+
import { validateGoalObjective } from "./goal-state.ts";
|
|
16
2
|
|
|
17
3
|
export type GoalCommand =
|
|
18
4
|
| { kind: "status" }
|
|
19
5
|
| { kind: "objective"; objective: string }
|
|
20
|
-
| { kind: "start"; objective: string; completionCheckName?: string; maxTurns?: number }
|
|
21
|
-
| { kind: "check"; value: { kind: "clear" } | { kind: "named"; name: string } }
|
|
22
|
-
| { kind: "checks" }
|
|
23
|
-
| { kind: "limit"; value: { kind: "clear" } | { kind: "count"; count: number } }
|
|
24
|
-
| { kind: "history"; count: number }
|
|
25
|
-
| { kind: "clear" }
|
|
26
|
-
| { kind: "edit" }
|
|
27
6
|
| { kind: "pause" }
|
|
28
7
|
| { kind: "resume" }
|
|
8
|
+
| { kind: "clear" }
|
|
29
9
|
| { kind: "invalid"; message: string };
|
|
30
10
|
|
|
31
|
-
const RESERVED_GOAL_WORDS = ["start", "check", "checks", "limit", "history", "clear", "edit", "pause", "resume"] as const;
|
|
32
|
-
|
|
33
|
-
function parseBoundedInteger(value: string, maximum: number): number | undefined {
|
|
34
|
-
if (!/^[1-9][0-9]*$/u.test(value)) return undefined;
|
|
35
|
-
const parsed = Number(value);
|
|
36
|
-
return Number.isSafeInteger(parsed) && parsed <= maximum ? parsed : undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseControlledGoalStart(input: string): ControlledGoalStart | undefined {
|
|
40
|
-
const separator = input.startsWith("-- ") ? 0 : input.indexOf(" -- ");
|
|
41
|
-
if (separator < 0) return undefined;
|
|
42
|
-
const optionText = input.slice(0, separator).trim();
|
|
43
|
-
const objective = validateGoalObjective(input.slice(separator + (separator === 0 ? 3 : 4)));
|
|
44
|
-
if (!objective) return undefined;
|
|
45
|
-
const tokens = optionText ? optionText.split(/\s+/u) : [];
|
|
46
|
-
if (tokens.filter((token) => token === "--check" || token.startsWith("--check=")).length > 1
|
|
47
|
-
|| tokens.filter((token) => token === "--turns" || token.startsWith("--turns=")).length > 1) return undefined;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = parseArgs({
|
|
50
|
-
args: tokens,
|
|
51
|
-
options: { check: { type: "string" }, turns: { type: "string" } },
|
|
52
|
-
strict: true,
|
|
53
|
-
allowPositionals: false,
|
|
54
|
-
});
|
|
55
|
-
const check = parsed.values.check;
|
|
56
|
-
const turns = parsed.values.turns;
|
|
57
|
-
if (check !== undefined && !GOAL_CHECK_NAME_PATTERN.test(check)) return undefined;
|
|
58
|
-
const maxTurns = turns === undefined ? undefined : parseBoundedInteger(turns, GOAL_MAX_TURNS);
|
|
59
|
-
if (turns !== undefined && maxTurns === undefined) return undefined;
|
|
60
|
-
return {
|
|
61
|
-
objective,
|
|
62
|
-
...(check === undefined ? {} : { completionCheckName: check }),
|
|
63
|
-
...(maxTurns === undefined ? {} : { maxTurns }),
|
|
64
|
-
};
|
|
65
|
-
} catch {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
11
|
/** Parses one raw /goal argument string into a closed command variant. */
|
|
71
12
|
export function parseGoalCommand(args: string): GoalCommand {
|
|
72
13
|
const input = args.trim();
|
|
73
14
|
if (!input) return { kind: "status" };
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (firstWord === "start") {
|
|
85
|
-
const start = parseControlledGoalStart(input.slice(rawFirstWord.length).trimStart());
|
|
86
|
-
if (start) return { kind: "start", ...start };
|
|
87
|
-
return { kind: "invalid", message: rawFirstWord === firstWord ? GOAL_START_USAGE : RESERVED_OBJECTIVE_USAGE };
|
|
88
|
-
}
|
|
89
|
-
if (firstWord === "check") {
|
|
90
|
-
const value = parts[1];
|
|
91
|
-
if (parts.length === 2 && value?.toLowerCase() === "clear") return { kind: "check", value: { kind: "clear" } };
|
|
92
|
-
if (parts.length === 2 && value && GOAL_CHECK_NAME_PATTERN.test(value)) return { kind: "check", value: { kind: "named", name: value } };
|
|
93
|
-
return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_CHECK_USAGE };
|
|
94
|
-
}
|
|
95
|
-
if (firstWord === "checks") {
|
|
96
|
-
return parts.length === 1
|
|
97
|
-
? { kind: "checks" }
|
|
98
|
-
: { kind: "invalid", message: rawFirstWord === firstWord ? GOAL_CHECKS_USAGE : RESERVED_OBJECTIVE_USAGE };
|
|
99
|
-
}
|
|
100
|
-
if (firstWord === "limit") {
|
|
101
|
-
const value = parts[1];
|
|
102
|
-
if (parts.length === 2 && value?.toLowerCase() === "clear") return { kind: "limit", value: { kind: "clear" } };
|
|
103
|
-
const count = parts.length === 2 && value ? parseBoundedInteger(value, GOAL_MAX_TURNS) : undefined;
|
|
104
|
-
if (count !== undefined) return { kind: "limit", value: { kind: "count", count } };
|
|
105
|
-
return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_LIMIT_USAGE };
|
|
106
|
-
}
|
|
107
|
-
if (firstWord === "history") {
|
|
108
|
-
if (parts.length === 1) return { kind: "history", count: 20 };
|
|
109
|
-
const count = parts.length === 2 && parts[1] ? parseBoundedInteger(parts[1], 50) : undefined;
|
|
110
|
-
if (count !== undefined) return { kind: "history", count };
|
|
111
|
-
return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_HISTORY_USAGE };
|
|
112
|
-
}
|
|
113
|
-
if (parts.length === 1) return { kind: firstWord };
|
|
114
|
-
return {
|
|
115
|
-
kind: "invalid",
|
|
116
|
-
message: rawFirstWord === firstWord
|
|
117
|
-
? `Usage: /goal ${firstWord}`
|
|
118
|
-
: RESERVED_OBJECTIVE_USAGE,
|
|
119
|
-
};
|
|
15
|
+
const lowered = input.toLowerCase();
|
|
16
|
+
if (lowered === "pause") return { kind: "pause" };
|
|
17
|
+
if (lowered === "resume") return { kind: "resume" };
|
|
18
|
+
if (lowered === "clear") return { kind: "clear" };
|
|
19
|
+
const objective = validateGoalObjective(input);
|
|
20
|
+
return objective
|
|
21
|
+
? { kind: "objective", objective }
|
|
22
|
+
: { kind: "invalid", message: "A goal objective may not exceed 4,000 characters" };
|
|
120
23
|
}
|
|
@@ -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,
|
package/killeros/hooks.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import {
|
|
3
2
|
spawn,
|
|
4
3
|
type SpawnOptionsWithStdioTuple,
|
|
@@ -9,8 +8,6 @@ import path from "node:path";
|
|
|
9
8
|
import { StringDecoder } from "node:string_decoder";
|
|
10
9
|
import { CONFIG_DIR_NAME, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
10
|
import { errorMessage, reportError } from "./errors.ts";
|
|
12
|
-
import { GOAL_CHECK_NAME_PATTERN } from "./goal-state.ts";
|
|
13
|
-
import type { GoalCompletionCheck } from "./runtime.ts";
|
|
14
11
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
15
12
|
|
|
16
13
|
type KillerosHookEvent = "tool_call" | "tool_result" | "agent_settled";
|
|
@@ -21,14 +18,8 @@ interface KillerosHook {
|
|
|
21
18
|
timeoutMs?: number;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
interface KillerosGoalCheck {
|
|
25
|
-
command: string;
|
|
26
|
-
timeoutMs?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
21
|
interface KillerosHookConfig {
|
|
30
22
|
hooks?: Partial<Record<KillerosHookEvent, KillerosHook[]>>;
|
|
31
|
-
goalChecks?: Record<string, KillerosGoalCheck>;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
interface HookExecutionResult {
|
|
@@ -70,8 +61,6 @@ const HOOK_OUTPUT_LIMIT = 16 * 1024;
|
|
|
70
61
|
const HOOK_PAYLOAD_LIMIT = 8_000;
|
|
71
62
|
const HOOK_TIMEOUT_DEFAULT_MS = 30_000;
|
|
72
63
|
const HOOK_TIMEOUT_MAX_MS = 300_000;
|
|
73
|
-
const GOAL_CHECK_LIMIT = 32;
|
|
74
|
-
const GOAL_CHECK_COMMAND_LIMIT = 8_000;
|
|
75
64
|
|
|
76
65
|
// Reads executable project configuration through a bounded, project-local file descriptor.
|
|
77
66
|
function readHookConfig(configPath: string, projectRoot: string): string {
|
|
@@ -122,12 +111,11 @@ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
|
122
111
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
123
112
|
}
|
|
124
113
|
|
|
125
|
-
function loadKillerosConfig(ctx: ExtensionContext
|
|
114
|
+
function loadKillerosConfig(ctx: ExtensionContext): KillerosHookConfig {
|
|
126
115
|
const configPath = path.join(ctx.cwd, CONFIG_DIR_NAME, "killeros-hooks.json");
|
|
127
116
|
if (!existsSync(configPath)) return {};
|
|
128
117
|
const displayPath = safeTerminalText(configPath).replaceAll("\n", "");
|
|
129
118
|
if (!ctx.isProjectTrusted()) {
|
|
130
|
-
if (strictGoalChecks) throw new Error("Goal completion checks require a trusted project");
|
|
131
119
|
ctx.ui.notify(`Ignored untrusted project hooks in ${displayPath}`, "warning");
|
|
132
120
|
return {};
|
|
133
121
|
}
|
|
@@ -181,34 +169,8 @@ function loadKillerosConfig(ctx: ExtensionContext, strictGoalChecks = false): Ki
|
|
|
181
169
|
hooks[event] = accepted;
|
|
182
170
|
}
|
|
183
171
|
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const candidates = parsed.goalChecks;
|
|
187
|
-
if (candidates !== undefined) {
|
|
188
|
-
if (!isUnknownRecord(candidates)) throw new Error("goalChecks must contain a JSON object");
|
|
189
|
-
const entries = Object.entries(candidates);
|
|
190
|
-
if (entries.length > GOAL_CHECK_LIMIT) throw new Error(`goalChecks may contain at most ${GOAL_CHECK_LIMIT} checks`);
|
|
191
|
-
goalChecks = {};
|
|
192
|
-
for (const [name, candidate] of entries) {
|
|
193
|
-
if (!GOAL_CHECK_NAME_PATTERN.test(name)) throw new Error(`Invalid goal check name: ${JSON.stringify(name)}`);
|
|
194
|
-
if (!isUnknownRecord(candidate)) throw new Error(`Goal check ${name} must contain a JSON object`);
|
|
195
|
-
const { command, timeoutMs } = candidate;
|
|
196
|
-
if (typeof command !== "string" || command.trim().length < 1 || command.trim().length > GOAL_CHECK_COMMAND_LIMIT) {
|
|
197
|
-
throw new Error(`Goal check ${name} command must contain 1 to ${GOAL_CHECK_COMMAND_LIMIT} characters`);
|
|
198
|
-
}
|
|
199
|
-
if (timeoutMs !== undefined && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > HOOK_TIMEOUT_MAX_MS)) {
|
|
200
|
-
throw new Error(`Goal check ${name} timeoutMs must be an integer from 1 to ${HOOK_TIMEOUT_MAX_MS}`);
|
|
201
|
-
}
|
|
202
|
-
goalChecks[name] = { command: command.trim(), ...(timeoutMs === undefined ? {} : { timeoutMs }) };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} catch (error) {
|
|
206
|
-
if (strictGoalChecks) throw error;
|
|
207
|
-
reportError(ctx, `Invalid ${CONFIG_DIR_NAME}/killeros-hooks.json goalChecks`, error);
|
|
208
|
-
}
|
|
209
|
-
return { hooks, ...(goalChecks === undefined ? {} : { goalChecks }) };
|
|
172
|
+
return { hooks };
|
|
210
173
|
} catch (error) {
|
|
211
|
-
if (strictGoalChecks) throw error;
|
|
212
174
|
reportError(ctx, `Invalid ${CONFIG_DIR_NAME}/killeros-hooks.json`, error);
|
|
213
175
|
return {};
|
|
214
176
|
}
|
|
@@ -427,48 +389,6 @@ function hookFailureMessage(result: HookExecutionResult): string {
|
|
|
427
389
|
return safeTerminalText(`Hook failed${result.timedOut ? " (timed out)" : ""}${result.exitUnconfirmed ? " (process exit unconfirmed)" : ""}\n${detail}`);
|
|
428
390
|
}
|
|
429
391
|
|
|
430
|
-
function goalCheckHash(check: KillerosGoalCheck): string {
|
|
431
|
-
return createHash("sha256")
|
|
432
|
-
.update(JSON.stringify({ command: check.command, timeoutMs: check.timeoutMs ?? HOOK_TIMEOUT_DEFAULT_MS }))
|
|
433
|
-
.digest("hex");
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** Lists validated completion-check names without exposing their definitions. */
|
|
437
|
-
export function listGoalCompletionChecks(ctx: ExtensionContext): readonly string[] {
|
|
438
|
-
if (!ctx.isProjectTrusted()) throw new Error("Goal completion checks require a trusted project");
|
|
439
|
-
return Object.keys(loadKillerosConfig(ctx, true).goalChecks ?? {}).sort();
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export function resolveGoalCompletionCheck(ctx: ExtensionContext, name: string): GoalCompletionCheck {
|
|
443
|
-
if (!GOAL_CHECK_NAME_PATTERN.test(name)) throw new Error("Invalid goal completion check name");
|
|
444
|
-
if (!ctx.isProjectTrusted()) throw new Error("Goal completion checks require a trusted project");
|
|
445
|
-
const check = loadKillerosConfig(ctx, true).goalChecks?.[name];
|
|
446
|
-
if (!check) throw new Error(`Unknown goal completion check: ${safeTerminalText(name)}`);
|
|
447
|
-
return { kind: "named-command", name, configHash: goalCheckHash(check) };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export async function runGoalCompletionCheck(
|
|
451
|
-
ctx: ExtensionContext,
|
|
452
|
-
bound: GoalCompletionCheck,
|
|
453
|
-
signal?: AbortSignal,
|
|
454
|
-
): Promise<void> {
|
|
455
|
-
if (!ctx.isProjectTrusted()) throw new Error("Goal completion checks require a trusted project");
|
|
456
|
-
const check = loadKillerosConfig(ctx, true).goalChecks?.[bound.name];
|
|
457
|
-
if (!check) throw new Error(`Unknown goal completion check: ${safeTerminalText(bound.name)}`);
|
|
458
|
-
if (goalCheckHash(check) !== bound.configHash) {
|
|
459
|
-
throw new Error(`Goal completion check ${safeTerminalText(bound.name)} changed; run /goal check ${safeTerminalText(bound.name)} to approve it`);
|
|
460
|
-
}
|
|
461
|
-
const result = await executeHook({
|
|
462
|
-
command: check.command,
|
|
463
|
-
cwd: ctx.cwd,
|
|
464
|
-
environment: { KILLEROS_EVENT: "goal_check", KILLEROS_GOAL_CHECK: bound.name },
|
|
465
|
-
timeoutMs: check.timeoutMs,
|
|
466
|
-
signal,
|
|
467
|
-
});
|
|
468
|
-
if (result.cancelled) throw new Error(`Goal completion check ${safeTerminalText(bound.name)} was cancelled`);
|
|
469
|
-
if (result.code !== 0) throw new Error(hookFailureMessage(result).replace(/^Hook failed/u, `Goal completion check ${safeTerminalText(bound.name)} failed`));
|
|
470
|
-
}
|
|
471
|
-
|
|
472
392
|
export function registerLifecycleHooks(pi: ExtensionAPI): void {
|
|
473
393
|
let config: KillerosHookConfig = {};
|
|
474
394
|
pi.on("session_start", (_event, ctx) => { config = loadKillerosConfig(ctx); });
|
package/killeros/runtime.ts
CHANGED
|
@@ -39,12 +39,6 @@ export interface GoalFileVerification {
|
|
|
39
39
|
baseline: GoalFileBaseline;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export interface GoalCompletionCheck {
|
|
43
|
-
kind: "named-command";
|
|
44
|
-
name: string;
|
|
45
|
-
configHash: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
42
|
export interface GoalStateCommon {
|
|
49
43
|
version: 1;
|
|
50
44
|
revision: number;
|
|
@@ -56,7 +50,6 @@ export interface GoalStateCommon {
|
|
|
56
50
|
blockedAuditStartTurn: number;
|
|
57
51
|
baselineTokens: number;
|
|
58
52
|
verification?: GoalFileVerification;
|
|
59
|
-
completionCheck?: GoalCompletionCheck;
|
|
60
53
|
maxTurns?: number;
|
|
61
54
|
}
|
|
62
55
|
|
package/killeros/worked-for.ts
CHANGED
|
@@ -348,7 +348,7 @@ export function registerWorkedFor(
|
|
|
348
348
|
const settled = active;
|
|
349
349
|
active = undefined;
|
|
350
350
|
const changes = await (await settled.collection).finish();
|
|
351
|
-
if (changes.state === "unavailable" && changes.reason !== "not-git" && !collectionNoticeShown) {
|
|
351
|
+
if (changes.state === "unavailable" && changes.reason !== "not-git" && changes.reason !== "timeout" && !collectionNoticeShown) {
|
|
352
352
|
collectionNoticeShown = true;
|
|
353
353
|
ctx.ui.notify(`Change receipt unavailable: ${changes.reason}`, "warning");
|
|
354
354
|
}
|
package/package.json
CHANGED
package/killeros/goal-history.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { formatTime, formatTokens } from "./display.ts";
|
|
2
|
-
import { parseGoalState } from "./goal-state.ts";
|
|
3
|
-
import type { GoalState } from "./runtime.ts";
|
|
4
|
-
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
5
|
-
|
|
6
|
-
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
7
|
-
const VALID_GOAL_EVENTS: ReadonlySet<string> = new Set([
|
|
8
|
-
"set", "replace", "edit", "check", "limit", "turn", "pause", "resume",
|
|
9
|
-
"blocker-audit", "blocked", "complete", "error", "clear", "checkpoint",
|
|
10
|
-
]);
|
|
11
|
-
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function tokenUsage(entry: Record<string, unknown>): number | undefined {
|
|
16
|
-
if (entry.type === "message" && isUnknownRecord(entry.message)
|
|
17
|
-
&& (entry.message.role === "assistant" || entry.message.role === "toolResult")
|
|
18
|
-
&& isUnknownRecord(entry.message.usage)
|
|
19
|
-
&& typeof entry.message.usage.totalTokens === "number"
|
|
20
|
-
&& Number.isFinite(entry.message.usage.totalTokens)
|
|
21
|
-
&& entry.message.usage.totalTokens >= 0) {
|
|
22
|
-
return entry.message.usage.totalTokens;
|
|
23
|
-
}
|
|
24
|
-
if ((entry.type === "compaction" || entry.type === "branch_summary")
|
|
25
|
-
&& isUnknownRecord(entry.usage)
|
|
26
|
-
&& typeof entry.usage.totalTokens === "number"
|
|
27
|
-
&& Number.isFinite(entry.usage.totalTokens)
|
|
28
|
-
&& entry.usage.totalTokens >= 0) {
|
|
29
|
-
return entry.usage.totalTokens;
|
|
30
|
-
}
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function preview(value: string): string {
|
|
35
|
-
const safe = safeTerminalText(value).replaceAll("\n", " ").trim();
|
|
36
|
-
const characters = [...safe];
|
|
37
|
-
return characters.length <= 160 ? safe : `${characters.slice(0, 159).join("")}…`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function eventDetail(event: string, state: GoalState): string {
|
|
41
|
-
if (event === "check") return state.completionCheck ? `check ${state.completionCheck.name}` : "check cleared";
|
|
42
|
-
if (event === "limit") return state.maxTurns === undefined ? "limit cleared" : `limit ${state.maxTurns}`;
|
|
43
|
-
if (event === "blocker-audit" && state.blockerAudit) {
|
|
44
|
-
return `Blocker ${state.blockerAudit.streak}/3: ${state.blockerAudit.evidence ?? state.blockerAudit.key}`;
|
|
45
|
-
}
|
|
46
|
-
return state.result || state.objective;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Projects branch entries into the latest bounded goal-history rows. */
|
|
50
|
-
export function formatGoalHistory(entries: readonly unknown[], count: number): string | undefined {
|
|
51
|
-
const lines: string[] = [];
|
|
52
|
-
let tokens = 0;
|
|
53
|
-
let previousState: GoalState | undefined;
|
|
54
|
-
for (const value of entries) {
|
|
55
|
-
if (!isUnknownRecord(value)) continue;
|
|
56
|
-
const usage = tokenUsage(value);
|
|
57
|
-
if (usage !== undefined) {
|
|
58
|
-
tokens += usage;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (value.type !== "custom" || value.customType !== GOAL_ENTRY_TYPE || !isUnknownRecord(value.data)) continue;
|
|
62
|
-
const event = value.data.event;
|
|
63
|
-
if (typeof event !== "string" || !VALID_GOAL_EVENTS.has(event)) continue;
|
|
64
|
-
const state = value.data.state === null ? previousState : parseGoalState(value.data.state);
|
|
65
|
-
if (!state) continue;
|
|
66
|
-
if (value.data.state !== null) previousState = state;
|
|
67
|
-
if (event === "turn" || event === "checkpoint") continue;
|
|
68
|
-
lines.push(`+${formatTime(Math.max(0, state.updatedAt - state.createdAt))} ${event} turn ${state.turns} ${formatTokens(Math.max(0, tokens - state.baselineTokens))} tokens ${preview(eventDetail(event, state))}`);
|
|
69
|
-
}
|
|
70
|
-
return lines.length ? lines.slice(-count).join("\n") : undefined;
|
|
71
|
-
}
|