killeros 2.0.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 +35 -0
- package/Killeros.ts +5 -2
- package/README.md +22 -5
- package/killeros/auto-compaction.ts +56 -15
- package/killeros/change-receipt.ts +727 -0
- package/killeros/commands.ts +1 -1
- package/killeros/display.ts +9 -7
- package/killeros/footer.ts +112 -21
- package/killeros/goal-command.ts +23 -0
- package/killeros/goal-interface.ts +420 -0
- package/killeros/goal-runtime.ts +347 -0
- package/killeros/goal-settlement.ts +261 -0
- package/killeros/goal-state.ts +66 -38
- package/killeros/hooks.ts +23 -8
- package/killeros/runtime.ts +2 -0
- package/killeros/worked-for.ts +271 -61
- package/package.json +3 -2
- package/killeros/goals.ts +0 -1022
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/footer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { watch } from "node:fs";
|
|
2
3
|
import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
4
5
|
import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
@@ -9,24 +10,45 @@ import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
|
9
10
|
import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
|
|
10
11
|
|
|
11
12
|
const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
13
|
+
const GIT_STATUS_TIMEOUT_MS = 5_000;
|
|
14
|
+
const GIT_STATUS_WATCH_DEBOUNCE_MS = 250;
|
|
15
|
+
const GIT_STATUS_WATCH_INTERVAL_MS = 5_000;
|
|
12
16
|
const CODEX_PROVIDER = "openai-codex";
|
|
13
17
|
const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
|
|
14
18
|
|
|
15
|
-
interface GitFileChanges {
|
|
19
|
+
export interface GitFileChanges {
|
|
16
20
|
modified: number;
|
|
17
21
|
added: number;
|
|
18
22
|
deleted: number;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
type GitStatusExecutor = (
|
|
26
|
+
file: string,
|
|
27
|
+
args: string[],
|
|
28
|
+
options: {
|
|
29
|
+
encoding: "utf8";
|
|
30
|
+
env: NodeJS.ProcessEnv;
|
|
31
|
+
maxBuffer: number;
|
|
32
|
+
timeout: number;
|
|
33
|
+
windowsHide: true;
|
|
34
|
+
},
|
|
35
|
+
callback: (error: Error | null, stdout: string) => void,
|
|
36
|
+
) => unknown;
|
|
37
|
+
|
|
38
|
+
/** Resolves changed-file counts with a bounded asynchronous Git status process. */
|
|
39
|
+
export function resolveGitFileChanges(
|
|
40
|
+
cwd: string,
|
|
41
|
+
execute: GitStatusExecutor = execFile,
|
|
42
|
+
): Promise<GitFileChanges | undefined> {
|
|
22
43
|
return new Promise((resolve) => {
|
|
23
|
-
|
|
44
|
+
execute(
|
|
24
45
|
"git",
|
|
25
46
|
["-C", cwd, "status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
26
47
|
{
|
|
27
48
|
encoding: "utf8",
|
|
49
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
28
50
|
maxBuffer: 4 * 1024 * 1024,
|
|
29
|
-
timeout:
|
|
51
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
30
52
|
windowsHide: true,
|
|
31
53
|
},
|
|
32
54
|
(error, stdout) => {
|
|
@@ -72,9 +94,16 @@ function createGitRefresh<T>(
|
|
|
72
94
|
return;
|
|
73
95
|
}
|
|
74
96
|
pending = true;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
let result: Promise<T | undefined>;
|
|
98
|
+
try {
|
|
99
|
+
result = resolveResult(cwd);
|
|
100
|
+
} catch {
|
|
101
|
+
result = Promise.resolve(undefined);
|
|
102
|
+
}
|
|
103
|
+
void result.then(
|
|
104
|
+
(value) => { if (!disposed) onResult(value); },
|
|
105
|
+
() => { if (!disposed) onResult(undefined); },
|
|
106
|
+
).finally(() => {
|
|
78
107
|
pending = false;
|
|
79
108
|
if (!disposed && queued) {
|
|
80
109
|
queued = false;
|
|
@@ -100,14 +129,20 @@ export function createGitStatusRefresh(
|
|
|
100
129
|
return createGitRefresh(cwd, onCount, resolveCount);
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
/** Coalesces file-change scans and emits only successful results. */
|
|
133
|
+
export function createGitFileChangesRefresh(
|
|
104
134
|
cwd: string,
|
|
105
|
-
onChanges: (changes: GitFileChanges
|
|
135
|
+
onChanges: (changes: GitFileChanges) => void,
|
|
136
|
+
resolveChanges: (cwd: string) => Promise<GitFileChanges | undefined> = resolveGitFileChanges,
|
|
106
137
|
): { request: () => void; dispose: () => void } {
|
|
107
|
-
return createGitRefresh(cwd,
|
|
138
|
+
return createGitRefresh(cwd, (changes) => {
|
|
139
|
+
if (changes !== undefined) onChanges(changes);
|
|
140
|
+
}, resolveChanges);
|
|
108
141
|
}
|
|
109
142
|
|
|
110
143
|
type ScheduleFallback = (refresh: () => void, intervalMs: number) => () => void;
|
|
144
|
+
type WatchDirectory = (cwd: string, onChange: () => void, onError: () => void) => () => void;
|
|
145
|
+
type ScheduleDelay = (refresh: () => void, delayMs: number) => () => void;
|
|
111
146
|
|
|
112
147
|
const scheduleFallback: ScheduleFallback = (refresh, intervalMs) => {
|
|
113
148
|
const timer = setInterval(refresh, intervalMs);
|
|
@@ -115,6 +150,18 @@ const scheduleFallback: ScheduleFallback = (refresh, intervalMs) => {
|
|
|
115
150
|
return () => clearInterval(timer);
|
|
116
151
|
};
|
|
117
152
|
|
|
153
|
+
const watchDirectory: WatchDirectory = (cwd, onChange, onError) => {
|
|
154
|
+
const watcher = watch(cwd, { recursive: true }, onChange);
|
|
155
|
+
watcher.on("error", onError);
|
|
156
|
+
return () => watcher.close();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const scheduleDelay: ScheduleDelay = (refresh, delayMs) => {
|
|
160
|
+
const timer = setTimeout(refresh, delayMs);
|
|
161
|
+
timer.unref?.();
|
|
162
|
+
return () => clearTimeout(timer);
|
|
163
|
+
};
|
|
164
|
+
|
|
118
165
|
/** Schedules the fallback Git scan independently from footer rendering. */
|
|
119
166
|
export function scheduleGitStatusFallback(
|
|
120
167
|
refresh: () => void,
|
|
@@ -123,6 +170,55 @@ export function scheduleGitStatusFallback(
|
|
|
123
170
|
return schedule(refresh, GIT_STATUS_REFRESH_INTERVAL_MS);
|
|
124
171
|
}
|
|
125
172
|
|
|
173
|
+
/** Throttles filesystem changes and falls back silently when watching is unavailable. */
|
|
174
|
+
export function scheduleGitStatusWatch(
|
|
175
|
+
cwd: string,
|
|
176
|
+
refresh: () => void,
|
|
177
|
+
startWatching: WatchDirectory = watchDirectory,
|
|
178
|
+
schedule: ScheduleDelay = scheduleDelay,
|
|
179
|
+
): () => void {
|
|
180
|
+
let disposed = false;
|
|
181
|
+
let coolingDown = false;
|
|
182
|
+
let queued = false;
|
|
183
|
+
let stopTimer: (() => void) | undefined;
|
|
184
|
+
let stopWatching: (() => void) | undefined;
|
|
185
|
+
const stop = (): void => {
|
|
186
|
+
if (disposed) return;
|
|
187
|
+
disposed = true;
|
|
188
|
+
stopTimer?.();
|
|
189
|
+
stopTimer = undefined;
|
|
190
|
+
stopWatching?.();
|
|
191
|
+
stopWatching = undefined;
|
|
192
|
+
};
|
|
193
|
+
const changed = (): void => {
|
|
194
|
+
if (disposed) return;
|
|
195
|
+
if (stopTimer) {
|
|
196
|
+
if (coolingDown) queued = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
stopTimer = schedule(() => {
|
|
200
|
+
stopTimer = undefined;
|
|
201
|
+
if (disposed) return;
|
|
202
|
+
refresh();
|
|
203
|
+
coolingDown = true;
|
|
204
|
+
stopTimer = schedule(() => {
|
|
205
|
+
stopTimer = undefined;
|
|
206
|
+
coolingDown = false;
|
|
207
|
+
if (queued) {
|
|
208
|
+
queued = false;
|
|
209
|
+
changed();
|
|
210
|
+
}
|
|
211
|
+
}, GIT_STATUS_WATCH_INTERVAL_MS);
|
|
212
|
+
}, GIT_STATUS_WATCH_DEBOUNCE_MS);
|
|
213
|
+
};
|
|
214
|
+
try {
|
|
215
|
+
stopWatching = startWatching(cwd, changed, stop);
|
|
216
|
+
} catch {
|
|
217
|
+
disposed = true;
|
|
218
|
+
}
|
|
219
|
+
return stop;
|
|
220
|
+
}
|
|
221
|
+
|
|
126
222
|
export function formatCost(usd: number): string {
|
|
127
223
|
if (!Number.isFinite(usd)) return "$—";
|
|
128
224
|
return `$${usd.toFixed(2)}`;
|
|
@@ -258,7 +354,8 @@ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
|
|
|
258
354
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
259
355
|
if (!state) return "";
|
|
260
356
|
if (state.status === "active") {
|
|
261
|
-
|
|
357
|
+
const turns = state.maxTurns === undefined ? "" : ` ${state.turns}/${state.maxTurns}`;
|
|
358
|
+
return theme.fg("warning", `/goal is active${turns} (${formatTime(goalElapsedMilliseconds(state, Date.now()))})`);
|
|
262
359
|
}
|
|
263
360
|
if (state.status === "paused") return theme.fg("warning", "/goal is paused");
|
|
264
361
|
if (state.status === "blocked") return theme.fg("error", "/goal is blocked");
|
|
@@ -278,7 +375,6 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
278
375
|
let cachedSessionCost = 0;
|
|
279
376
|
let sessionCostDirty = true;
|
|
280
377
|
let unsubscribeCodexFast: (() => void) | undefined;
|
|
281
|
-
let requestGitStatusRefresh: (() => void) | undefined;
|
|
282
378
|
const resetSessionCost = (): void => {
|
|
283
379
|
cachedSessionCost = 0;
|
|
284
380
|
sessionCostDirty = true;
|
|
@@ -314,19 +410,19 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
314
410
|
gitFileChanges = changes;
|
|
315
411
|
tui.requestRender();
|
|
316
412
|
});
|
|
317
|
-
requestGitStatusRefresh = gitStatus.request;
|
|
318
413
|
const unsubscribe = footerData.onBranchChange(() => {
|
|
319
414
|
gitStatus.request();
|
|
320
415
|
tui.requestRender();
|
|
321
416
|
});
|
|
322
417
|
gitStatus.request();
|
|
418
|
+
const stopWatch = scheduleGitStatusWatch(ctx.cwd, gitStatus.request);
|
|
323
419
|
const stopFallback = scheduleGitStatusFallback(gitStatus.request);
|
|
324
420
|
return {
|
|
325
421
|
dispose() {
|
|
326
422
|
unsubscribe();
|
|
423
|
+
stopWatch();
|
|
327
424
|
stopFallback();
|
|
328
425
|
gitStatus.dispose();
|
|
329
|
-
if (requestGitStatusRefresh === gitStatus.request) requestGitStatusRefresh = undefined;
|
|
330
426
|
if (activeTui === tui) activeTui = undefined;
|
|
331
427
|
},
|
|
332
428
|
invalidate() {},
|
|
@@ -393,12 +489,8 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
393
489
|
thinkingLevel = event.level;
|
|
394
490
|
activeTui?.requestRender();
|
|
395
491
|
});
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
requestGitStatusRefresh?.();
|
|
399
|
-
};
|
|
400
|
-
pi.on("turn_end", refreshAfterActivity);
|
|
401
|
-
pi.on("session_compact", refreshAfterActivity);
|
|
492
|
+
pi.on("turn_end", invalidateSessionCost);
|
|
493
|
+
pi.on("session_compact", invalidateSessionCost);
|
|
402
494
|
pi.on("session_tree", () => {
|
|
403
495
|
resetSessionCost();
|
|
404
496
|
activeTui?.requestRender();
|
|
@@ -408,7 +500,6 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
408
500
|
unsubscribeCodexFast = undefined;
|
|
409
501
|
resetSessionCost();
|
|
410
502
|
activeTui = undefined;
|
|
411
|
-
requestGitStatusRefresh = undefined;
|
|
412
503
|
goalRuntime.requestRender = undefined;
|
|
413
504
|
});
|
|
414
505
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { validateGoalObjective } from "./goal-state.ts";
|
|
2
|
+
|
|
3
|
+
export type GoalCommand =
|
|
4
|
+
| { kind: "status" }
|
|
5
|
+
| { kind: "objective"; objective: string }
|
|
6
|
+
| { kind: "pause" }
|
|
7
|
+
| { kind: "resume" }
|
|
8
|
+
| { kind: "clear" }
|
|
9
|
+
| { kind: "invalid"; message: string };
|
|
10
|
+
|
|
11
|
+
/** Parses one raw /goal argument string into a closed command variant. */
|
|
12
|
+
export function parseGoalCommand(args: string): GoalCommand {
|
|
13
|
+
const input = args.trim();
|
|
14
|
+
if (!input) return { kind: "status" };
|
|
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" };
|
|
23
|
+
}
|