killeros 2.0.21 → 2.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/Killeros.ts +5 -2
- package/README.md +25 -3
- package/killeros/auto-compaction.ts +56 -15
- package/killeros/change-receipt.ts +727 -0
- package/killeros/commands.ts +1 -1
- package/killeros/footer.ts +157 -32
- package/killeros/goal-command.ts +120 -0
- package/killeros/goal-history.ts +71 -0
- package/killeros/goal-interface.ts +599 -0
- package/killeros/goal-runtime.ts +349 -0
- package/killeros/goal-settlement.ts +261 -0
- package/killeros/goal-state.ts +95 -13
- package/killeros/hooks.ts +144 -20
- package/killeros/personal-instructions.ts +5 -0
- package/killeros/runtime.ts +9 -0
- package/killeros/worked-for.ts +271 -61
- package/package.json +3 -2
- package/killeros/goals.ts +0 -1022
package/killeros/worked-for.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, Theme, ToolResultEvent } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { StopReason } from "@earendil-works/pi-ai";
|
|
3
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
5
|
+
import {
|
|
6
|
+
beginChangeReceipt,
|
|
7
|
+
disposeChangeReceipts,
|
|
8
|
+
recognizedCheck,
|
|
9
|
+
CHECK_LABELS,
|
|
10
|
+
type ChangeReceiptCollection,
|
|
11
|
+
type ChangeSummary,
|
|
12
|
+
type ChangedFile,
|
|
13
|
+
type CheckAttempt,
|
|
14
|
+
} from "./change-receipt.ts";
|
|
4
15
|
import { formatTokens } from "./display.ts";
|
|
5
16
|
import { errorMessage } from "./errors.ts";
|
|
17
|
+
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
6
18
|
|
|
7
19
|
const WORKED_FOR_ENTRY_TYPE = "killeros-worked-for";
|
|
20
|
+
const MAX_PAYLOAD_BYTES = 64 * 1024;
|
|
8
21
|
|
|
9
22
|
interface WorkedForEntryDataV1 {
|
|
10
23
|
version: 1;
|
|
@@ -26,7 +39,17 @@ interface WorkedForEntryDataV3 {
|
|
|
26
39
|
tokens: number;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
export interface WorkedForEntryDataV4 {
|
|
43
|
+
version: 4;
|
|
44
|
+
milliseconds: number;
|
|
45
|
+
outcome: WorkedForOutcome;
|
|
46
|
+
tokens: number;
|
|
47
|
+
changes: ChangeSummary;
|
|
48
|
+
checks: CheckAttempt[];
|
|
49
|
+
omittedChecks: { passed: number; failed: number };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2 | WorkedForEntryDataV3 | WorkedForEntryDataV4;
|
|
30
53
|
|
|
31
54
|
const OUTCOMES = {
|
|
32
55
|
done: { marker: "✓", label: "Done", color: "success" },
|
|
@@ -38,6 +61,95 @@ function isWorkedForOutcome(value: unknown): value is WorkedForOutcome {
|
|
|
38
61
|
return value === "done" || value === "stopped" || value === "failed";
|
|
39
62
|
}
|
|
40
63
|
|
|
64
|
+
function integer(value: unknown): value is number {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
69
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validPath(value: unknown): value is string {
|
|
73
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) return false;
|
|
74
|
+
if (path.posix.isAbsolute(value) || path.win32.isAbsolute(value)) return false;
|
|
75
|
+
return !value.split(/[\\/]/u).includes("..");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseFile(value: unknown): ChangedFile | undefined {
|
|
79
|
+
if (!record(value) || !validPath(value.path) || !integer(value.additions) || !integer(value.deletions)) return undefined;
|
|
80
|
+
if (value.detail !== undefined && value.detail !== "binary" && value.detail !== "mode") return undefined;
|
|
81
|
+
const detail: "binary" | "mode" | undefined = value.detail === "binary" || value.detail === "mode" ? value.detail : undefined;
|
|
82
|
+
if (detail && (value.additions !== 0 || value.deletions !== 0)) return undefined;
|
|
83
|
+
const shared = {
|
|
84
|
+
path: value.path,
|
|
85
|
+
additions: value.additions,
|
|
86
|
+
deletions: value.deletions,
|
|
87
|
+
...(detail ? { detail } : {}),
|
|
88
|
+
};
|
|
89
|
+
if (value.kind === "renamed") {
|
|
90
|
+
if (!validPath(value.previousPath)) return undefined;
|
|
91
|
+
return { kind: "renamed", previousPath: value.previousPath, ...shared };
|
|
92
|
+
}
|
|
93
|
+
if (value.previousPath !== undefined || value.kind !== "added" && value.kind !== "modified" && value.kind !== "deleted") return undefined;
|
|
94
|
+
return { kind: value.kind, ...shared };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseChanges(value: unknown): ChangeSummary | undefined {
|
|
98
|
+
if (!record(value)) return undefined;
|
|
99
|
+
if (value.state === "unavailable") {
|
|
100
|
+
return value.reason === "not-git" || value.reason === "timeout" || value.reason === "too-large" || value.reason === "error"
|
|
101
|
+
? { state: "unavailable", reason: value.reason }
|
|
102
|
+
: undefined;
|
|
103
|
+
}
|
|
104
|
+
if (value.state !== "available" || !integer(value.totalFiles) || !integer(value.additions) || !integer(value.deletions)
|
|
105
|
+
|| !integer(value.omittedFiles) || !Array.isArray(value.files) || value.files.length > 20) return undefined;
|
|
106
|
+
const files = value.files.map(parseFile);
|
|
107
|
+
if (files.some((file) => !file)) return undefined;
|
|
108
|
+
const parsed = files as ChangedFile[];
|
|
109
|
+
if (value.totalFiles !== parsed.length + value.omittedFiles) return undefined;
|
|
110
|
+
if (value.omittedFiles === 0) {
|
|
111
|
+
if (value.additions !== parsed.reduce((total, file) => total + file.additions, 0)
|
|
112
|
+
|| value.deletions !== parsed.reduce((total, file) => total + file.deletions, 0)) return undefined;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
state: "available",
|
|
116
|
+
totalFiles: value.totalFiles,
|
|
117
|
+
additions: value.additions,
|
|
118
|
+
deletions: value.deletions,
|
|
119
|
+
files: parsed,
|
|
120
|
+
omittedFiles: value.omittedFiles,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseV4(data: Record<string, unknown>): WorkedForEntryDataV4 | undefined {
|
|
125
|
+
try {
|
|
126
|
+
if (Buffer.byteLength(JSON.stringify(data), "utf8") > MAX_PAYLOAD_BYTES) return undefined;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (!integer(data.milliseconds) || !integer(data.tokens) || !isWorkedForOutcome(data.outcome)) return undefined;
|
|
131
|
+
const changes = parseChanges(data.changes);
|
|
132
|
+
if (!changes || !Array.isArray(data.checks) || data.checks.length > 20 || !record(data.omittedChecks)
|
|
133
|
+
|| !integer(data.omittedChecks.passed) || !integer(data.omittedChecks.failed)) return undefined;
|
|
134
|
+
if (data.omittedChecks.passed + data.omittedChecks.failed > 0 && data.checks.length !== 20) return undefined;
|
|
135
|
+
const checks: CheckAttempt[] = [];
|
|
136
|
+
for (const check of data.checks) {
|
|
137
|
+
if (!record(check) || check.outcome !== "passed" && check.outcome !== "failed") return undefined;
|
|
138
|
+
const label = CHECK_LABELS.find((candidate) => candidate === check.label);
|
|
139
|
+
if (!label) return undefined;
|
|
140
|
+
checks.push({ label, outcome: check.outcome });
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
version: 4,
|
|
144
|
+
milliseconds: data.milliseconds,
|
|
145
|
+
outcome: data.outcome,
|
|
146
|
+
tokens: data.tokens,
|
|
147
|
+
changes,
|
|
148
|
+
checks,
|
|
149
|
+
omittedChecks: { passed: data.omittedChecks.passed, failed: data.omittedChecks.failed },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
41
153
|
function sessionTokenTotal(ctx: ExtensionContext): number | undefined {
|
|
42
154
|
try {
|
|
43
155
|
let total = 0;
|
|
@@ -59,110 +171,208 @@ export function formatWorkedForDuration(milliseconds: number): string {
|
|
|
59
171
|
const boundedMilliseconds = Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : 0;
|
|
60
172
|
const totalSeconds = Math.max(1, Math.floor(boundedMilliseconds / 1_000));
|
|
61
173
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
62
|
-
|
|
63
174
|
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
64
|
-
if (totalMinutes < 60) {
|
|
65
|
-
return `${totalMinutes}m ${(totalSeconds % 60).toString().padStart(2, "0")}s`;
|
|
66
|
-
}
|
|
67
|
-
|
|
175
|
+
if (totalMinutes < 60) return `${totalMinutes}m ${(totalSeconds % 60).toString().padStart(2, "0")}s`;
|
|
68
176
|
return `${Math.floor(totalMinutes / 60)}h ${(totalMinutes % 60).toString().padStart(2, "0")}m`;
|
|
69
177
|
}
|
|
70
178
|
|
|
71
179
|
function parseWorkedForEntryData(data: unknown): WorkedForEntryData | undefined {
|
|
72
|
-
if (!data ||
|
|
73
|
-
if (
|
|
74
|
-
if (typeof data.milliseconds !== "number" || !Number.isFinite(data.milliseconds) || data.milliseconds < 0)
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
180
|
+
if (!record(data) || !("version" in data) || !("milliseconds" in data)) return undefined;
|
|
181
|
+
if (data.version === 4) return parseV4(data);
|
|
182
|
+
if (typeof data.milliseconds !== "number" || !Number.isFinite(data.milliseconds) || data.milliseconds < 0) return undefined;
|
|
77
183
|
if (data.version === 1) return { version: 1, milliseconds: data.milliseconds };
|
|
78
184
|
if (!("outcome" in data) || !isWorkedForOutcome(data.outcome)) return undefined;
|
|
79
185
|
if (data.version === 2) return { version: 2, milliseconds: data.milliseconds, outcome: data.outcome };
|
|
80
|
-
if (data.version !== 3 || !(
|
|
81
|
-
|| typeof data.tokens !== "number" || !Number.isFinite(data.tokens) || data.tokens < 0) return undefined;
|
|
186
|
+
if (data.version !== 3 || typeof data.tokens !== "number" || !Number.isFinite(data.tokens) || data.tokens < 0) return undefined;
|
|
82
187
|
return { version: 3, milliseconds: data.milliseconds, outcome: data.outcome, tokens: data.tokens };
|
|
83
188
|
}
|
|
84
189
|
|
|
190
|
+
function safePath(value: string): string {
|
|
191
|
+
return safeTerminalText(value).replaceAll("\n", "⏎");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class WorkedForV4Component implements Component {
|
|
195
|
+
private readonly data: WorkedForEntryDataV4;
|
|
196
|
+
private readonly expanded: boolean;
|
|
197
|
+
private readonly theme: Theme;
|
|
198
|
+
|
|
199
|
+
constructor(
|
|
200
|
+
data: WorkedForEntryDataV4,
|
|
201
|
+
expanded: boolean,
|
|
202
|
+
theme: Theme,
|
|
203
|
+
) {
|
|
204
|
+
this.data = data;
|
|
205
|
+
this.expanded = expanded;
|
|
206
|
+
this.theme = theme;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
render(width: number): string[] {
|
|
210
|
+
if (width <= 0) return [];
|
|
211
|
+
const { data, theme } = this;
|
|
212
|
+
const outcome = OUTCOMES[data.outcome];
|
|
213
|
+
const lines = [
|
|
214
|
+
`${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)} · ↑ ${formatTokens(data.tokens)} tokens`)}`,
|
|
215
|
+
];
|
|
216
|
+
if (data.changes.state === "unavailable") lines.push(theme.fg("dim", " Changes unavailable"));
|
|
217
|
+
else if (data.changes.totalFiles === 0) lines.push(theme.fg("dim", " No files changed"));
|
|
218
|
+
else {
|
|
219
|
+
const count = `${data.changes.totalFiles} ${data.changes.totalFiles === 1 ? "file" : "files"}`;
|
|
220
|
+
lines.push(`${theme.fg("accent", ` ${width < 40 ? count : `Changed ${count}`}`)}${theme.fg("dim", " · ")}${theme.fg("success", `+${data.changes.additions}`)} ${theme.fg("error", `−${data.changes.deletions}`)}`);
|
|
221
|
+
}
|
|
222
|
+
const passed = data.checks.filter((check) => check.outcome === "passed").length + data.omittedChecks.passed;
|
|
223
|
+
const failed = data.checks.filter((check) => check.outcome === "failed").length + data.omittedChecks.failed;
|
|
224
|
+
const totalChecks = passed + failed;
|
|
225
|
+
if (totalChecks === 0) {
|
|
226
|
+
if (data.changes.state === "available" && data.changes.totalFiles > 0) lines.push(theme.fg("warning", " No check recorded"));
|
|
227
|
+
} else if (totalChecks === 1) {
|
|
228
|
+
const check = data.checks[0];
|
|
229
|
+
if (check?.outcome === "passed") lines.push(theme.fg("success", ` Check passed: ${check.label} ✓`));
|
|
230
|
+
else if (check) lines.push(theme.fg("error", ` Check failed: ${check.label} ×`));
|
|
231
|
+
} else if (failed === 0) {
|
|
232
|
+
lines.push(theme.fg("success", ` Checks: ${passed} passed`));
|
|
233
|
+
} else {
|
|
234
|
+
lines.push(` ${theme.fg("accent", "Checks:")} ${theme.fg("success", `${passed} passed`)}${theme.fg("dim", " · ")}${theme.fg("error", `${failed} failed`)}`);
|
|
235
|
+
}
|
|
236
|
+
if (this.expanded && data.changes.state === "available") {
|
|
237
|
+
for (const file of data.changes.files) {
|
|
238
|
+
const marker = file.kind === "added" ? "A" : file.kind === "deleted" ? "D" : file.kind === "renamed" ? "R" : "M";
|
|
239
|
+
const label = file.kind === "renamed" ? `${safePath(file.previousPath)} → ${safePath(file.path)}` : safePath(file.path);
|
|
240
|
+
const prefix = ` ${marker} `;
|
|
241
|
+
const detail = file.detail ? ` ${file.detail}` : ` +${file.additions} −${file.deletions}`;
|
|
242
|
+
const labelWidth = width - visibleWidth(prefix) - visibleWidth(detail);
|
|
243
|
+
const fittedLabel = labelWidth > 0 ? truncateToWidth(label, labelWidth, "…") : "";
|
|
244
|
+
const styledDetail = file.detail
|
|
245
|
+
? theme.fg("dim", detail)
|
|
246
|
+
: `${theme.fg("success", ` +${file.additions}`)} ${theme.fg("error", `−${file.deletions}`)}`;
|
|
247
|
+
lines.push(`${theme.fg("accent", `${prefix}${fittedLabel}`)}${styledDetail}`);
|
|
248
|
+
}
|
|
249
|
+
if (data.changes.omittedFiles > 0) lines.push(theme.fg("dim", ` … ${data.changes.omittedFiles} more files`));
|
|
250
|
+
}
|
|
251
|
+
if (this.expanded) {
|
|
252
|
+
for (const check of data.checks) lines.push(theme.fg(check.outcome === "passed" ? "success" : "error", ` ${check.outcome === "passed" ? "✓" : "×"} ${check.label}`));
|
|
253
|
+
const omitted = data.omittedChecks.passed + data.omittedChecks.failed;
|
|
254
|
+
if (omitted > 0) lines.push(theme.fg("dim", ` … ${omitted} more checks`));
|
|
255
|
+
}
|
|
256
|
+
return lines.map((line) => truncateToWidth(line, width, "…"));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
invalidate(): void {}
|
|
260
|
+
}
|
|
261
|
+
|
|
85
262
|
export function workedForOutcome(stopReason: StopReason | undefined): WorkedForOutcome {
|
|
86
263
|
if (stopReason === "stop") return "done";
|
|
87
264
|
if (stopReason === "aborted") return "stopped";
|
|
88
265
|
return "failed";
|
|
89
266
|
}
|
|
90
267
|
|
|
268
|
+
type ActiveReceipt = {
|
|
269
|
+
startedAt: number;
|
|
270
|
+
startedTokens: number | undefined;
|
|
271
|
+
stopReason: StopReason | undefined;
|
|
272
|
+
collection: Promise<ChangeReceiptCollection>;
|
|
273
|
+
checks: CheckAttempt[];
|
|
274
|
+
omittedChecks: { passed: number; failed: number };
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
function fitPayload(data: WorkedForEntryDataV4): WorkedForEntryDataV4 {
|
|
278
|
+
if (data.changes.state === "unavailable") return data;
|
|
279
|
+
const changes = { ...data.changes, files: [...data.changes.files] };
|
|
280
|
+
const fitted = { ...data, changes };
|
|
281
|
+
while (changes.files.length > 0 && Buffer.byteLength(JSON.stringify(fitted), "utf8") > MAX_PAYLOAD_BYTES) {
|
|
282
|
+
changes.files.pop();
|
|
283
|
+
changes.omittedFiles += 1;
|
|
284
|
+
}
|
|
285
|
+
return fitted;
|
|
286
|
+
}
|
|
287
|
+
|
|
91
288
|
export function registerWorkedFor(
|
|
92
289
|
pi: ExtensionAPI,
|
|
93
290
|
now: () => number = Date.now,
|
|
291
|
+
collect: (cwd: string) => Promise<ChangeReceiptCollection> = beginChangeReceipt,
|
|
94
292
|
): void {
|
|
95
|
-
let
|
|
96
|
-
let
|
|
97
|
-
let stopReason: StopReason | undefined;
|
|
293
|
+
let active: ActiveReceipt | undefined;
|
|
294
|
+
let collectionNoticeShown = false;
|
|
98
295
|
|
|
99
|
-
pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry,
|
|
296
|
+
pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, options, theme) => {
|
|
100
297
|
const data = parseWorkedForEntryData(entry.data);
|
|
101
298
|
if (!data) return undefined;
|
|
102
|
-
if (data.version === 1) {
|
|
103
|
-
|
|
104
|
-
theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`),
|
|
105
|
-
0,
|
|
106
|
-
0,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
299
|
+
if (data.version === 1) return new Text(theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(data.milliseconds)}`), 0, 0);
|
|
300
|
+
if (data.version === 4) return new WorkedForV4Component(data, options.expanded, theme);
|
|
109
301
|
const outcome = OUTCOMES[data.outcome];
|
|
110
302
|
const tokens = data.version === 3 ? ` · ↑ ${formatTokens(data.tokens)} tokens` : "";
|
|
111
|
-
return new Text(
|
|
112
|
-
`${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`,
|
|
113
|
-
0,
|
|
114
|
-
0,
|
|
115
|
-
);
|
|
303
|
+
return new Text(`${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 0, 0);
|
|
116
304
|
});
|
|
117
305
|
|
|
118
|
-
pi.on("session_start", () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
306
|
+
pi.on("session_start", async () => {
|
|
307
|
+
const stale = active;
|
|
308
|
+
active = undefined;
|
|
309
|
+
collectionNoticeShown = false;
|
|
310
|
+
if (stale) await (await stale.collection).dispose();
|
|
122
311
|
});
|
|
123
312
|
|
|
124
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
125
|
-
if (ctx.mode !== "tui" ||
|
|
126
|
-
|
|
127
|
-
|
|
313
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
314
|
+
if (ctx.mode !== "tui" || active) return;
|
|
315
|
+
const state: ActiveReceipt = {
|
|
316
|
+
startedAt: now(),
|
|
317
|
+
startedTokens: sessionTokenTotal(ctx),
|
|
318
|
+
stopReason: undefined,
|
|
319
|
+
collection: collect(ctx.cwd),
|
|
320
|
+
checks: [],
|
|
321
|
+
omittedChecks: { passed: 0, failed: 0 },
|
|
322
|
+
};
|
|
323
|
+
active = state;
|
|
324
|
+
const collection = await state.collection;
|
|
325
|
+
if (active !== state) await collection.dispose();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
pi.on("tool_result", (event: ToolResultEvent, ctx) => {
|
|
329
|
+
if (ctx.mode !== "tui" || !active || event.toolName !== "bash" && event.toolName !== "powershell") return;
|
|
330
|
+
const check = recognizedCheck(event.input.command, event.isError);
|
|
331
|
+
if (!check) return;
|
|
332
|
+
if (active.checks.length < 20) active.checks.push(check);
|
|
333
|
+
else active.omittedChecks[check.outcome] += 1;
|
|
128
334
|
});
|
|
129
335
|
|
|
130
336
|
pi.on("agent_end", (event, ctx) => {
|
|
131
|
-
if (ctx.mode !== "tui" ||
|
|
337
|
+
if (ctx.mode !== "tui" || !active) return;
|
|
132
338
|
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
133
339
|
const message = event.messages[index];
|
|
134
340
|
if (message?.role !== "assistant") continue;
|
|
135
|
-
stopReason = message.stopReason;
|
|
341
|
+
active.stopReason = message.stopReason;
|
|
136
342
|
break;
|
|
137
343
|
}
|
|
138
344
|
});
|
|
139
345
|
|
|
140
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
141
|
-
if (ctx.mode !== "tui" ||
|
|
142
|
-
const
|
|
346
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
347
|
+
if (ctx.mode !== "tui" || !active) return;
|
|
348
|
+
const settled = active;
|
|
349
|
+
active = undefined;
|
|
350
|
+
const changes = await (await settled.collection).finish();
|
|
351
|
+
if (changes.state === "unavailable" && changes.reason !== "not-git" && !collectionNoticeShown) {
|
|
352
|
+
collectionNoticeShown = true;
|
|
353
|
+
ctx.ui.notify(`Change receipt unavailable: ${changes.reason}`, "warning");
|
|
354
|
+
}
|
|
143
355
|
const settledTokens = sessionTokenTotal(ctx);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
: Math.max(0,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
356
|
+
const data = fitPayload({
|
|
357
|
+
version: 4,
|
|
358
|
+
milliseconds: Math.max(0, now() - settled.startedAt),
|
|
359
|
+
outcome: workedForOutcome(settled.stopReason),
|
|
360
|
+
tokens: settled.startedTokens === undefined || settledTokens === undefined ? 0 : Math.max(0, settledTokens - settled.startedTokens),
|
|
361
|
+
changes,
|
|
362
|
+
checks: settled.checks,
|
|
363
|
+
omittedChecks: settled.omittedChecks,
|
|
364
|
+
});
|
|
151
365
|
try {
|
|
152
|
-
pi.appendEntry<
|
|
153
|
-
version: 3,
|
|
154
|
-
milliseconds,
|
|
155
|
-
outcome,
|
|
156
|
-
tokens,
|
|
157
|
-
});
|
|
366
|
+
pi.appendEntry<WorkedForEntryDataV4>(WORKED_FOR_ENTRY_TYPE, data);
|
|
158
367
|
} catch (error) {
|
|
159
368
|
ctx.ui.notify(`Worked-for timing could not be saved: ${errorMessage(error)}`, "error");
|
|
160
369
|
}
|
|
161
370
|
});
|
|
162
371
|
|
|
163
|
-
pi.on("session_shutdown", () => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
372
|
+
pi.on("session_shutdown", async () => {
|
|
373
|
+
const stale = active;
|
|
374
|
+
active = undefined;
|
|
375
|
+
if (stale) await (await stale.collection).dispose();
|
|
376
|
+
disposeChangeReceipts();
|
|
167
377
|
});
|
|
168
378
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.22",
|
|
4
4
|
"description": "TUI, goals, and workflow automation for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
"node": ">=22.19.0"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
+
"benchmark:change-receipt": "node --test --experimental-strip-types test/ChangeReceipt.bench.ts",
|
|
33
34
|
"check": "tsc --noEmit && eslint .",
|
|
34
35
|
"lint": "eslint .",
|
|
35
|
-
"test": "node --test --experimental-strip-types test/*.test.*"
|
|
36
|
+
"test": "node --test --test-force-exit --experimental-strip-types test/*.test.*"
|
|
36
37
|
},
|
|
37
38
|
"pi": {
|
|
38
39
|
"extensions": [
|