killeros 2.0.4 → 2.0.6
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 +2 -0
- package/README.md +20 -12
- package/killeros/commands.ts +1 -2
- package/killeros/goals.ts +179 -51
- package/killeros/hooks.ts +25 -10
- package/killeros/init-evidence.ts +49 -13
- package/killeros/notifications.ts +1 -1
- package/killeros/question.ts +381 -182
- package/killeros/runtime.ts +7 -0
- package/killeros/shell-ui.ts +41 -13
- package/killeros/worked-for.ts +80 -0
- package/package.json +1 -1
package/killeros/runtime.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface InitRuntime {
|
|
|
20
20
|
|
|
21
21
|
export type GoalStatus = "active" | "paused" | "blocked" | "complete";
|
|
22
22
|
|
|
23
|
+
export interface GoalBlockerAudit {
|
|
24
|
+
key: string;
|
|
25
|
+
streak: number;
|
|
26
|
+
lastTurn: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export interface GoalState {
|
|
24
30
|
version: 1;
|
|
25
31
|
revision: number;
|
|
@@ -34,6 +40,7 @@ export interface GoalState {
|
|
|
34
40
|
baselineTokens: number;
|
|
35
41
|
result?: string;
|
|
36
42
|
resumeAfterManualCompaction?: true;
|
|
43
|
+
blockerAudit?: GoalBlockerAudit;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
export interface GoalRuntime {
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import {
|
|
13
13
|
Container,
|
|
14
|
+
CURSOR_MARKER,
|
|
14
15
|
Text,
|
|
15
16
|
truncateToWidth,
|
|
16
17
|
visibleWidth,
|
|
@@ -43,6 +44,13 @@ const STARTUP_TIPS = [
|
|
|
43
44
|
"Run /notification to enable a terminal bell when work settles.",
|
|
44
45
|
] as const;
|
|
45
46
|
|
|
47
|
+
const EDITOR_SUGGESTIONS = [
|
|
48
|
+
'Try "how does <filepath> work?"',
|
|
49
|
+
'Try "find edge cases in <filepath>"',
|
|
50
|
+
'Try "simplify <filepath> without changing behavior"',
|
|
51
|
+
'Try "write tests for <filepath>"',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
46
54
|
export function resolveGitBranch(cwd: string): Promise<string | undefined> {
|
|
47
55
|
return new Promise((resolve) => {
|
|
48
56
|
execFile(
|
|
@@ -66,13 +74,26 @@ export function resolveGitBranch(cwd: string): Promise<string | undefined> {
|
|
|
66
74
|
});
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
function
|
|
70
|
-
const
|
|
71
|
-
for (let index =
|
|
77
|
+
function shuffledDeck(values: readonly string[]): string[] {
|
|
78
|
+
const deck = [...values];
|
|
79
|
+
for (let index = deck.length - 1; index > 0; index -= 1) {
|
|
72
80
|
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
73
|
-
[
|
|
81
|
+
[deck[index], deck[swapIndex]] = [deck[swapIndex]!, deck[index]!];
|
|
74
82
|
}
|
|
75
|
-
return
|
|
83
|
+
return deck;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let tipDeck: string[] = [];
|
|
87
|
+
let editorSuggestionDeck: string[] = [];
|
|
88
|
+
|
|
89
|
+
function nextStartupTip(): string {
|
|
90
|
+
if (tipDeck.length === 0) tipDeck = shuffledDeck(STARTUP_TIPS);
|
|
91
|
+
return tipDeck.pop() ?? STARTUP_TIPS[0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function nextEditorSuggestion(): string {
|
|
95
|
+
if (editorSuggestionDeck.length === 0) editorSuggestionDeck = shuffledDeck(EDITOR_SUGGESTIONS);
|
|
96
|
+
return editorSuggestionDeck.pop() ?? EDITOR_SUGGESTIONS[0];
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
function compactBoxLine(content: string, width: number, theme: Theme): string {
|
|
@@ -167,16 +188,19 @@ function isScrolledTopBorder(line: string): boolean {
|
|
|
167
188
|
class PiCodeEditor extends CustomEditor {
|
|
168
189
|
private readonly appKeybindings: KeybindingsManager;
|
|
169
190
|
private readonly runtimeTheme: Theme;
|
|
191
|
+
private readonly suggestion: string;
|
|
170
192
|
|
|
171
193
|
constructor(
|
|
172
194
|
tui: TUI,
|
|
173
195
|
theme: EditorTheme,
|
|
174
196
|
appKeybindings: KeybindingsManager,
|
|
175
197
|
runtimeTheme: Theme,
|
|
198
|
+
suggestion: string,
|
|
176
199
|
) {
|
|
177
200
|
super(tui, theme, appKeybindings);
|
|
178
201
|
this.appKeybindings = appKeybindings;
|
|
179
202
|
this.runtimeTheme = runtimeTheme;
|
|
203
|
+
this.suggestion = suggestion;
|
|
180
204
|
}
|
|
181
205
|
|
|
182
206
|
override handleInput(data: string): void {
|
|
@@ -220,8 +244,16 @@ class PiCodeEditor extends CustomEditor {
|
|
|
220
244
|
}
|
|
221
245
|
|
|
222
246
|
for (let index = 1; index < bottomBorderIndex; index += 1) {
|
|
223
|
-
const
|
|
224
|
-
|
|
247
|
+
const isPromptLine = index === 1 && !isScrolledHeader;
|
|
248
|
+
const prefix = isPromptLine ? gray("❯\u00A0") : " ";
|
|
249
|
+
let content = lines[index] ?? "";
|
|
250
|
+
if (isPromptLine && this.getText() === "") {
|
|
251
|
+
const first = this.suggestion.slice(0, 1);
|
|
252
|
+
const rest = this.suggestion.slice(1);
|
|
253
|
+
const cursorMarker = this.focused ? CURSOR_MARKER : "";
|
|
254
|
+
content = `${cursorMarker}\x1B[7m${gray(first)}\x1B[27m${gray(rest)}`;
|
|
255
|
+
}
|
|
256
|
+
framed.push(`${prefix}${padRight(content, innerWidth)}`);
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
const bottom = stripAnsi(lines[bottomBorderIndex] ?? "");
|
|
@@ -258,11 +290,6 @@ export function registerShellUi(pi: ExtensionAPI): void {
|
|
|
258
290
|
let activityDeck: string[] = [];
|
|
259
291
|
let lastActivityWord: string | undefined;
|
|
260
292
|
let activityTimer: ReturnType<typeof setInterval> | undefined;
|
|
261
|
-
let tipDeck: string[] = [];
|
|
262
|
-
const nextStartupTip = (): string => {
|
|
263
|
-
if (tipDeck.length === 0) tipDeck = shuffledTips();
|
|
264
|
-
return tipDeck.pop() ?? STARTUP_TIPS[0];
|
|
265
|
-
};
|
|
266
293
|
const refillActivityDeck = (): void => {
|
|
267
294
|
activityDeck = [...ACTIVITY_WORDS];
|
|
268
295
|
for (let index = activityDeck.length - 1; index > 0; index -= 1) {
|
|
@@ -302,8 +329,9 @@ export function registerShellUi(pi: ExtensionAPI): void {
|
|
|
302
329
|
ctx.ui.setHiddenThinkingLabel("└ Thinking…");
|
|
303
330
|
const existingEditorFactory = ctx.ui.getEditorComponent?.();
|
|
304
331
|
if (!existingEditorFactory || existingEditorFactory === killerosEditorFactory) {
|
|
332
|
+
const editorSuggestion = nextEditorSuggestion();
|
|
305
333
|
killerosEditorFactory = (tui, editorTheme, keybindings) =>
|
|
306
|
-
new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme);
|
|
334
|
+
new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme, editorSuggestion);
|
|
307
335
|
ctx.ui.setEditorComponent(killerosEditorFactory);
|
|
308
336
|
}
|
|
309
337
|
} catch (error) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
const WORKED_FOR_ENTRY_TYPE = "killeros-worked-for";
|
|
5
|
+
const WORKED_FOR_ENTRY_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
interface WorkedForEntryData {
|
|
8
|
+
version: typeof WORKED_FOR_ENTRY_VERSION;
|
|
9
|
+
milliseconds: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatWorkedForDuration(milliseconds: number): string {
|
|
13
|
+
const boundedMilliseconds = Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : 0;
|
|
14
|
+
const totalSeconds = Math.max(1, Math.floor(boundedMilliseconds / 1_000));
|
|
15
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
16
|
+
|
|
17
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
18
|
+
if (totalMinutes < 60) {
|
|
19
|
+
return `${totalMinutes}m ${(totalSeconds % 60).toString().padStart(2, "0")}s`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${Math.floor(totalMinutes / 60)}h ${(totalMinutes % 60).toString().padStart(2, "0")}m`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isWorkedForEntryData(data: unknown): data is WorkedForEntryData {
|
|
26
|
+
if (!data || typeof data !== "object") return false;
|
|
27
|
+
const candidate = data as Partial<WorkedForEntryData>;
|
|
28
|
+
return candidate.version === WORKED_FOR_ENTRY_VERSION
|
|
29
|
+
&& typeof candidate.milliseconds === "number"
|
|
30
|
+
&& Number.isFinite(candidate.milliseconds)
|
|
31
|
+
&& candidate.milliseconds >= 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function errorMessage(error: unknown): string {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerWorkedFor(
|
|
39
|
+
pi: ExtensionAPI,
|
|
40
|
+
now: () => number = Date.now,
|
|
41
|
+
): void {
|
|
42
|
+
let startedAt: number | undefined;
|
|
43
|
+
|
|
44
|
+
pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, _options, theme) => {
|
|
45
|
+
if (!isWorkedForEntryData(entry.data)) return undefined;
|
|
46
|
+
return new Text(
|
|
47
|
+
theme.fg("dim", `✻ Worked for ${formatWorkedForDuration(entry.data.milliseconds)}`),
|
|
48
|
+
0,
|
|
49
|
+
0,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.on("session_start", () => {
|
|
54
|
+
startedAt = undefined;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
58
|
+
if (ctx.mode !== "tui" || startedAt !== undefined) return;
|
|
59
|
+
startedAt = now();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
63
|
+
if (ctx.mode !== "tui" || startedAt === undefined) return;
|
|
64
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
65
|
+
const milliseconds = Math.max(0, now() - startedAt);
|
|
66
|
+
startedAt = undefined;
|
|
67
|
+
try {
|
|
68
|
+
pi.appendEntry<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, {
|
|
69
|
+
version: WORKED_FOR_ENTRY_VERSION,
|
|
70
|
+
milliseconds,
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
ctx.ui.notify(`Worked-for timing could not be saved: ${errorMessage(error)}`, "error");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.on("session_shutdown", () => {
|
|
78
|
+
startedAt = undefined;
|
|
79
|
+
});
|
|
80
|
+
}
|