killeros 1.4.6 → 1.4.7
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 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/subagents.ts +49 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.4.7] - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Serialized every write-capable task in a parallel batch in input order instead of rejecting batches with multiple writers.
|
|
10
|
+
- Settled queued tasks on interrupted parallel batches and documented the shared-worktree execution model.
|
|
11
|
+
- Restricted the `message` parameter to `action: "steer"` and added focused regression coverage.
|
|
12
|
+
|
|
5
13
|
## [1.4.6] - 2026-08-01
|
|
6
14
|
|
|
7
15
|
### Fixed
|
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.7
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -97,7 +97,7 @@ 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}`. 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. For example:
|
|
100
|
+
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. Parallel batches run read-only roles concurrently, up to four at a time, then run every write-capable role serially in input order because all children share the parent worktree. 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"}
|
|
@@ -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.7`, publish after the validation checks pass:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
npm login
|
package/package.json
CHANGED
package/subagents.ts
CHANGED
|
@@ -113,6 +113,7 @@ export interface SubagentDetails {
|
|
|
113
113
|
mode: "single" | "parallel" | "chain";
|
|
114
114
|
agentScope: AgentScope;
|
|
115
115
|
projectAgentsDir: string | null;
|
|
116
|
+
executionNote?: string;
|
|
116
117
|
results: SubagentTaskResult[];
|
|
117
118
|
aggregateUsage: SubagentUsage;
|
|
118
119
|
parentId?: string;
|
|
@@ -712,7 +713,7 @@ async function mapReadTasks<T>(items: T[], concurrency: number, run: (item: T, i
|
|
|
712
713
|
await Promise.all(workers);
|
|
713
714
|
}
|
|
714
715
|
|
|
715
|
-
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "taskCharacters">) {
|
|
716
|
+
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
716
717
|
const taskSchema = Type.Object({
|
|
717
718
|
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
718
719
|
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task for the role" }),
|
|
@@ -727,11 +728,11 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "taskCha
|
|
|
727
728
|
description: "Thread lifecycle action",
|
|
728
729
|
})),
|
|
729
730
|
threadId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Stable child thread ID" })),
|
|
730
|
-
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "Bounded steering message" })),
|
|
731
|
+
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "Bounded steering message; only valid with action steer" })),
|
|
731
732
|
all: Type.Optional(Type.Boolean({ description: "Interrupt every active child thread" })),
|
|
732
733
|
agent: Type.Optional(Type.String({ minLength: 1, maxLength: 64, description: "Agent role for single mode" })),
|
|
733
734
|
task: Type.Optional(Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task for single mode" })),
|
|
734
|
-
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description:
|
|
735
|
+
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: `Parallel role tasks: read-only roles run concurrently up to ${limits.maxReadConcurrency}; write-capable roles run serially in input order because all children share the parent worktree` })),
|
|
735
736
|
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
736
737
|
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Model for every task as provider/model; inherit uses each role setting or the active parent" })),
|
|
737
738
|
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "Thinking effort for every task: off, minimal, low, medium, high, xhigh, max, or inherit" })),
|
|
@@ -1029,11 +1030,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1029
1030
|
pi.registerTool({
|
|
1030
1031
|
name: "subagent",
|
|
1031
1032
|
label: "Subagents",
|
|
1032
|
-
description:
|
|
1033
|
+
description: `Spawn and manage named child threads. Children finish naturally. Parallel tasks run read-only roles concurrently up to ${limits.maxReadConcurrency}, then run write-capable roles serially in input order because all children share the parent worktree. The message parameter is only valid with action steer. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.`,
|
|
1033
1034
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
1034
1035
|
promptGuidelines: [
|
|
1035
1036
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
1036
|
-
|
|
1037
|
+
`Parallel tasks run read-only roles concurrently up to ${limits.maxReadConcurrency}, then queue write-capable roles in input order because all children share the parent worktree.`,
|
|
1037
1038
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
1038
1039
|
"When the user names a model or thinking effort, pass model and thinking separately; use inherit when the active parent or role setting should decide.",
|
|
1039
1040
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
@@ -1043,6 +1044,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1043
1044
|
|
|
1044
1045
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1045
1046
|
const action = params.action ?? "spawn";
|
|
1047
|
+
if (params.message !== undefined && action !== "steer") {
|
|
1048
|
+
throw new Error("message is only valid with action steer");
|
|
1049
|
+
}
|
|
1046
1050
|
const parentId = parentThreadId(ctx);
|
|
1047
1051
|
const actionDetails = (selectedThreadId?: string): SubagentDetails => detailsFor(parentId, "single", params.agentScope ?? "user", null, selectedThreadId);
|
|
1048
1052
|
const actionResult = (text: string, selectedThreadId?: string) => {
|
|
@@ -1161,10 +1165,17 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1161
1165
|
? [{ agent: params.agent!, task: params.task! }]
|
|
1162
1166
|
: hasParallel ? params.tasks! : params.chain!;
|
|
1163
1167
|
if (inputs.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
+
const readIndexes = hasParallel
|
|
1169
|
+
? inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "read")
|
|
1170
|
+
: [];
|
|
1171
|
+
const writerIndexes = hasParallel
|
|
1172
|
+
? inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "write").map(({ index }) => index)
|
|
1173
|
+
: [];
|
|
1174
|
+
const executionNote = hasParallel
|
|
1175
|
+
? writerIndexes.length
|
|
1176
|
+
? `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}; write-capable tasks are queued (serialized) in input order because all children share the parent worktree.`
|
|
1177
|
+
: `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}.`
|
|
1178
|
+
: undefined;
|
|
1168
1179
|
|
|
1169
1180
|
const inFlight = threads.listAll().filter((thread) => ["queued", "active"].includes(thread.state)).length;
|
|
1170
1181
|
if (inFlight + inputs.length > limits.maxTasks) {
|
|
@@ -1187,7 +1198,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1187
1198
|
const currentResults = results.map(cloneResult);
|
|
1188
1199
|
(onUpdate as ToolUpdate | undefined)?.({
|
|
1189
1200
|
content: [{ type: "text", text: message }],
|
|
1190
|
-
details: { ...board, results: currentResults, aggregateUsage: aggregateUsage(currentResults) },
|
|
1201
|
+
details: { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) },
|
|
1191
1202
|
});
|
|
1192
1203
|
};
|
|
1193
1204
|
const runAt = async (index: number, task: string): Promise<void> => {
|
|
@@ -1368,6 +1379,23 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1368
1379
|
emit();
|
|
1369
1380
|
};
|
|
1370
1381
|
|
|
1382
|
+
const settleQueued = (reason: string): void => {
|
|
1383
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
1384
|
+
const result = results[index]!;
|
|
1385
|
+
if (result.status !== "queued") continue;
|
|
1386
|
+
const thread = threads.inspect(threadRecords[index]!.id);
|
|
1387
|
+
const alreadyStopped = thread?.state === "stopped";
|
|
1388
|
+
result.status = signal?.aborted || alreadyStopped ? "cancelled" : "failed";
|
|
1389
|
+
result.terminationReason = alreadyStopped
|
|
1390
|
+
? thread.stopReason ?? "interrupted"
|
|
1391
|
+
: signal?.aborted ? "abort" : reason;
|
|
1392
|
+
if (thread?.state === "queued" || thread?.state === "active") {
|
|
1393
|
+
threads.stop(threadRecords[index]!.id, { reason: result.terminationReason });
|
|
1394
|
+
}
|
|
1395
|
+
savedResults.set(threadRecords[index]!.id, cloneResult(result));
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1371
1399
|
emit(`${mode}: ${results.length} queued`);
|
|
1372
1400
|
if (hasChain) {
|
|
1373
1401
|
let previous = "";
|
|
@@ -1377,33 +1405,21 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1377
1405
|
if (results[index]!.status !== "complete") break;
|
|
1378
1406
|
previous = results[index]!.output;
|
|
1379
1407
|
}
|
|
1380
|
-
|
|
1381
|
-
const result = results[index]!;
|
|
1382
|
-
if (result.status === "queued") {
|
|
1383
|
-
const thread = threads.inspect(threadRecords[index]!.id);
|
|
1384
|
-
const alreadyStopped = thread?.state === "stopped";
|
|
1385
|
-
result.status = signal?.aborted || alreadyStopped ? "cancelled" : "failed";
|
|
1386
|
-
result.terminationReason = alreadyStopped
|
|
1387
|
-
? thread.stopReason ?? "interrupted"
|
|
1388
|
-
: signal?.aborted ? "abort" : "chain_stopped";
|
|
1389
|
-
if (thread?.state === "queued" || thread?.state === "active") {
|
|
1390
|
-
threads.stop(threadRecords[index]!.id, { reason: result.terminationReason });
|
|
1391
|
-
}
|
|
1392
|
-
savedResults.set(threadRecords[index]!.id, cloneResult(result));
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1408
|
+
settleQueued("chain_stopped");
|
|
1395
1409
|
} else if (hasParallel) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1410
|
+
try {
|
|
1411
|
+
await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
|
|
1412
|
+
for (const index of writerIndexes) await runAt(index, inputs[index]!.task);
|
|
1413
|
+
} finally {
|
|
1414
|
+
settleQueued("parallel_stopped");
|
|
1415
|
+
}
|
|
1400
1416
|
} else {
|
|
1401
1417
|
await runAt(0, inputs[0]!.task);
|
|
1402
1418
|
}
|
|
1403
1419
|
|
|
1404
1420
|
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1405
1421
|
const currentResults = results.map(cloneResult);
|
|
1406
|
-
const details: SubagentDetails = { ...board, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
1422
|
+
const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
1407
1423
|
return {
|
|
1408
1424
|
content: [{ type: "text", text: buildToolContent(mode, details.results, limits.toolOutputBytes) }],
|
|
1409
1425
|
details,
|
|
@@ -1414,7 +1430,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1414
1430
|
renderCall(args, theme) {
|
|
1415
1431
|
const scope = args.agentScope ?? "user";
|
|
1416
1432
|
if (args.action && args.action !== "spawn") return new Text(`${theme.fg("toolTitle", theme.bold("threads "))}${theme.fg("accent", args.action)}${theme.fg("dim", args.threadId ? ` · ${args.threadId}` : "")}`, 0, 0);
|
|
1417
|
-
if (args.tasks?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${args.tasks.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1433
|
+
if (args.tasks?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${args.tasks.length} · readers first; writers serial`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1418
1434
|
if (args.chain?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `chain ${args.chain.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1419
1435
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "…")}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1420
1436
|
},
|
|
@@ -1437,6 +1453,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1437
1453
|
theme.fg("toolTitle", theme.bold(`Done (${board.done.length})`)),
|
|
1438
1454
|
...board.done.map((task) => `${theme.fg(task.state.status === "complete" ? "success" : "warning", `${task.state.label}`)} ${theme.fg("toolTitle", theme.bold(task.agent))}${theme.fg("dim", ` · ${task.id} · ${task.usage.text}`)}`),
|
|
1439
1455
|
];
|
|
1456
|
+
if (details.executionNote) lines.push(theme.fg("dim", details.executionNote));
|
|
1440
1457
|
lines.push(theme.fg("dim", `Total · ${formatUsage(details.aggregateUsage)} · Ctrl+O to expand`));
|
|
1441
1458
|
return new Text(lines.join("\n"), 0, 0);
|
|
1442
1459
|
}
|
|
@@ -1444,6 +1461,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1444
1461
|
const container = new Container();
|
|
1445
1462
|
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`Subagents · ${details.mode}`)), 0, 0));
|
|
1446
1463
|
container.addChild(new Text(theme.fg("dim", `Active ${board.active.length} · Done ${board.done.length} · Controls: Inspect · Steer · Interrupt · Collect · Close`), 0, 0));
|
|
1464
|
+
if (details.executionNote) container.addChild(new Text(theme.fg("dim", details.executionNote), 0, 0));
|
|
1447
1465
|
if (board.selected) {
|
|
1448
1466
|
const inspection = formatThreadInspection(threadBoardRecord(details.results.find((task) => task.id === board.selected!.id)!));
|
|
1449
1467
|
container.addChild(new Spacer(1));
|