@nredd/pi-subagents 0.22.0-nredd.8 → 0.22.0-nredd.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.
Files changed (50) hide show
  1. package/README.md +136 -55
  2. package/package.json +14 -11
  3. package/src/abortable.ts +1 -1
  4. package/src/agent-file-toggle.ts +0 -1
  5. package/src/agent-manager.ts +360 -103
  6. package/src/agent-runner.ts +121 -63
  7. package/src/child-context.ts +19 -1
  8. package/src/completion-outbox.ts +22 -3
  9. package/src/cross-extension-rpc.ts +10 -6
  10. package/src/custom-agents.ts +2 -1
  11. package/src/default-agents.ts +19 -17
  12. package/src/detached-results.ts +18 -2
  13. package/src/enterprise-local.ts +2 -1
  14. package/src/error-message.ts +11 -0
  15. package/src/git-path.ts +66 -0
  16. package/src/group-join.ts +4 -10
  17. package/src/index.ts +731 -231
  18. package/src/invocation-config.ts +1 -5
  19. package/src/mention-clone.ts +4 -3
  20. package/src/model-resolver.ts +51 -14
  21. package/src/model-selection.ts +17 -6
  22. package/src/model-tiers.ts +5 -0
  23. package/src/nested-tools.ts +80 -21
  24. package/src/notification-result.ts +118 -0
  25. package/src/plan-mode-children.ts +176 -0
  26. package/src/prompts.ts +7 -2
  27. package/src/quota-admission.ts +16 -6
  28. package/src/resource-loader.ts +20 -6
  29. package/src/schedule.ts +65 -19
  30. package/src/settings.ts +21 -1
  31. package/src/skill-loader.ts +244 -94
  32. package/src/status-note.ts +28 -5
  33. package/src/types.ts +52 -2
  34. package/src/ui/agent-mention.ts +5 -2
  35. package/src/ui/agent-widget.ts +41 -22
  36. package/src/ui/conversation-viewer.ts +4 -2
  37. package/src/ui/fallback-flag.ts +28 -4
  38. package/src/ui/fleet-list.ts +89 -54
  39. package/src/ui/notification-view.ts +4 -1
  40. package/src/ui/workflow-card.ts +1 -8
  41. package/src/ui/workflow-dialog.ts +31 -18
  42. package/src/workflow/host.ts +4 -5
  43. package/src/workflow/meta.ts +7 -4
  44. package/src/workflow/progress.ts +4 -3
  45. package/src/workflow/runtime.ts +23 -7
  46. package/src/workflow/saved.ts +2 -4
  47. package/src/workflow/task.ts +12 -7
  48. package/src/workflow/tool-description.ts +2 -2
  49. package/src/workflow/worker-source.ts +6 -2
  50. package/src/worktree.ts +309 -19
@@ -6,7 +6,17 @@
6
6
 
7
7
  import type { AgentConfig } from "./types.js";
8
8
 
9
- const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"];
9
+ /**
10
+ * Tool surface shared by Explore and Plan. `bash` is left out because a prompt cannot
11
+ * make an unrestricted shell read-only, and extensions are off because extension tools
12
+ * can run commands or write. A `.pi/agents/<Name>.md` override can opt back in with an
13
+ * explicit `extensions:` list.
14
+ */
15
+ const READ_ONLY_SURFACE: Pick<AgentConfig, "builtinToolNames" | "extensions" | "skills"> = {
16
+ builtinToolNames: ["read", "grep", "find", "ls"],
17
+ extensions: false,
18
+ skills: true,
19
+ };
10
20
 
11
21
  export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
12
22
  [
@@ -31,9 +41,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
31
41
  name: "Explore",
32
42
  displayName: "Explore",
33
43
  description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions.",
34
- builtinToolNames: READ_ONLY_TOOLS,
35
- extensions: true,
36
- skills: true,
44
+ ...READ_ONLY_SURFACE,
37
45
  // Bind the role per machine, not to a provider or model generation.
38
46
  model: "@cheap",
39
47
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
@@ -49,13 +57,10 @@ You are STRICTLY PROHIBITED from:
49
57
  - Using redirect operators (>, >>, |) or heredocs to write to files
50
58
  - Running ANY commands that change system state
51
59
 
52
- Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
53
-
54
60
  # Tool Usage
55
- - Use the find tool for file pattern matching (NOT the bash find command)
56
- - Use the grep tool for content search (NOT bash grep/rg command)
57
- - Use the read tool for reading files (NOT bash cat/head/tail)
58
- - Use Bash ONLY for read-only operations
61
+ - Use the find tool for file pattern matching
62
+ - Use the grep tool for content search
63
+ - Use the read tool for reading files
59
64
  - Make independent tool calls in parallel for efficiency
60
65
  - Adapt search approach based on thoroughness level specified
61
66
 
@@ -74,9 +79,7 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
74
79
  name: "Plan",
75
80
  displayName: "Plan",
76
81
  description: "Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.",
77
- builtinToolNames: READ_ONLY_TOOLS,
78
- extensions: true,
79
- skills: true,
82
+ ...READ_ONLY_SURFACE,
80
83
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
81
84
  You are a software architect and planning specialist.
82
85
  Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
@@ -104,10 +107,9 @@ You are STRICTLY PROHIBITED from:
104
107
  - Follow existing patterns where appropriate
105
108
 
106
109
  # Tool Usage
107
- - Use the find tool for file pattern matching (NOT the bash find command)
108
- - Use the grep tool for content search (NOT bash grep/rg command)
109
- - Use the read tool for reading files (NOT bash cat/head/tail)
110
- - Use Bash ONLY for read-only operations
110
+ - Use the find tool for file pattern matching
111
+ - Use the grep tool for content search
112
+ - Use the read tool for reading files
111
113
 
112
114
  # Output Format
113
115
  - Use absolute file paths
@@ -13,7 +13,9 @@ import { createHash } from "node:crypto";
13
13
  import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
- import type { AgentRecord } from "./types.js";
16
+ import type { EvictedNote } from "./agent-manager.js";
17
+ import type { NoticeParts } from "./notification-result.js";
18
+ import type { AgentRecord, AgentTombstone } from "./types.js";
17
19
  import type { AgentActivity } from "./ui/agent-widget.js";
18
20
 
19
21
  /** One finished agent, in the shape `session_start` needs to re-announce it. */
@@ -25,6 +27,14 @@ export interface DetachedResult {
25
27
  details: unknown;
26
28
  /** History entry for a result that finished detached; absent if already appended. */
27
29
  record?: Record<string, unknown>;
30
+ /** The `subagents:completed`/`subagents:failed` event the live path would have emitted, replayed on delivery. */
31
+ lifecycle?: { channel: "subagents:completed" | "subagents:failed"; data: Record<string, unknown> };
32
+ /** What the reopened session keeps so these agents' ids still answer after their records are gone. */
33
+ evicted?: EvictedNote[];
34
+ /** Reopenable sessions among them, so a steer or resume of these ids continues the saved conversation. */
35
+ tombstones?: AgentTombstone[];
36
+ /** A full-result notice's parts, so delivery clips it against its batch's budget; `content` is the fallback. */
37
+ notice?: NoticeParts & { footer: string };
28
38
  }
29
39
 
30
40
  const SINK_KEY = Symbol.for("pi-subagents:detached-sink");
@@ -96,7 +106,8 @@ export function watchDetached(sessionFile: string, onSpooled: () => void): () =>
96
106
  export interface RunningDetached {
97
107
  record: AgentRecord;
98
108
  activity?: AgentActivity;
99
- abort(): boolean;
109
+ /** `by` as for `AgentManager.abort`: `"parent"` for `stop_subagent`, omitted for a human. */
110
+ abort(by?: AgentRecord["stoppedBy"]): boolean;
100
111
  steer(message: string): boolean;
101
112
  }
102
113
 
@@ -127,6 +138,11 @@ export function unregisterRunningDetached(sessionFile: string, id: string): void
127
138
  if (byId?.size === 0) running().delete(sessionFile);
128
139
  }
129
140
 
141
+ /** Every orphan still running in the process with the session file it is registered (and wakes) under. */
142
+ export function listAllRunningDetached(): Array<{ sessionFile: string; agent: RunningDetached }> {
143
+ return [...running()].flatMap(([sessionFile, byId]) => [...byId.values()].map((agent) => ({ sessionFile, agent })));
144
+ }
145
+
130
146
  /** Orphans still running for `sessionFile`, in registration order. */
131
147
  export function listRunningDetached(sessionFile: string | undefined): RunningDetached[] {
132
148
  if (sessionFile === undefined) return [];
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, statSync } from "node:fs";
10
10
  import { join } from "node:path";
11
+ import { errorMessage } from "./error-message.js";
11
12
  import { resolveAgentDir } from "./home-paths.js";
12
13
 
13
14
  const MANIFEST_VERSION = 1;
@@ -59,7 +60,7 @@ export function readEnterpriseLocalManifest(homeDir?: string): EnterpriseLocalMa
59
60
  }
60
61
  return manifest as unknown as EnterpriseLocalManifest;
61
62
  } catch (error) {
62
- const reason = error instanceof Error ? error.message : String(error);
63
+ const reason = errorMessage(error);
63
64
  console.warn(`[pi-subagents] Ignoring invalid ${path}: ${reason}`);
64
65
  return undefined;
65
66
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * error-message.ts — the text a caught value reports.
3
+ *
4
+ * `catch` binds `unknown`: an `Error` gives its message, and anything else a
5
+ * caller threw (a string, a rejected non-Error) is stringified as-is.
6
+ */
7
+
8
+ /** The message of `error` when it is an `Error`, else `String(error)`. */
9
+ export function errorMessage(error: unknown): string {
10
+ return error instanceof Error ? error.message : String(error);
11
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * git-path.ts -- decode the C-style quoted paths git prints without `-z`.
3
+ *
4
+ * `pi.exec` decodes each stdout chunk as UTF-8 on its own, so a multibyte
5
+ * character split across two chunks becomes U+FFFD. With `core.quotePath=true`
6
+ * git escapes every non-ASCII byte (and control characters, `"` and `\`) as
7
+ * `"..."` with octal escapes, so a listing read without `-z` is pure ASCII and
8
+ * survives any chunking; this module turns it back into the real path.
9
+ */
10
+
11
+ const SIMPLE_ESCAPES: Record<string, number> = {
12
+ a: 0x07,
13
+ b: 0x08,
14
+ f: 0x0c,
15
+ n: 0x0a,
16
+ r: 0x0d,
17
+ t: 0x09,
18
+ v: 0x0b,
19
+ '"': 0x22,
20
+ "\\": 0x5c,
21
+ };
22
+
23
+ const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
24
+
25
+ /** Args that make `git` quote every non-ASCII path byte; put them before the subcommand. */
26
+ export const QUOTE_PATH_ARGS = ["-c", "core.quotePath=true"];
27
+
28
+ /**
29
+ * Decode one path as git prints it: a plain name is returned as is, a `"..."` name is
30
+ * unescaped to bytes and decoded as UTF-8. Throws on anything that cannot name a file
31
+ * as a JS string: raw non-ASCII (the listing was not quoted, so it may be mis-decoded),
32
+ * a malformed escape, or bytes that are not valid UTF-8.
33
+ */
34
+ export function unquoteGitPath(text: string): string {
35
+ const quoted = text.length >= 2 && text.startsWith('"') && text.endsWith('"');
36
+ const body = quoted ? text.slice(1, -1) : text;
37
+ for (let i = 0; i < body.length; i++) {
38
+ if (body.charCodeAt(i) > 0x7f) throw new Error(`git printed a non-ASCII path unquoted (${JSON.stringify(text)}); it may be mis-decoded`);
39
+ }
40
+ if (!quoted) return text;
41
+ const bytes: number[] = [];
42
+ for (let i = 0; i < body.length; i++) {
43
+ const char = body.charAt(i);
44
+ if (char === '"') throw new Error(`malformed quoted git path ${JSON.stringify(text)}`);
45
+ if (char !== "\\") {
46
+ bytes.push(body.charCodeAt(i));
47
+ continue;
48
+ }
49
+ const next = body.charAt(i + 1);
50
+ const octal = /^[0-3][0-7]{2}$/.test(body.slice(i + 1, i + 4)) ? body.slice(i + 1, i + 4) : undefined;
51
+ if (octal) {
52
+ bytes.push(Number.parseInt(octal, 8));
53
+ i += 3;
54
+ } else if (next in SIMPLE_ESCAPES) {
55
+ bytes.push(SIMPLE_ESCAPES[next] as number);
56
+ i += 1;
57
+ } else {
58
+ throw new Error(`malformed escape in quoted git path ${JSON.stringify(text)}`);
59
+ }
60
+ }
61
+ try {
62
+ return UTF8.decode(Uint8Array.from(bytes));
63
+ } catch {
64
+ throw new Error(`git path ${JSON.stringify(text)} is not valid UTF-8 and cannot be inspected`);
65
+ }
66
+ }
package/src/group-join.ts CHANGED
@@ -15,7 +15,6 @@ interface AgentGroup {
15
15
  agentIds: Set<string>;
16
16
  completedRecords: Map<string, AgentRecord>;
17
17
  timeoutHandle?: ReturnType<typeof setTimeout>;
18
- delivered: boolean;
19
18
  /** Shorter timeout for stragglers after a partial delivery. */
20
19
  isStraggler: boolean;
21
20
  }
@@ -40,7 +39,6 @@ export class GroupJoinManager {
40
39
  groupId,
41
40
  agentIds: new Set(agentIds),
42
41
  completedRecords: new Map(),
43
- delivered: false,
44
42
  isStraggler: false,
45
43
  };
46
44
  this.groups.set(groupId, group);
@@ -61,7 +59,7 @@ export class GroupJoinManager {
61
59
  if (!groupId) return 'pass';
62
60
 
63
61
  const group = this.groups.get(groupId);
64
- if (!group || group.delivered) return 'pass';
62
+ if (!group) return 'pass';
65
63
 
66
64
  group.completedRecords.set(record.id, record);
67
65
 
@@ -83,7 +81,6 @@ export class GroupJoinManager {
83
81
  }
84
82
 
85
83
  private onTimeout(group: AgentGroup): void {
86
- if (group.delivered) return;
87
84
  group.timeoutHandle = undefined;
88
85
 
89
86
  // Partial delivery — some agents still running
@@ -112,18 +109,15 @@ export class GroupJoinManager {
112
109
  clearTimeout(group.timeoutHandle);
113
110
  group.timeoutHandle = undefined;
114
111
  }
115
- group.delivered = true;
116
112
  this.deliverCb([...group.completedRecords.values()], partial);
117
- this.cleanupGroup(group.groupId);
113
+ this.cleanupGroup(group);
118
114
  }
119
115
 
120
- private cleanupGroup(groupId: string): void {
121
- const group = this.groups.get(groupId);
122
- if (!group) return;
116
+ private cleanupGroup(group: AgentGroup): void {
123
117
  for (const id of group.agentIds) {
124
118
  this.agentToGroup.delete(id);
125
119
  }
126
- this.groups.delete(groupId);
120
+ this.groups.delete(group.groupId);
127
121
  }
128
122
 
129
123
  /** Check if an agent is in a group. */