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 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.6
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.6`, publish after the validation checks pass:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [
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: "Parallel role tasks" })),
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: "Spawn and manage named child threads. Children finish naturally; explicit execution guards, task count, and concurrency are the hard edges. Retention only bounds stored detail. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.",
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
- "Do not request multiple write-capable subagents in one parallel batch.",
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
- if (hasParallel) {
1165
- const writers = inputs.filter((input) => roles.get(input.agent)!.access === "write");
1166
- if (writers.length > 1) throw new Error("Parallel batches may contain at most one write-capable subagent; writers are serialized");
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
- for (let index = 0; index < results.length; index += 1) {
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
- const readIndexes = inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "read");
1397
- const writerIndex = inputs.findIndex((input) => roles.get(input.agent)!.access === "write");
1398
- await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
1399
- if (writerIndex >= 0) await runAt(writerIndex, inputs[writerIndex]!.task);
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));