@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.
- package/CHANGELOG.md +22 -0
- package/dist/public.d.ts +1 -3
- package/docs/architecture/architecture.md +28 -32
- package/docs/plans/0264-remove-extension-lifecycle-control.md +275 -0
- package/docs/plans/0272-export-workspace-collaborator-types.md +147 -0
- package/docs/retro/0264-remove-extension-lifecycle-control.md +48 -0
- package/docs/retro/0272-export-workspace-collaborator-types.md +38 -0
- package/package.json +1 -1
- package/src/config/agent-types.ts +0 -2
- package/src/config/custom-agents.ts +0 -30
- package/src/config/default-agents.ts +1 -7
- package/src/config/invocation-config.ts +0 -3
- package/src/index.ts +0 -2
- package/src/lifecycle/agent-manager.ts +0 -2
- package/src/lifecycle/agent-runner.ts +6 -14
- package/src/lifecycle/agent.ts +0 -4
- package/src/service/service-adapter.ts +0 -1
- package/src/service/service.ts +20 -9
- package/src/session/prompts.ts +2 -23
- package/src/session/session-config.ts +2 -37
- package/src/tools/agent-tool.ts +0 -5
- package/src/tools/background-spawner.ts +0 -1
- package/src/tools/foreground-runner.ts +0 -1
- package/src/tools/spawn-config.ts +0 -4
- package/src/types.ts +0 -7
- package/src/ui/agent-config-editor.ts +0 -5
- package/src/ui/agent-creation-wizard.ts +0 -4
- package/src/ui/display.ts +1 -2
- package/src/session/safe-fs.ts +0 -45
- package/src/session/skill-loader.ts +0 -104
|
@@ -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", "
|
|
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}`);
|
package/src/session/safe-fs.ts
DELETED
|
@@ -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
|
-
}
|