killeros 1.4.7 → 1.4.9
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 +11 -2
- package/Killeros.ts +335 -70
- package/README.md +8 -8
- package/package.json +6 -6
- package/subagent-lifecycle.ts +27 -1
- package/subagent-process.ts +26 -2
- package/subagents.ts +241 -49
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
|
33
33
|
Pin an install to a release:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.
|
|
36
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.9
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -97,13 +97,13 @@ KillerOS ships `planner`, `reviewer`, `scout`, and `security` as read-only roles
|
|
|
97
97
|
|
|
98
98
|
The default `agentScope: "user"` uses bundled and personal roles. Use `"project"` or `"both"` to opt into trusted project roles; a selected project override requires interactive confirmation. Role frontmatter requires `name`, `description`, `access`, and an explicit `tools` list. Optional fields are `model`, `thinking`, and `timeoutMs`. Every bundled role shows `model: inherit` and `thinking: inherit` as editable placeholders. Replace them with an available `provider/model` and a separate thinking level when you want to pin a role; `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max` are checked against that model’s supported capabilities.
|
|
99
99
|
|
|
100
|
-
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`.
|
|
100
|
+
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. Read-only-only batches run concurrently, up to four at a time. Batches with write-capable roles use one shared slot by default; set `writerConcurrency` above `1` only after proving path ownership in the shared worktree. Reader-only batches reject `writerConcurrency` because it does not apply. All children share the parent worktree, so concurrent writers must avoid file conflicts. A call can also set `model` and `thinking` for every task, overriding role settings; use `inherit` to fall back to each role and then the active parent model. The `message` field is only valid with `action: "steer"`. For example:
|
|
101
101
|
|
|
102
102
|
```json
|
|
103
103
|
{"agent":"reviewer","task":"Review the change","model":"provider/model","thinking":"high"}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. Children have no default token, dollar, turn, tool-call, research, wall-time,
|
|
106
|
+
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. Children have no default token, dollar, turn, tool-call, research, wall-time, trace, stderr, or returned-output execution quota; each JSONL record still has a bounded 8 MiB parser ceiling. KillerOS bounds retained trace, stderr, and returned text and spills a large JSONL line to temporary storage; retention never stops a child or marks it `limited`. The parent limits each request to ten tasks, read-only-only batches to four concurrent readers, and bounds role files, task input, and combined parent output. An embedding caller may opt into named child resource guards. A parent tool-call abort cancels queued tasks but lets already-running children finish; explicit `interrupt` actions and session shutdown terminate active children and escalate after five seconds.
|
|
107
107
|
|
|
108
108
|
### Thread lifecycle
|
|
109
109
|
|
|
@@ -111,9 +111,9 @@ Each delegated task creates a named child thread. Its contract records the paren
|
|
|
111
111
|
|
|
112
112
|
Threads move through `queued`, `active`, `done`, `failed`, `stopped`, and `closed`. The parent renders separate **Active** and **Done** lists. Active threads show their name, task, model, usage, and direct controls. Done threads keep their handoff and trace available until the parent closes them.
|
|
113
113
|
|
|
114
|
-
The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; steer an active thread with one bounded follow-up; interrupt one child or all active children; collect a concise handoff into parent context; and close a finished or stopped thread. An interrupt preserves the partial trace, states the reason, and reports the handoff as partial rather than successful.
|
|
114
|
+
The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; steer an active thread with one bounded follow-up; interrupt one child or all active children; collect a concise handoff into parent context; and close a finished or stopped thread. An interrupt preserves the partial trace, states the reason, and reports the handoff as partial rather than successful. Closing removes a thread from the active workspace; heavy trace and result payloads are evicted as needed under the bounded retention budget, leaving a small inspectable tombstone.
|
|
115
115
|
|
|
116
|
-
A child completes naturally when it returns a final answer. The default path has no per-child execution quota. Explicit embedding options can add wall-time, output, trace, stderr, JSONL, token, or cost guards; those guards report their cause and return partial work clearly. The parent still bounds task count, reader concurrency, role files, task input, and combined parent output.
|
|
116
|
+
A child completes naturally when it returns a final answer. The default path has no per-child execution quota, while every JSONL record has an 8 MiB parser ceiling. Explicit embedding options can add wall-time, output, trace, stderr, JSONL, token, or cost guards; those guards report their cause and return partial work clearly. The parent still bounds task count, reader concurrency, role files, task input, and combined parent output. Parent tool-call aborts leave active children running while queued work is settled as cancelled; explicit `interrupt` actions and real child-process failures remain visible. Session shutdown still terminates active children and escalates after five seconds.
|
|
117
117
|
|
|
118
118
|
The replacement lifecycle has nine phases:
|
|
119
119
|
|
|
@@ -124,8 +124,8 @@ The replacement lifecycle has nine phases:
|
|
|
124
124
|
5. **Interrupt:** stop one or all active children while preserving partial work.
|
|
125
125
|
6. **Collect:** return a concise handoff while retaining the expanded trace.
|
|
126
126
|
7. **Guard:** honor only explicitly configured child resource guards; do not impose a routine turn stop.
|
|
127
|
-
8. **Close:** remove a finished or stopped thread from the workspace
|
|
128
|
-
9. **Prove:** test identity, visibility, controls, natural completion, guards, partial handoffs, and closure.
|
|
127
|
+
8. **Close:** remove a finished or stopped thread from the workspace while retaining a small inspectable tombstone; heavy payloads may be evicted under the retention budget.
|
|
128
|
+
9. **Prove:** test identity, visibility, controls, natural completion, guards, partial handoffs, bounded retention, and closure.
|
|
129
129
|
|
|
130
130
|
## Configuration
|
|
131
131
|
|
|
@@ -163,7 +163,7 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
163
163
|
|
|
164
164
|
The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
|
|
165
165
|
|
|
166
|
-
For release `1.4.
|
|
166
|
+
For release `1.4.9`, publish after the validation checks pass:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
npm login
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
4
4
|
"description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@earendil-works/pi-ai": "
|
|
50
|
-
"@earendil-works/pi-coding-agent": "
|
|
51
|
-
"@earendil-works/pi-tui": "
|
|
52
|
-
"pi-web-access": "
|
|
53
|
-
"typebox": "
|
|
49
|
+
"@earendil-works/pi-ai": ">=0.82.1",
|
|
50
|
+
"@earendil-works/pi-coding-agent": ">=0.82.1",
|
|
51
|
+
"@earendil-works/pi-tui": ">=0.82.1",
|
|
52
|
+
"pi-web-access": ">=0.17.1",
|
|
53
|
+
"typebox": ">=1.1.38 <2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@earendil-works/pi-coding-agent": "0.82.1",
|
package/subagent-lifecycle.ts
CHANGED
|
@@ -97,6 +97,8 @@ export interface SubagentThread extends SubagentThreadSpec {
|
|
|
97
97
|
result?: string;
|
|
98
98
|
failure?: { message: string; code?: string };
|
|
99
99
|
stopReason?: string;
|
|
100
|
+
/** True when close evicted the heavy trace, prompt, handoff, and result fields. */
|
|
101
|
+
evicted: boolean;
|
|
100
102
|
timestamps: SubagentThreadTimestamps;
|
|
101
103
|
version: number;
|
|
102
104
|
}
|
|
@@ -192,6 +194,7 @@ function snapshot(thread: SubagentThread): SubagentThread {
|
|
|
192
194
|
result: thread.result,
|
|
193
195
|
failure: thread.failure ? { ...thread.failure } : undefined,
|
|
194
196
|
stopReason: thread.stopReason,
|
|
197
|
+
evicted: thread.evicted,
|
|
195
198
|
timestamps: { ...thread.timestamps },
|
|
196
199
|
version: thread.version,
|
|
197
200
|
};
|
|
@@ -287,6 +290,7 @@ export class SubagentThreadRegistry {
|
|
|
287
290
|
usage: emptyUsage(),
|
|
288
291
|
trace: [],
|
|
289
292
|
steering: [],
|
|
293
|
+
evicted: false,
|
|
290
294
|
timestamps: { createdAt: timestamp, updatedAt: timestamp },
|
|
291
295
|
version: 1,
|
|
292
296
|
};
|
|
@@ -413,7 +417,7 @@ export class SubagentThreadRegistry {
|
|
|
413
417
|
return snapshot(thread);
|
|
414
418
|
}
|
|
415
419
|
|
|
416
|
-
/** Closes a terminal record
|
|
420
|
+
/** Closes a terminal record and retains only a small tombstone for inspection. */
|
|
417
421
|
close(id: SubagentThreadId): SubagentThread {
|
|
418
422
|
this.assertOpen();
|
|
419
423
|
const thread = this.requireThread(id);
|
|
@@ -421,10 +425,32 @@ export class SubagentThreadRegistry {
|
|
|
421
425
|
if (!isTerminal(thread.state)) throw new Error(`Cannot close thread ${id} from ${thread.state}`);
|
|
422
426
|
thread.state = "closed";
|
|
423
427
|
thread.timestamps.closedAt = this.now();
|
|
428
|
+
thread.prompt = "[closed thread prompt evicted]";
|
|
429
|
+
thread.handoff = undefined;
|
|
430
|
+
thread.trace = [];
|
|
431
|
+
thread.steering = [];
|
|
432
|
+
thread.result = undefined;
|
|
433
|
+
thread.failure = thread.failure ? { message: thread.failure.message.slice(0, 512), code: thread.failure.code } : undefined;
|
|
434
|
+
thread.evicted = true;
|
|
424
435
|
this.changed(thread, "close");
|
|
425
436
|
return snapshot(thread);
|
|
426
437
|
}
|
|
427
438
|
|
|
439
|
+
/** Remove the oldest closed tombstones and return bounded eviction notices. */
|
|
440
|
+
pruneClosed(maxRecords: number): SubagentThread[] {
|
|
441
|
+
this.assertOpen();
|
|
442
|
+
const closed = [...this.threads.values()]
|
|
443
|
+
.filter((thread) => thread.state === "closed")
|
|
444
|
+
.sort((left, right) => (left.timestamps.closedAt ?? left.timestamps.updatedAt) - (right.timestamps.closedAt ?? right.timestamps.updatedAt));
|
|
445
|
+
const removed: SubagentThread[] = [];
|
|
446
|
+
while (closed.length > Math.max(0, Math.floor(maxRecords))) {
|
|
447
|
+
const thread = closed.shift()!;
|
|
448
|
+
this.threads.delete(thread.id);
|
|
449
|
+
removed.push(snapshot(thread));
|
|
450
|
+
}
|
|
451
|
+
return removed;
|
|
452
|
+
}
|
|
453
|
+
|
|
428
454
|
subscribe(listener: SubagentThreadListener): () => void {
|
|
429
455
|
if (this.disposed) return () => {};
|
|
430
456
|
this.listeners.add(listener);
|
package/subagent-process.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, statSync, write
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
+
export const MAX_NODE_TIMER_MS = 2_147_483_647;
|
|
7
|
+
|
|
6
8
|
export const SUBAGENT_PROCESS_LIMITS = {
|
|
9
|
+
jsonlLineBytes: 8 * 1024 * 1024,
|
|
7
10
|
killGraceMs: 5_000,
|
|
8
11
|
} as const;
|
|
9
12
|
|
|
@@ -96,6 +99,10 @@ export interface SubagentProcessRetention {
|
|
|
96
99
|
|
|
97
100
|
export interface SubagentProcessHandle {
|
|
98
101
|
readonly pid: number | undefined;
|
|
102
|
+
/** True after the child close event, or when no child was spawned. */
|
|
103
|
+
readonly hasExited: boolean;
|
|
104
|
+
/** Resolves after the child close event, or when no child was spawned. */
|
|
105
|
+
readonly exited: Promise<void>;
|
|
99
106
|
readonly result: Promise<SubagentProcessResult>;
|
|
100
107
|
/** Stop this child and retain any work received before it exits. */
|
|
101
108
|
stop(reason?: string): void;
|
|
@@ -215,7 +222,10 @@ function normalizeLimits(overrides: Partial<SubagentProcessLimits> | undefined):
|
|
|
215
222
|
const limits = { ...SUBAGENT_PROCESS_LIMITS, ...overrides };
|
|
216
223
|
for (const name of ["wallTimeMs", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs"] as const) {
|
|
217
224
|
const value = limits[name];
|
|
218
|
-
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0
|
|
225
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0 || value > MAX_NODE_TIMER_MS && (name === "wallTimeMs" || name === "killGraceMs"))) {
|
|
226
|
+
const bound = name === "wallTimeMs" || name === "killGraceMs" ? ` no greater than ${MAX_NODE_TIMER_MS}` : "";
|
|
227
|
+
throw new RangeError(`${name} must be a positive safe integer${bound}`);
|
|
228
|
+
}
|
|
219
229
|
}
|
|
220
230
|
for (const name of ["quotaTokens", "quotaUsd"] as const) {
|
|
221
231
|
const value = limits[name];
|
|
@@ -324,6 +334,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
324
334
|
durationMs: 0,
|
|
325
335
|
};
|
|
326
336
|
let child: SubagentProcessChild | undefined;
|
|
337
|
+
let processExited = false;
|
|
327
338
|
let closed = false;
|
|
328
339
|
let finishing = false;
|
|
329
340
|
let requestedStatus: Exclude<SubagentProcessStatus, "running" | "complete"> | undefined;
|
|
@@ -338,7 +349,14 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
338
349
|
let settleTimer: NodeJS.Timeout | undefined;
|
|
339
350
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
340
351
|
let resolveResult!: (result: SubagentProcessResult) => void;
|
|
352
|
+
let resolveExited!: () => void;
|
|
341
353
|
const result = new Promise<SubagentProcessResult>((resolve) => { resolveResult = resolve; });
|
|
354
|
+
const exited = new Promise<void>((resolve) => { resolveExited = resolve; });
|
|
355
|
+
const markExited = (): void => {
|
|
356
|
+
if (processExited) return;
|
|
357
|
+
processExited = true;
|
|
358
|
+
resolveExited();
|
|
359
|
+
};
|
|
342
360
|
|
|
343
361
|
const clearStdoutLine = (): void => {
|
|
344
362
|
if (stdoutLineSpoolDescriptor !== undefined) {
|
|
@@ -385,6 +403,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
385
403
|
const finish = (code: number | null): void => {
|
|
386
404
|
if (closed || finishing) return;
|
|
387
405
|
finishing = true;
|
|
406
|
+
if (!child || processExited) markExited();
|
|
388
407
|
if (stdoutLineBytes && !requestedStatus) processLine(readStdoutLine());
|
|
389
408
|
else clearStdoutLine();
|
|
390
409
|
closed = true;
|
|
@@ -559,7 +578,10 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
559
578
|
}
|
|
560
579
|
});
|
|
561
580
|
child.on("error", (error) => requestTermination("failed", "spawn_error", error.message));
|
|
562
|
-
child.once("close",
|
|
581
|
+
child.once("close", (code) => {
|
|
582
|
+
markExited();
|
|
583
|
+
finish(code);
|
|
584
|
+
});
|
|
563
585
|
if (limits.wallTimeMs !== undefined) timeoutTimer = setTimeout(() => requestTermination("limited", "wall_time_limit"), limits.wallTimeMs);
|
|
564
586
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
565
587
|
if (options.signal?.aborted) abortHandler();
|
|
@@ -570,6 +592,8 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
570
592
|
|
|
571
593
|
return {
|
|
572
594
|
get pid() { return child?.pid; },
|
|
595
|
+
get hasExited() { return processExited; },
|
|
596
|
+
exited,
|
|
573
597
|
result,
|
|
574
598
|
stop(reason = "stopped") { requestTermination("cancelled", reason); },
|
|
575
599
|
snapshot: () => cloneResult(state),
|