killeros 1.5.7 → 2.0.0
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 +8 -119
- package/Killeros.ts +1 -25
- package/README.md +130 -220
- package/killeros/commands.ts +1 -238
- package/killeros/display.ts +6 -2
- package/killeros/footer.ts +8 -3
- package/killeros/goals.ts +10 -9
- package/killeros/hooks.ts +1 -1
- package/killeros/limits.ts +1 -0
- package/killeros/personal-instructions.ts +3 -1
- package/killeros/question.ts +19 -4
- package/package.json +2 -8
- package/agents/debugger.md +0 -50
- package/agents/documenter.md +0 -49
- package/agents/planner.md +0 -53
- package/agents/reviewer.md +0 -58
- package/agents/scout.md +0 -56
- package/agents/security.md +0 -54
- package/agents/tester.md +0 -50
- package/agents/worker.md +0 -54
- package/killeros/subagent-lifecycle.ts +0 -761
- package/killeros/subagent-persistence.ts +0 -572
- package/killeros/subagent-process.ts +0 -626
- package/killeros/subagent-ui.ts +0 -245
- package/killeros/subagents.ts +0 -3048
- package/subagent-lifecycle.ts +0 -1
- package/subagent-process.ts +0 -1
- package/subagent-ui.ts +0 -1
- package/subagents.ts +0 -1
package/killeros/commands.ts
CHANGED
|
@@ -1,76 +1,5 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext
|
|
1
|
+
import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
-
import { formatThreadControls, type ThreadStatus } from "./subagent-ui.ts";
|
|
4
|
-
|
|
5
|
-
export type SubagentControlAction = "list" | "inspect" | "wait" | "steer" | "interrupt" | "collect" | "resume" | "close";
|
|
6
|
-
|
|
7
|
-
export interface SubagentControlRequest {
|
|
8
|
-
action: SubagentControlAction;
|
|
9
|
-
threadId?: string;
|
|
10
|
-
all?: true;
|
|
11
|
-
message?: string;
|
|
12
|
-
task?: string;
|
|
13
|
-
timeoutMs?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface SubagentControlThread {
|
|
17
|
-
id: string;
|
|
18
|
-
displayName?: string;
|
|
19
|
-
name?: string;
|
|
20
|
-
agent?: string;
|
|
21
|
-
role?: string;
|
|
22
|
-
task?: string;
|
|
23
|
-
prompt?: string;
|
|
24
|
-
status?: string;
|
|
25
|
-
state?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SubagentControlDetails {
|
|
29
|
-
results?: readonly SubagentControlThread[];
|
|
30
|
-
threads?: readonly SubagentControlThread[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface SubagentControlResult {
|
|
34
|
-
text: string;
|
|
35
|
-
details?: SubagentControlDetails;
|
|
36
|
-
usage?: unknown;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SubagentControlApi {
|
|
40
|
-
execute(request: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface SubagentToolLike {
|
|
44
|
-
name: string;
|
|
45
|
-
execute(
|
|
46
|
-
toolCallId: string,
|
|
47
|
-
params: unknown,
|
|
48
|
-
signal: AbortSignal | undefined,
|
|
49
|
-
onUpdate: undefined,
|
|
50
|
-
ctx: ExtensionContext,
|
|
51
|
-
): Promise<{
|
|
52
|
-
content?: readonly { type: string; text?: string }[];
|
|
53
|
-
details?: unknown;
|
|
54
|
-
usage?: unknown;
|
|
55
|
-
}>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function createSubagentControlApi(tool: SubagentToolLike): SubagentControlApi {
|
|
59
|
-
return {
|
|
60
|
-
async execute(request, ctx) {
|
|
61
|
-
const toolRequest = request.action === "interrupt" && request.threadId === "all"
|
|
62
|
-
? { action: "interrupt", all: true }
|
|
63
|
-
: request;
|
|
64
|
-
const result = await tool.execute("subagents-command", toolRequest, ctx.signal, undefined, ctx);
|
|
65
|
-
const text = result.content?.find((item) => item.type === "text")?.text ?? "";
|
|
66
|
-
return {
|
|
67
|
-
text,
|
|
68
|
-
details: result.details as SubagentControlDetails | undefined,
|
|
69
|
-
usage: result.usage,
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
3
|
|
|
75
4
|
async function confirmNewSession(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
76
5
|
if (!ctx.hasUI) return true;
|
|
@@ -128,7 +57,6 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
|
|
|
128
57
|
const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
|
|
129
58
|
goal: "/goal [objective|clear|edit|pause|resume]",
|
|
130
59
|
variants: "/variants [level]",
|
|
131
|
-
subagents: "/subagents [list|inspect|wait|steer|interrupt|collect|resume|close] [thread]",
|
|
132
60
|
model: "/model [provider/model]",
|
|
133
61
|
"scoped-models": "/scoped-models",
|
|
134
62
|
login: "/login [provider]",
|
|
@@ -151,162 +79,6 @@ function scoreCommandMatch(name: string, prefix: string): number {
|
|
|
151
79
|
return 0;
|
|
152
80
|
}
|
|
153
81
|
|
|
154
|
-
const SUBAGENT_COMMAND_USAGE = "/subagents [list|inspect|wait|steer|interrupt|collect|resume|close] [thread]";
|
|
155
|
-
|
|
156
|
-
function subagentCommandError(message: string): Error {
|
|
157
|
-
return new Error(`${message} Usage: ${SUBAGENT_COMMAND_USAGE}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function parseThreadReference(action: SubagentControlAction, tail: string): string {
|
|
161
|
-
const reference = tail.match(/^(\S+)(?:\s+([\s\S]*))?$/u)?.[1];
|
|
162
|
-
if (!reference) throw subagentCommandError(`/subagents ${action} requires a thread reference.`);
|
|
163
|
-
return reference;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseExplicitSubagentCommand(args: string): SubagentControlRequest {
|
|
167
|
-
const trimmed = args.trim();
|
|
168
|
-
const match = trimmed.match(/^(\S+)(?:\s+([\s\S]*))?$/u);
|
|
169
|
-
if (!match) throw subagentCommandError("/subagents requires an explicit verb outside TUI.");
|
|
170
|
-
const action = match[1]!.toLocaleLowerCase() as SubagentControlAction;
|
|
171
|
-
const tail = match[2]?.trim() ?? "";
|
|
172
|
-
|
|
173
|
-
if (action === "list") {
|
|
174
|
-
if (tail) throw subagentCommandError("/subagents list does not accept arguments.");
|
|
175
|
-
return { action };
|
|
176
|
-
}
|
|
177
|
-
if (action === "wait") {
|
|
178
|
-
if (!tail) return { action };
|
|
179
|
-
const parts = tail.split(/\s+/u);
|
|
180
|
-
if (parts.length > 2) throw subagentCommandError("/subagents wait accepts one thread reference and one timeout-ms value.");
|
|
181
|
-
if (parts.length === 1 && /^\d+$/u.test(parts[0]!)) {
|
|
182
|
-
return { action, timeoutMs: parseTimeout(parts[0]!) };
|
|
183
|
-
}
|
|
184
|
-
const request: SubagentControlRequest = { action, threadId: parts[0] };
|
|
185
|
-
if (parts[1] !== undefined) request.timeoutMs = parseTimeout(parts[1]);
|
|
186
|
-
return request;
|
|
187
|
-
}
|
|
188
|
-
if (action === "steer") {
|
|
189
|
-
const referenceAndMessage = tail.match(/^(\S+)(?:\s+([\s\S]+))?$/u);
|
|
190
|
-
if (!referenceAndMessage?.[1]) throw subagentCommandError("/subagents steer requires a thread reference.");
|
|
191
|
-
if (!referenceAndMessage[2]?.trim()) throw subagentCommandError("/subagents steer requires a message.");
|
|
192
|
-
return { action, threadId: referenceAndMessage[1], message: referenceAndMessage[2] };
|
|
193
|
-
}
|
|
194
|
-
if (action === "resume") {
|
|
195
|
-
const referenceAndTask = tail.match(/^(\S+)(?:\s+([\s\S]+))?$/u);
|
|
196
|
-
if (!referenceAndTask?.[1]) throw subagentCommandError("/subagents resume requires a thread reference.");
|
|
197
|
-
return {
|
|
198
|
-
action,
|
|
199
|
-
threadId: referenceAndTask[1],
|
|
200
|
-
...(referenceAndTask[2] ? { task: referenceAndTask[2] } : {}),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (action === "inspect" || action === "interrupt" || action === "collect" || action === "close") {
|
|
204
|
-
const threadId = parseThreadReference(action, tail);
|
|
205
|
-
if (tail.slice(threadId.length).trim()) throw subagentCommandError(`/subagents ${action} accepts one thread reference.`);
|
|
206
|
-
return { action, threadId };
|
|
207
|
-
}
|
|
208
|
-
throw subagentCommandError(`Unknown /subagents action ${JSON.stringify(match[1])}.`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function parseTimeout(value: string): number {
|
|
212
|
-
const timeoutMs = Number(value);
|
|
213
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
|
|
214
|
-
throw subagentCommandError("/subagents wait timeout-ms must be a non-negative integer.");
|
|
215
|
-
}
|
|
216
|
-
return timeoutMs;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function controlThreads(result: SubagentControlResult): SubagentControlThread[] {
|
|
220
|
-
const details = result.details;
|
|
221
|
-
if (!details) return [];
|
|
222
|
-
const results = [...(details.results ?? [])];
|
|
223
|
-
const threads = [...(details.threads ?? [])];
|
|
224
|
-
const candidates = results.length ? results : threads;
|
|
225
|
-
return candidates.filter((thread) => thread && typeof thread.id === "string" && thread.state !== "closed" && thread.status !== "closed");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function threadStatus(thread: SubagentControlThread): ThreadStatus {
|
|
229
|
-
const status = (thread.status ?? thread.state ?? "queued").toLocaleLowerCase();
|
|
230
|
-
if (status === "active" || status === "running") return "running";
|
|
231
|
-
if (status === "done" || status === "complete" || status === "closed") return "complete";
|
|
232
|
-
if (status === "stopped" || status === "cancelled") return "cancelled";
|
|
233
|
-
if (status === "limited") return "limited";
|
|
234
|
-
if (status === "orphaned") return "orphaned";
|
|
235
|
-
if (status === "failed") return "failed";
|
|
236
|
-
return "queued";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function threadLabel(thread: SubagentControlThread): string {
|
|
240
|
-
const name = thread.displayName ?? thread.name ?? thread.agent ?? thread.role ?? thread.id;
|
|
241
|
-
return `${name} · ${thread.id} · ${threadStatus(thread)}`;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function selectedThread(threads: readonly SubagentControlThread[], labels: readonly string[], choice: string): SubagentControlThread | undefined {
|
|
245
|
-
const index = labels.indexOf(choice);
|
|
246
|
-
if (index >= 0) return threads[index];
|
|
247
|
-
return threads.find((thread) => thread.id === choice || thread.displayName === choice || thread.name === choice);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function executeSubagentControl(
|
|
251
|
-
control: SubagentControlApi | undefined,
|
|
252
|
-
request: SubagentControlRequest,
|
|
253
|
-
ctx: ExtensionCommandContext,
|
|
254
|
-
): Promise<void> {
|
|
255
|
-
if (!control) throw new Error("Subagent control API is not available.");
|
|
256
|
-
const result = await control.execute(request, ctx);
|
|
257
|
-
if (result?.text) ctx.ui.notify(result.text, "info");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function runTuiSubagentCommand(control: SubagentControlApi | undefined, ctx: ExtensionCommandContext): Promise<void> {
|
|
261
|
-
if (!control) throw new Error("Subagent control API is not available.");
|
|
262
|
-
const listed = await control.execute({ action: "list" }, ctx);
|
|
263
|
-
const threads = controlThreads(listed);
|
|
264
|
-
if (!threads.length) {
|
|
265
|
-
ctx.ui.notify("No child threads.", "info");
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const labels = threads.map(threadLabel);
|
|
270
|
-
const selected = await ctx.ui.select("Select a thread", labels);
|
|
271
|
-
if (selected === undefined) return;
|
|
272
|
-
const thread = selectedThread(threads, labels, selected);
|
|
273
|
-
if (!thread) return;
|
|
274
|
-
|
|
275
|
-
const controls = formatThreadControls(threadStatus(thread)).filter((item) => item.enabled);
|
|
276
|
-
const controlLabels = controls.map((item) => item.label);
|
|
277
|
-
const selectedControl = await ctx.ui.select("Select a control", controlLabels);
|
|
278
|
-
if (selectedControl === undefined) return;
|
|
279
|
-
const chosen = controls.find((item) => item.label === selectedControl || item.id === selectedControl);
|
|
280
|
-
if (!chosen) return;
|
|
281
|
-
|
|
282
|
-
const request: SubagentControlRequest = { action: chosen.id, threadId: thread.id };
|
|
283
|
-
if (chosen.id === "steer") {
|
|
284
|
-
const message = await ctx.ui.input("Steer child thread", "Message");
|
|
285
|
-
if (message === undefined || !message.trim()) return;
|
|
286
|
-
request.message = message;
|
|
287
|
-
} else if (chosen.id === "resume") {
|
|
288
|
-
const task = await ctx.ui.input("Resume child thread", "Optional task");
|
|
289
|
-
if (task === undefined) return;
|
|
290
|
-
if (task) request.task = task;
|
|
291
|
-
}
|
|
292
|
-
await executeSubagentControl(control, request, ctx);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function registerSubagentCommand(pi: ExtensionAPI, control?: SubagentControlApi | void): void {
|
|
296
|
-
const api = control && typeof control.execute === "function" ? control : undefined;
|
|
297
|
-
pi.registerCommand("subagents", {
|
|
298
|
-
description: "Inspect and control child threads",
|
|
299
|
-
handler: async (args, ctx) => {
|
|
300
|
-
if (!args.trim()) {
|
|
301
|
-
if (ctx.mode !== "tui") throw subagentCommandError("/subagents requires an explicit verb outside TUI.");
|
|
302
|
-
await runTuiSubagentCommand(api, ctx);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await executeSubagentControl(api, parseExplicitSubagentCommand(args), ctx);
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
82
|
export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
311
83
|
const usage = new Map<string, number>();
|
|
312
84
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -349,15 +121,6 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
|
349
121
|
}
|
|
350
122
|
}
|
|
351
123
|
|
|
352
|
-
if (!commands.has("subagents")) {
|
|
353
|
-
commands.set("subagents", {
|
|
354
|
-
name: "subagents",
|
|
355
|
-
description: "Inspect and control child threads",
|
|
356
|
-
category: "Extension",
|
|
357
|
-
syntaxHint: COMMAND_SYNTAX_HINTS.subagents,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
124
|
const ranked = [...commands.values()]
|
|
362
125
|
.map((command) => ({
|
|
363
126
|
command,
|
package/killeros/display.ts
CHANGED
|
@@ -6,9 +6,11 @@ export function formatCwd(cwd: string): string {
|
|
|
6
6
|
if (!home) return cwd;
|
|
7
7
|
const normalizedHome = home.replace(/[\\/]+$/, "");
|
|
8
8
|
const normalizedCwd = cwd.replace(/[\\/]+$/, "");
|
|
9
|
-
|
|
9
|
+
const comparedHome = process.platform === "win32" ? normalizedHome.toLocaleLowerCase() : normalizedHome;
|
|
10
|
+
const comparedCwd = process.platform === "win32" ? normalizedCwd.toLocaleLowerCase() : normalizedCwd;
|
|
11
|
+
if (comparedCwd === comparedHome) return "~";
|
|
10
12
|
const separator = normalizedCwd.slice(normalizedHome.length, normalizedHome.length + 1);
|
|
11
|
-
return
|
|
13
|
+
return comparedCwd.startsWith(comparedHome) && (separator === "/" || separator === "\\")
|
|
12
14
|
? `~${normalizedCwd.slice(normalizedHome.length)}`
|
|
13
15
|
: cwd;
|
|
14
16
|
}
|
|
@@ -20,6 +22,7 @@ export function padRight(text: string, width: number): string {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function formatTime(milliseconds: number): string {
|
|
25
|
+
if (!Number.isFinite(milliseconds)) return "0s";
|
|
23
26
|
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
24
27
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
25
28
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -28,6 +31,7 @@ export function formatTime(milliseconds: number): string {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function formatTokens(value: number): string {
|
|
34
|
+
if (!Number.isFinite(value)) return "0";
|
|
31
35
|
const amount = Math.max(0, value);
|
|
32
36
|
if (amount < 1_000) return `${Math.round(amount)}`;
|
|
33
37
|
if (amount >= 1_000_000) {
|
package/killeros/footer.ts
CHANGED
|
@@ -13,8 +13,8 @@ export function formatCost(usd: number): string {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
|
|
16
|
-
if (tokensUsed === null) return theme.fg("dim", "—% left (—)");
|
|
17
|
-
const windowSize = contextWindow > 0 ? contextWindow : 128_000;
|
|
16
|
+
if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "—% left (—)");
|
|
17
|
+
const windowSize = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 128_000;
|
|
18
18
|
const remaining = Math.max(0, Math.min(windowSize, windowSize - Math.max(0, tokensUsed)));
|
|
19
19
|
const percentLeft = Math.max(0, Math.min(100, Math.round((remaining / windowSize) * 100)));
|
|
20
20
|
const color: ThemeColor = percentLeft < 20 ? "error" : percentLeft <= 50 ? "warning" : "success";
|
|
@@ -150,7 +150,12 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
150
150
|
const level = model?.reasoning === false
|
|
151
151
|
? theme.fg("thinkingOff", "no reasoning")
|
|
152
152
|
: theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
|
|
153
|
-
|
|
153
|
+
let usage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
154
|
+
try {
|
|
155
|
+
usage = ctx.getContextUsage();
|
|
156
|
+
} catch {
|
|
157
|
+
usage = undefined;
|
|
158
|
+
}
|
|
154
159
|
const contextWindow = usage?.contextWindow ?? model?.contextWindow ?? 128_000;
|
|
155
160
|
const context = formatContextProgress(usage?.tokens ?? null, contextWindow, theme);
|
|
156
161
|
const branch = footerData.getGitBranch();
|
package/killeros/goals.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { MAX_NODE_TIMER_MS } from "./
|
|
4
|
+
import { MAX_NODE_TIMER_MS } from "./limits.ts";
|
|
5
5
|
import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
|
|
6
6
|
import { formatTime, formatTokens } from "./display.ts";
|
|
7
7
|
import { reportError } from "./errors.ts";
|
|
@@ -209,7 +209,7 @@ function scheduleGoalContinuation(
|
|
|
209
209
|
runtime: GoalRuntime,
|
|
210
210
|
initState: InitRuntime,
|
|
211
211
|
ctx: ExtensionContext,
|
|
212
|
-
):
|
|
212
|
+
): boolean {
|
|
213
213
|
if (!isGoalModeSupported(ctx)
|
|
214
214
|
|| !isSavedSession(ctx)
|
|
215
215
|
|| runtime.state?.status !== "active"
|
|
@@ -217,7 +217,7 @@ function scheduleGoalContinuation(
|
|
|
217
217
|
|| runtime.continuationHeld
|
|
218
218
|
|| runtime.goalTurnInFlight
|
|
219
219
|
|| initState.active
|
|
220
|
-
|| ctx.hasPendingMessages()) return;
|
|
220
|
+
|| ctx.hasPendingMessages()) return false;
|
|
221
221
|
const current = runtime.state;
|
|
222
222
|
runtime.continuationScheduled = true;
|
|
223
223
|
runtime.goalTurnInFlight = false;
|
|
@@ -230,10 +230,12 @@ function scheduleGoalContinuation(
|
|
|
230
230
|
content: goalContinuationMessage(current, ctx),
|
|
231
231
|
display: false,
|
|
232
232
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
233
|
+
return true;
|
|
233
234
|
} catch (error) {
|
|
234
235
|
runtime.continuationScheduled = false;
|
|
235
236
|
runtime.goalTurnInFlight = false;
|
|
236
237
|
pauseGoalAfterFailure(pi, runtime, ctx, `continuation could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
238
|
+
return false;
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
@@ -551,8 +553,7 @@ export function registerGoal(
|
|
|
551
553
|
try {
|
|
552
554
|
transitionGoal(pi, runtime, "resume", "active", undefined, true);
|
|
553
555
|
runtime.continuationScheduled = false;
|
|
554
|
-
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
555
|
-
ctx.ui.notify("Goal resumed", "info");
|
|
556
|
+
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
|
|
556
557
|
} catch (error) {
|
|
557
558
|
reportError(ctx, "Goal could not be resumed", error);
|
|
558
559
|
}
|
|
@@ -612,8 +613,7 @@ export function registerGoal(
|
|
|
612
613
|
try {
|
|
613
614
|
persistGoalState(pi, runtime, "edit", next);
|
|
614
615
|
runtime.continuationScheduled = false;
|
|
615
|
-
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
616
|
-
ctx.ui.notify("Goal updated and active", "info");
|
|
616
|
+
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal updated and active", "info");
|
|
617
617
|
} catch (error) {
|
|
618
618
|
pauseGoalAfterFailure(
|
|
619
619
|
pi,
|
|
@@ -677,8 +677,9 @@ export function registerGoal(
|
|
|
677
677
|
};
|
|
678
678
|
try {
|
|
679
679
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
680
|
-
scheduleGoalContinuation(pi, runtime, initState, ctx)
|
|
681
|
-
|
|
680
|
+
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
|
|
681
|
+
ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
|
|
682
|
+
}
|
|
682
683
|
} catch (error) {
|
|
683
684
|
reportError(ctx, "Goal could not be started", error);
|
|
684
685
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
package/killeros/hooks.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CONFIG_DIR_NAME, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { reportError } from "./errors.ts";
|
|
6
|
-
import { MAX_NODE_TIMER_MS } from "./
|
|
6
|
+
import { MAX_NODE_TIMER_MS } from "./limits.ts";
|
|
7
7
|
|
|
8
8
|
type KillerosHookEvent = "tool_call" | "tool_result" | "agent_settled";
|
|
9
9
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_NODE_TIMER_MS = 2_147_483_647;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import type { InitRuntime } from "./runtime.ts";
|
|
@@ -14,7 +15,8 @@ function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT):
|
|
|
14
15
|
descriptor = openSync(filePath, "r");
|
|
15
16
|
const buffer = Buffer.alloc(limit + 1);
|
|
16
17
|
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
17
|
-
const
|
|
18
|
+
const decoder = new StringDecoder("utf8");
|
|
19
|
+
const content = decoder.write(buffer.subarray(0, Math.min(bytesRead, limit)));
|
|
18
20
|
if (!content.trim()) return undefined;
|
|
19
21
|
return bytesRead > limit
|
|
20
22
|
? `${content}\n\n[Personal instructions truncated by KillerOS]`
|
package/killeros/question.ts
CHANGED
|
@@ -56,6 +56,8 @@ type QuestionSelection =
|
|
|
56
56
|
const CUSTOM_INPUT_MAX_CHARACTERS = 4_000;
|
|
57
57
|
const CUSTOM_INPUT_HISTORY_LIMIT = 100;
|
|
58
58
|
const CUSTOM_INPUT_HISTORY_BYTES = 64 * 1024;
|
|
59
|
+
const FILTER_QUERY_MAX_CHARACTERS = 4_000;
|
|
60
|
+
const FILTER_QUERY_MAX_BYTES = 16_000;
|
|
59
61
|
|
|
60
62
|
function isPrintableInput(data: string): boolean {
|
|
61
63
|
return data.length > 0 && !/[\u0000-\u001F\u007F-\u009F]/u.test(data);
|
|
@@ -123,7 +125,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
123
125
|
pi.registerTool<typeof QuestionParams, QuestionDetails>({
|
|
124
126
|
name: "question",
|
|
125
127
|
label: "Question",
|
|
126
|
-
description:
|
|
128
|
+
description: `Ask one interactive multiple-choice question. Provide 1-9 concise options. The user can filter options or type a custom answer. Filter queries are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes.`,
|
|
127
129
|
promptSnippet: "Ask the user one multiple-choice question when a decision is required to proceed",
|
|
128
130
|
promptGuidelines: [
|
|
129
131
|
"Use question only when user input is required to choose between concrete alternatives; do not use question for rhetorical or optional follow-up prompts.",
|
|
@@ -198,6 +200,21 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
198
200
|
tui.requestRender();
|
|
199
201
|
};
|
|
200
202
|
|
|
203
|
+
const appendFilterInput = (value: string): void => {
|
|
204
|
+
const nextCharacters = inputCharacterCount(filterQuery) + inputCharacterCount(value);
|
|
205
|
+
const nextBytes = Buffer.byteLength(filterQuery, "utf8") + Buffer.byteLength(value, "utf8");
|
|
206
|
+
if (nextCharacters > FILTER_QUERY_MAX_CHARACTERS || nextBytes > FILTER_QUERY_MAX_BYTES) {
|
|
207
|
+
ctx.ui.notify(
|
|
208
|
+
`Question filters are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes`,
|
|
209
|
+
"error",
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
filterQuery += value;
|
|
214
|
+
optionIndex = 0;
|
|
215
|
+
refresh();
|
|
216
|
+
};
|
|
217
|
+
|
|
201
218
|
editor.onSubmit = (value) => {
|
|
202
219
|
const answer = value.trim();
|
|
203
220
|
if (answer) {
|
|
@@ -288,9 +305,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
288
305
|
return;
|
|
289
306
|
}
|
|
290
307
|
if (printableInput) {
|
|
291
|
-
|
|
292
|
-
optionIndex = 0;
|
|
293
|
-
refresh();
|
|
308
|
+
appendFilterInput(printableInput);
|
|
294
309
|
}
|
|
295
310
|
};
|
|
296
311
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -22,11 +22,6 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"Killeros.ts",
|
|
24
24
|
"killeros/*.ts",
|
|
25
|
-
"subagents.ts",
|
|
26
|
-
"subagent-lifecycle.ts",
|
|
27
|
-
"subagent-process.ts",
|
|
28
|
-
"subagent-ui.ts",
|
|
29
|
-
"agents/*.md",
|
|
30
25
|
"themes/killeros.json",
|
|
31
26
|
"README.md",
|
|
32
27
|
"CHANGELOG.md"
|
|
@@ -36,7 +31,7 @@
|
|
|
36
31
|
},
|
|
37
32
|
"scripts": {
|
|
38
33
|
"check": "tsc --noEmit",
|
|
39
|
-
"test": "node --test --experimental-strip-types test/*.test.
|
|
34
|
+
"test": "node --test --experimental-strip-types test/*.test.ts"
|
|
40
35
|
},
|
|
41
36
|
"pi": {
|
|
42
37
|
"extensions": [
|
|
@@ -50,7 +45,6 @@
|
|
|
50
45
|
"@earendil-works/pi-ai": ">=0.82.1",
|
|
51
46
|
"@earendil-works/pi-coding-agent": ">=0.82.1",
|
|
52
47
|
"@earendil-works/pi-tui": ">=0.82.1",
|
|
53
|
-
"pi-web-access": ">=0.17.1",
|
|
54
48
|
"typebox": ">=1.1.38 <2"
|
|
55
49
|
},
|
|
56
50
|
"devDependencies": {
|
package/agents/debugger.md
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: debugger
|
|
3
|
-
description: debugger — reproduce failures, eliminate competing root-cause hypotheses, fix the shared cause, and prove the regression is gone
|
|
4
|
-
access: write
|
|
5
|
-
tools: read, grep, find, ls, edit, write, bash, web_search, source_check, fetch_content, get_search_content
|
|
6
|
-
# Replace inherit with provider/model to pin this role; set thinking separately when needed.
|
|
7
|
-
model: inherit
|
|
8
|
-
thinking: inherit
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Role
|
|
12
|
-
|
|
13
|
-
You are the `debugger` role, a calm incident investigator who treats a symptom as a clue, never as a diagnosis. You may repair the code, but only after the failure and its cause are understood well enough to avoid a plausible-looking patch.
|
|
14
|
-
|
|
15
|
-
## Diagnostic gate
|
|
16
|
-
|
|
17
|
-
Require a concrete error, failing test, reproduction step, expected result, or observable mismatch. If the report is too vague, state the missing evidence and the cheapest way to obtain it instead of guessing or editing around the symptom.
|
|
18
|
-
|
|
19
|
-
## Investigation
|
|
20
|
-
|
|
21
|
-
1. **Reproduce.** Use the smallest existing command or test and preserve the actual output and conditions that matter.
|
|
22
|
-
2. **Trace.** Follow entry point to state transition to failure. Inspect every relevant caller, boundary, shared helper, cleanup path, and error transformation.
|
|
23
|
-
3. **Classify.** Decide whether the failure is runtime, logic, integration, configuration, dependency, timing, or data-flow related.
|
|
24
|
-
4. **Compete.** Keep two or three plausible hypotheses when the cause is not proven. Test the cheapest falsifier first. Use targeted history or blame only when current code leaves competing explanations.
|
|
25
|
-
5. **Prove.** Create a minimal reproduction or regression test before the fix when practical, then make the smallest root-cause change without unrelated refactoring.
|
|
26
|
-
|
|
27
|
-
## Verification
|
|
28
|
-
|
|
29
|
-
Rerun the original proof and the nearest regression checks. A passing unrelated test is not evidence that the reported bug is fixed. If the failure cannot be reproduced or the repair cannot be verified, say so plainly and do not claim completion.
|
|
30
|
-
|
|
31
|
-
## Report
|
|
32
|
-
|
|
33
|
-
Return the diagnosis, evidence chain, competing hypotheses that were eliminated, changed paths, commands and results, residual risk, and the next missing proof. Keep the repair narrow and leave broader cleanup to a separate request.
|
|
34
|
-
|
|
35
|
-
## Skills and web research
|
|
36
|
-
|
|
37
|
-
Before doing task work, always inspect the available skill list and load the most relevant skill with `read` from its `SKILL.md`. If no relevant skill exists, say so instead of inventing one.
|
|
38
|
-
|
|
39
|
-
When the task depends on current facts, external documentation, standards, package behavior, or a user-requested web lookup, use `web_search` to find sources and `fetch_content` to read the strongest pages. Use `source_check` when a claim needs exact passage evidence and `get_search_content` to retrieve bounded slices from stored results. Prefer primary sources, vary research queries when the question is broad, and cite URLs in the report. Do not claim to have searched or loaded a skill unless the tool call succeeded.
|
|
40
|
-
|
|
41
|
-
## Communication
|
|
42
|
-
|
|
43
|
-
Use these six rules in every response:
|
|
44
|
-
|
|
45
|
-
1. Never use a metaphor, simile, or other figure of speech which you are used to seeing in print.
|
|
46
|
-
2. Never use a long word where a short one will do.
|
|
47
|
-
3. If it is possible to cut a word out, always cut it out.
|
|
48
|
-
4. Never use the passive where you can use the active.
|
|
49
|
-
5. Never use a foreign phrase, a scientific word, or a jargon word if you can think of an everyday English equivalent.
|
|
50
|
-
6. Break any of these rules sooner than say anything outright barbarous.
|
package/agents/documenter.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: documenter
|
|
3
|
-
description: documenter — make source-backed docs answer the reader’s next question while preserving exact code, command, and behavior parity
|
|
4
|
-
access: write
|
|
5
|
-
tools: read, grep, find, ls, edit, write, bash, web_search, source_check, fetch_content, get_search_content
|
|
6
|
-
# Replace inherit with provider/model to pin this role; set thinking separately when needed.
|
|
7
|
-
model: inherit
|
|
8
|
-
thinking: inherit
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Role
|
|
12
|
-
|
|
13
|
-
You are the `documenter` role, a technical writer who treats documentation as part of the product’s interface, not a place to decorate guesses. The reader should finish knowing what to do, what will happen, and what to do when it does not.
|
|
14
|
-
|
|
15
|
-
## Reader contract
|
|
16
|
-
|
|
17
|
-
Before editing, identify the audience, their existing knowledge, the job they are trying to complete, the document’s scope, and its non-scope. Put the essential answer first; a busy reader may only see the opening paragraph.
|
|
18
|
-
|
|
19
|
-
## Source of truth
|
|
20
|
-
|
|
21
|
-
Read the implementation, tests, manifests, configuration, and existing documentation that establish the behavior. Source and runnable checks outrank stale prose. Derive every command, option, example, guarantee, version, and limitation from repository evidence. Never invent a feature, benchmark, workflow, or user outcome.
|
|
22
|
-
|
|
23
|
-
## Writing workflow
|
|
24
|
-
|
|
25
|
-
1. Map the reader’s goal to the smallest useful path: prerequisites, exact action, expected result, failure recovery, and useful depth.
|
|
26
|
-
2. Preserve project terminology and voice. Explain unfamiliar concepts by relating them to behavior the reader already knows.
|
|
27
|
-
3. Reuse verified examples and update the smallest relevant documentation surface; do not rewrite unrelated prose for style.
|
|
28
|
-
4. Keep README, API, configuration, and code claims in parity. Distinguish source files from generated output and call out ambiguity instead of laundering it into confident text.
|
|
29
|
-
|
|
30
|
-
## Verification and report
|
|
31
|
-
|
|
32
|
-
Check links, headings, code fences, examples, paths, versions, and cross-references when practical. Report the audience and evidence used, changed paths, checks performed, and any behavior that still needs an authoritative decision. Do not modify production code unless the request explicitly includes it.
|
|
33
|
-
|
|
34
|
-
## Skills and web research
|
|
35
|
-
|
|
36
|
-
Before doing task work, always inspect the available skill list and load the most relevant skill with `read` from its `SKILL.md`. If no relevant skill exists, say so instead of inventing one.
|
|
37
|
-
|
|
38
|
-
When the task depends on current facts, external documentation, standards, package behavior, or a user-requested web lookup, use `web_search` to find sources and `fetch_content` to read the strongest pages. Use `source_check` when a claim needs exact passage evidence and `get_search_content` to retrieve bounded slices from stored results. Prefer primary sources, vary research queries when the question is broad, and cite URLs in the report. Do not claim to have searched or loaded a skill unless the tool call succeeded.
|
|
39
|
-
|
|
40
|
-
## Communication
|
|
41
|
-
|
|
42
|
-
Use these six rules in every response:
|
|
43
|
-
|
|
44
|
-
1. Never use a metaphor, simile, or other figure of speech which you are used to seeing in print.
|
|
45
|
-
2. Never use a long word where a short one will do.
|
|
46
|
-
3. If it is possible to cut a word out, always cut it out.
|
|
47
|
-
4. Never use the passive where you can use the active.
|
|
48
|
-
5. Never use a foreign phrase, a scientific word, or a jargon word if you can think of an everyday English equivalent.
|
|
49
|
-
6. Break any of these rules sooner than say anything outright barbarous.
|
package/agents/planner.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: planner
|
|
3
|
-
description: planner — turn an ambiguous request into the smallest buildable route with explicit evidence, contracts, decisions, and proof
|
|
4
|
-
access: read
|
|
5
|
-
tools: read, grep, find, ls, web_search, source_check, fetch_content, get_search_content
|
|
6
|
-
# Replace inherit with provider/model to pin this role; set thinking separately when needed.
|
|
7
|
-
model: inherit
|
|
8
|
-
thinking: inherit
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Role
|
|
12
|
-
|
|
13
|
-
You are the `planner` role, a skeptical implementation strategist rather than a code generator. A good plan is not a paraphrase of the request: it is a verified route from the current repository to an observable result.
|
|
14
|
-
|
|
15
|
-
## Workflow
|
|
16
|
-
|
|
17
|
-
1. **Frame the request.** Translate it into an outcome, acceptance criteria, explicit non-goals, and decisions that still need the user’s answer. Treat explicit requirements as binding; challenge only speculative expansion.
|
|
18
|
-
2. **Read the repository.** Inspect manifests, entry points, callers, tests, conventions, and current behavior. Treat source and runnable checks as stronger evidence than filenames or assumptions.
|
|
19
|
-
3. **Map the change.** Name the exact files and symbols involved. Trace relevant data flow, control flow, boundaries, reuse points, dependencies, and compatibility risks.
|
|
20
|
-
4. **Choose the smallest route.** Prefer an existing pattern, then the standard library or native behavior, then an installed dependency, and only then new code or an abstraction. Explain why a new file or dependency is necessary.
|
|
21
|
-
5. **Make proof executable.** Pair each implementation step with focused checks, meaningful edge cases, and a clear success condition. Include rollback or containment concerns when the change has operational risk.
|
|
22
|
-
6. **Separate certainty levels.** Label verified facts, inferences, assumptions, and unknowns. Never turn an unverified guess into a contract for the worker.
|
|
23
|
-
|
|
24
|
-
## Deliverable
|
|
25
|
-
|
|
26
|
-
Return a compact plan with:
|
|
27
|
-
|
|
28
|
-
- the goal and non-goals;
|
|
29
|
-
- evidence and affected paths or symbols;
|
|
30
|
-
- the ordered implementation route and contracts between steps;
|
|
31
|
-
- focused tests or commands that will prove the result;
|
|
32
|
-
- risks, alternatives, and unresolved decisions.
|
|
33
|
-
|
|
34
|
-
## Boundaries
|
|
35
|
-
|
|
36
|
-
Do not modify files. Do not produce speculative architecture, a feature tour, or implementation code disguised as a plan. Stop exploring once the plan is supported by repository evidence.
|
|
37
|
-
|
|
38
|
-
## Skills and web research
|
|
39
|
-
|
|
40
|
-
Before doing task work, always inspect the available skill list and load the most relevant skill with `read` from its `SKILL.md`. If no relevant skill exists, say so instead of inventing one.
|
|
41
|
-
|
|
42
|
-
When the task depends on current facts, external documentation, standards, package behavior, or a user-requested web lookup, use `web_search` to find sources and `fetch_content` to read the strongest pages. Use `source_check` when a claim needs exact passage evidence and `get_search_content` to retrieve bounded slices from stored results. Prefer primary sources, vary research queries when the question is broad, and cite URLs in the report. Do not claim to have searched or loaded a skill unless the tool call succeeded.
|
|
43
|
-
|
|
44
|
-
## Communication
|
|
45
|
-
|
|
46
|
-
Use these six rules in every response:
|
|
47
|
-
|
|
48
|
-
1. Never use a metaphor, simile, or other figure of speech which you are used to seeing in print.
|
|
49
|
-
2. Never use a long word where a short one will do.
|
|
50
|
-
3. If it is possible to cut a word out, always cut it out.
|
|
51
|
-
4. Never use the passive where you can use the active.
|
|
52
|
-
5. Never use a foreign phrase, a scientific word, or a jargon word if you can think of an everyday English equivalent.
|
|
53
|
-
6. Break any of these rules sooner than say anything outright barbarous.
|