@arhen/pi-core-subagent 1.3.47 → 1.3.49
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/README.md +1 -1
- package/package.json +1 -1
- package/src/format.ts +4 -2
- package/src/index.ts +3 -3
- package/src/manager.ts +9 -3
- package/src/peek.ts +0 -2
- package/src/types.ts +2 -0
- package/src/worktree.ts +16 -3
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Chain — `{previous}` is replaced with the prior agent's output:
|
|
|
117
117
|
|
|
118
118
|
## Agent files
|
|
119
119
|
|
|
120
|
-
A user agent file in an agents directory is matched by its `description` frontmatter against the spawn goal (`agent` name + `task`) — not by name. When matched, the file is **authoritative**: body = system prompt, frontmatter `model`/`tools` apply, inline `prompt`/`model
|
|
120
|
+
A user agent file in an agents directory is matched by its `description` frontmatter against the spawn goal (`agent` name + `task`) — not by name. When matched, the file is **authoritative**: body = system prompt, frontmatter `model`/`tools` apply, inline `prompt`/`model` are ignored — with one exception: explicit per-call `tools`/`write` override the file's tools (the file narrows defaults, it never displaces explicit intent, and it can never widen past the leader's read/write choice). An override is surfaced on the task's notice and summary. No match → the inline on-demand definition stands. The model stays in control: it names the agent and states the goal; user files that describe that goal take over.
|
|
121
121
|
|
|
122
122
|
```md
|
|
123
123
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/format.ts
CHANGED
|
@@ -243,8 +243,9 @@ export function makeSummary(run: RunSnapshot): string {
|
|
|
243
243
|
const edge = task.needs?.length ? ` (${task.id}, needs ${task.needs.join(", ")})` : ` (${task.id})`;
|
|
244
244
|
const fileNote = task.agentFile ? ` [${task.agentFile}]` : "";
|
|
245
245
|
const swap = task.modelNote ? `\nModel: ${task.modelNote}` : "";
|
|
246
|
+
const tools = task.toolsNote ? `\nTools: ${task.toolsNote}` : "";
|
|
246
247
|
lines.push(
|
|
247
|
-
`\n## ${task.agent}${edge}${fileNote} ${statusIcon(task.status)}${swap}${task.error ? `\nError: ${task.error}` : `\n${truncateText(task.finalText || "(no output)")}`}${worktreeLine(task, run.tasks)}`,
|
|
248
|
+
`\n## ${task.agent}${edge}${fileNote} ${statusIcon(task.status)}${swap}${tools}${task.error ? `\nError: ${task.error}` : `\n${truncateText(task.finalText || "(no output)")}`}${worktreeLine(task, run.tasks)}`,
|
|
248
249
|
);
|
|
249
250
|
}
|
|
250
251
|
// Ceiling on the WHOLE summary — 16 tasks × 24KB would otherwise flood the parent context.
|
|
@@ -267,9 +268,10 @@ export function makeTaskNotice(run: RunSnapshot, task: TaskSnapshot, kind: strin
|
|
|
267
268
|
// in the notice, not only in the run summary the leader may never read.
|
|
268
269
|
const src = task.agentFile ? `\nAgent file: ${task.agentFile}${task.model ? ` (model ${task.model})` : ""}` : "";
|
|
269
270
|
const swap = task.modelNote ? `\nModel: ${task.modelNote}` : "";
|
|
271
|
+
const tools = task.toolsNote ? `\nTools: ${task.toolsNote}` : "";
|
|
270
272
|
return [
|
|
271
273
|
`Task ${task.agent} (${task.id}) ${kind} in run ${run.id}: ${detail}${wt}`,
|
|
272
|
-
`Goal: ${goal}${src}${swap}`,
|
|
274
|
+
`Goal: ${goal}${src}${swap}${tools}`,
|
|
273
275
|
isStartupFailure(task, kind)
|
|
274
276
|
? "Never started — stop and diagnose before spawning anything else: a config-level error (model, plan, auth, agent file) fails identically on every respawn."
|
|
275
277
|
: `Use subagent_result(runId: "${run.id}", taskId: "${task.id}") for full output.`,
|
package/src/index.ts
CHANGED
|
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
157
|
// ponytail: this string is billed on every request. No example block — an example
|
|
158
158
|
// biases the model toward one shape; guidelines + JSON schema describe all of them.
|
|
159
159
|
description:
|
|
160
|
-
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. If a user agent file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) has a `description` matching the spawn goal (name + task), that file is authoritative: body = system prompt, frontmatter `model`/`tools` apply
|
|
160
|
+
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. If a user agent file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) has a `description` matching the spawn goal (name + task), that file is authoritative: body = system prompt, frontmatter `model`/`tools` apply and inline prompt/model are ignored — except explicit per-call `tools`/`write`, which override the file's tools. No match → the inline definition stands. Write agents run in an isolated git worktree: on completion the result reports the branch + changed files — review, then merge with `git merge --no-ff <branch>` (merged branches are cleaned automatically). Every run is background: the call returns a runId immediately and completion notifies you — do NOT park waiting on it. If you have no other work, end your turn; the completion notice wakes you with the results. Set autoAwait:true only when the very next step in the SAME turn consumes the result. Children always carry talk tools: they can ask you questions, notify you, and message siblings.",
|
|
161
161
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
162
162
|
promptGuidelines: [
|
|
163
163
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
@@ -166,7 +166,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
166
166
|
"Prefer flat `tasks` (plain parallel) unless a real dependency exists — only add `needs` edges when ordering genuinely matters.",
|
|
167
167
|
"End each task with a runnable check, e.g. 'Verify: npx tsc --noEmit && bun test'. A subagent's claim of success is not evidence.",
|
|
168
168
|
"For write agents (write:true) in a git repo, the child works in an isolated worktree and its changes are committed to a branch — the result reports branch + changed files. Review the diff, then merge with `git merge --no-ff <branch>`; merged branches are cleaned up automatically. Never leave a worktree branch unmerged at the end of the task.",
|
|
169
|
-
"Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only. A user agent file (`.agents/agents`, `.claude/agents`, `.pi/agents` — project first, then home) whose `description` matches the spawn goal (name + task) takes over: its body is the system prompt, frontmatter `model`/`tools` apply and are validated against the model registry. Matching is by description, not name — name the agent whatever fits the goal.",
|
|
169
|
+
"Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only. A user agent file (`.agents/agents`, `.claude/agents`, `.pi/agents` — project first, then home) whose `description` matches the spawn goal (name + task) takes over: its body is the system prompt, frontmatter `model`/`tools` apply and are validated against the model registry — explicit per-call `tools`/`write` still override the file's tools. Matching is by description, not name — name the agent whatever fits the goal.",
|
|
170
170
|
"Right after a background spawn, call subagent_status(runId) ONCE before any other work — confirm each task is running (or already progressing), not stuck queued or failed at startup. A child that dies on spawn otherwise stays invisible until far later.",
|
|
171
171
|
"If that first status shows a task failed or never started, fix or respawn immediately; do not move on assuming it runs.",
|
|
172
172
|
"Never block with nothing to do: if you have no work left after spawning, end your turn. Task completion notifies you and wakes a fresh turn with the results — await_subagent/autoAwait in that situation only burns time and tokens.",
|
|
@@ -175,7 +175,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
175
175
|
],
|
|
176
176
|
parameters: SubagentParams,
|
|
177
177
|
executionMode: "parallel", // sibling subagent calls run concurrently, not serialized
|
|
178
|
-
async execute(_toolCallId, params,
|
|
178
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
179
179
|
const typed = params as SubagentParamsShape;
|
|
180
180
|
const details = manager.startInBackground(typed, ctx);
|
|
181
181
|
if (typed.autoAwait) {
|
package/src/manager.ts
CHANGED
|
@@ -845,11 +845,17 @@ export class SubagentManager {
|
|
|
845
845
|
if (file?.path) task.agentFile = file.path; // recorded for audit — which file won
|
|
846
846
|
const prompt = file?.body ?? input.prompt?.trim();
|
|
847
847
|
const thinking = input.thinking;
|
|
848
|
-
//
|
|
849
|
-
//
|
|
848
|
+
// File tools are default policy, applied only when the call carries no
|
|
849
|
+
// explicit tool intent: tools: or write: true win over them — silently
|
|
850
|
+
// displacing explicit intent produced read-only children that "completed"
|
|
851
|
+
// with zero edits (issue #3). The gate still holds: a repo-planted file
|
|
852
|
+
// can never WIDEN past the leader's read/write choice (filtered above).
|
|
850
853
|
const allowedTools = input.write ? WRITE_TOOLS : READONLY_TOOLS;
|
|
851
854
|
const fileTools = file?.tools?.filter((t) => allowedTools.includes(t));
|
|
852
|
-
const
|
|
855
|
+
const explicitTools = input.tools ?? (input.write ? WRITE_TOOLS : undefined);
|
|
856
|
+
const baseTools = explicitTools ?? (fileTools?.length ? fileTools : allowedTools);
|
|
857
|
+
if (explicitTools && fileTools?.length)
|
|
858
|
+
task.toolsNote = `explicit tools overrode agent-file tools (${fileTools.join(", ")})`;
|
|
853
859
|
const tools = [...baseTools, ...CHILD_TALK_TOOLS];
|
|
854
860
|
// Isolation follows the DELIVERED toolset, never the raw request: explicit
|
|
855
861
|
// tools: [bash] without write:true still gets a worktree, and a file that
|
package/src/peek.ts
CHANGED
|
@@ -48,9 +48,7 @@ function stripThinking(text: string): string {
|
|
|
48
48
|
.trim();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping real terminal escapes is the point
|
|
52
51
|
const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
53
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: same
|
|
54
52
|
const CONTROL = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
55
53
|
|
|
56
54
|
/** Tool output is terminal output: it carries colour escapes and carriage returns
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface TaskSnapshot {
|
|
|
39
39
|
/** Why `model` is not what was requested: preflight failed and the session's
|
|
40
40
|
* model took over. Silent substitution is worse than a slow spawn. */
|
|
41
41
|
modelNote?: string;
|
|
42
|
+
/** Set when explicit per-call tools/write displaced a matched file's tools. */
|
|
43
|
+
toolsNote?: string;
|
|
42
44
|
thinking?: string;
|
|
43
45
|
tools?: string[];
|
|
44
46
|
usage: UsageStats;
|
package/src/worktree.ts
CHANGED
|
@@ -392,10 +392,23 @@ export function ownerAlive(path: string, ownedHere?: (path: string) => boolean):
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
/** Stable per-boot id, so a recycled pid from before a reboot can't look alive.
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* live marker as dead
|
|
395
|
+
* Prefer the OS's own boot identity — the clock formula (Date.now - uptime)
|
|
396
|
+
* breaks on NTP-stepped clocks (CI runners): the step flips the floor and a
|
|
397
|
+
* live marker suddenly reads as pre-reboot/dead. The formula stays as the
|
|
398
|
+
* last-resort fallback: one floor on the expressed seconds, not two —
|
|
399
|
+
* separate floors of walltime and uptime flip by ±1 around integer
|
|
400
|
+
* boundaries and would read a live marker as dead on a cross-second read. */
|
|
398
401
|
function bootId(): string {
|
|
402
|
+
try {
|
|
403
|
+
if (process.platform === "linux") return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
404
|
+
if (process.platform === "darwin") {
|
|
405
|
+
// kern.boottime = "{ sec = 1756…, usec = … }" — sec alone is stable per boot.
|
|
406
|
+
const out = execFileSync("sysctl", ["-n", "kern.boottime"], { encoding: "utf8" });
|
|
407
|
+
return out.match(/sec = (\d+)/)?.[1] ?? out.trim();
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
/* fall through to the formula */
|
|
411
|
+
}
|
|
399
412
|
return String(Math.floor((Date.now() - uptime() * 1000) / 1000));
|
|
400
413
|
}
|
|
401
414
|
|