@gotgenes/pi-subagents 12.0.0 → 13.0.0

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.
@@ -47,13 +47,8 @@ export function buildEjectContent(cfg: AgentConfig): string {
47
47
  if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
48
48
  if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
49
49
  fmFields.push(`prompt_mode: ${cfg.promptMode}`);
50
- if (!cfg.extensions) fmFields.push("extensions: false");
51
- if (cfg.skills === false) fmFields.push("skills: false");
52
- else if (Array.isArray(cfg.skills))
53
- fmFields.push(`skills: ${cfg.skills.join(", ")}`);
54
50
  if (cfg.inheritContext) fmFields.push("inherit_context: true");
55
51
  if (cfg.runInBackground) fmFields.push("run_in_background: true");
56
- if (cfg.isolated) fmFields.push("isolated: true");
57
52
  return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
58
53
  }
59
54
 
@@ -104,11 +104,8 @@ model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5-2
104
104
  thinking: <optional thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit>
105
105
  max_turns: <optional max agentic turns. 0 or omit for unlimited (default)>
106
106
  prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is appended to default prompt). Default: replace>
107
- extensions: <true (inherit all MCP/extension tools) or false (none). Default: true>
108
- skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
109
107
  inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
110
108
  run_in_background: <true to run in background by default. Default: false>
111
- isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
112
109
  ---
113
110
 
114
111
  <system prompt body — instructions for the agent>
@@ -120,7 +117,6 @@ Guidelines for choosing settings:
120
117
  - Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
121
118
  - Use prompt_mode: replace for fully custom agents with their own personality/instructions
122
119
  - Set inherit_context: true if the agent needs to know what was discussed in the parent conversation
123
- - Set isolated: true if the agent should NOT have access to MCP servers or other extensions
124
120
  - Only include frontmatter fields that differ from defaults — omit fields where the default is fine
125
121
 
126
122
  Write the file using the write tool. Only write the file, nothing else.`;
package/src/ui/display.ts CHANGED
@@ -30,7 +30,7 @@ export interface AgentDetails {
30
30
  spinnerFrame?: number;
31
31
  /** Short model name if different from parent (e.g. "haiku", "sonnet"). */
32
32
  modelName?: string;
33
- /** Notable config tags (e.g. ["thinking: high", "isolated"]). */
33
+ /** Notable config tags (e.g. ["thinking: high", "inherit context"]). */
34
34
  tags?: string[];
35
35
  /** Current turn count. */
36
36
  turnCount?: number;
@@ -135,7 +135,6 @@ export function buildInvocationTags(
135
135
  const tags: string[] = [];
136
136
  if (!invocation) return { tags };
137
137
  if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
138
- if (invocation.isolated) tags.push("isolated");
139
138
  if (invocation.inheritContext) tags.push("inherit context");
140
139
  if (invocation.runInBackground) tags.push("background");
141
140
  if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
@@ -1,45 +0,0 @@
1
- /**
2
- * safe-fs.ts — Filesystem safety utilities for reading untrusted paths.
3
- *
4
- * Used by skill-loader.ts to reject symlinks and path-traversal names
5
- * before reading skill files from disk.
6
- */
7
-
8
- import { existsSync, lstatSync, readFileSync } from "node:fs";
9
- import { debugLog } from "#src/debug";
10
-
11
- /**
12
- * Returns true if a name contains characters not allowed in agent/skill names.
13
- * Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
14
- */
15
- export function isUnsafeName(name: string): boolean {
16
- if (!name || name.length > 128) return true;
17
- return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
18
- }
19
-
20
- /**
21
- * Returns true if the given path is a symlink (defense against symlink attacks).
22
- */
23
- export function isSymlink(filePath: string): boolean {
24
- try {
25
- return lstatSync(filePath).isSymbolicLink();
26
- } catch (err) {
27
- debugLog("lstatSync", err);
28
- return false;
29
- }
30
- }
31
-
32
- /**
33
- * Safely read a file, rejecting symlinks.
34
- * Returns undefined if the file doesn't exist, is a symlink, or can't be read.
35
- */
36
- export function safeReadFile(filePath: string): string | undefined {
37
- if (!existsSync(filePath)) return undefined;
38
- if (isSymlink(filePath)) return undefined;
39
- try {
40
- return readFileSync(filePath, "utf-8");
41
- } catch (err) {
42
- debugLog("readFileSync", err);
43
- return undefined;
44
- }
45
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * skill-loader.ts — Preload named skills.
3
- *
4
- * Roots, in precedence order:
5
- * - <cwd>/.pi/skills (project, Pi's standard)
6
- * - <cwd>/.agents/skills (project, cross-tool Agent Skills spec — https://agentskills.io)
7
- * - getAgentDir()/skills (user, default ~/.pi/agent/skills — Pi's standard)
8
- * - ~/.agents/skills (user, cross-tool Agent Skills spec)
9
- * - ~/.pi/skills (legacy global, pre-Pi)
10
- *
11
- * Layout per root:
12
- * - <root>/<name>.md (flat file at the top level)
13
- * - <root>/.../<name>/SKILL.md (directory skill, may be nested — Pi's standard)
14
- *
15
- * Recursion skips dotfile entries and node_modules. A directory that itself contains
16
- * SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
17
- *
18
- * Symlinks are rejected for security (deviation from Pi, which follows them).
19
- */
20
-
21
- import type { Dirent } from "node:fs";
22
- import { existsSync, readdirSync } from "node:fs";
23
- import { homedir } from "node:os";
24
- import { join } from "node:path";
25
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
26
- import { debugLog } from "#src/debug";
27
- import { isSymlink, isUnsafeName, safeReadFile } from "#src/session/safe-fs";
28
-
29
- export interface PreloadedSkill {
30
- name: string;
31
- content: string;
32
- }
33
-
34
- export function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[] {
35
- return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
36
- }
37
-
38
- function loadSkillContent(name: string, cwd: string): string {
39
- if (isUnsafeName(name)) {
40
- return `(Skill "${name}" skipped: name contains path traversal characters)`;
41
- }
42
- const roots = [
43
- join(cwd, ".pi", "skills"), // project — Pi standard
44
- join(cwd, ".agents", "skills"), // project — Agent Skills spec
45
- join(getAgentDir(), "skills"), // user — Pi standard
46
- join(homedir(), ".agents", "skills"), // user — Agent Skills spec
47
- join(homedir(), ".pi", "skills"), // legacy global, pre-Pi
48
- ];
49
- for (const root of roots) {
50
- const content = findInRoot(root, name);
51
- if (content !== undefined) return content;
52
- }
53
- return `(Skill "${name}" not found in .pi/skills/, .agents/skills/, or global skill locations)`;
54
- }
55
-
56
- function findInRoot(root: string, name: string): string | undefined {
57
- if (isSymlink(root)) return undefined; // reject symlinked roots entirely
58
- const flat = safeReadFile(join(root, `${name}.md`))?.trim();
59
- if (flat !== undefined) return flat;
60
- return findSkillDirectory(root, name);
61
- }
62
-
63
- /** BFS under `root` for a directory named `name` containing `SKILL.md`. Pi-conforming filters. */
64
- function findSkillDirectory(root: string, name: string): string | undefined {
65
- if (!existsSync(root)) return undefined;
66
- const queue: string[] = [root];
67
-
68
- while (queue.length > 0) {
69
- const current = queue.shift();
70
- if (current === undefined) continue;
71
-
72
- let entries: Dirent[];
73
- try {
74
- entries = readdirSync(current, { withFileTypes: true });
75
- } catch (err) {
76
- debugLog("readdirSync skill root", err);
77
- continue;
78
- }
79
-
80
- // Deterministic byte-order traversal — locale-independent.
81
- entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
82
-
83
- for (const entry of entries) {
84
- if (!entry.isDirectory()) continue;
85
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
86
-
87
- // Symlinked dirs already filtered by entry.isDirectory() — Dirent uses lstat semantics.
88
- const path = join(current, entry.name);
89
- const skillMd = join(path, "SKILL.md");
90
- const isSkillDir = existsSync(skillMd);
91
-
92
- if (isSkillDir) {
93
- if (entry.name === name) {
94
- const content = safeReadFile(skillMd)?.trim();
95
- if (content !== undefined) return content;
96
- }
97
- continue; // Pi rule: skills don't nest — don't descend into a skill dir
98
- }
99
-
100
- queue.push(path);
101
- }
102
- }
103
- return undefined;
104
- }