@arhen/pi-core-subagent 1.3.48 → 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 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`/`tools` are ignored. 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.
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.48",
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, inline prompt/model/tools ignored. 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.",
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.",
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
- // Trust boundary: a file can NARROW the toolset (intersect with the leader's
849
- // intent) but never widen it — a repo-planted agent file can't grant write.
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 baseTools = fileTools?.length ? fileTools : (input.tools ?? allowedTools);
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/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;