opensec-pi-subagents 0.19.1 → 0.19.2
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 +5 -0
- package/README.md +3 -0
- package/dist/custom-agents.js +5 -2
- package/dist/discovery-cache.d.ts +6 -0
- package/dist/discovery-cache.js +42 -0
- package/dist/settings.js +3 -1
- package/package.json +10 -2
- package/src/custom-agents.ts +11 -2
- package/src/discovery-cache.ts +42 -0
- package/src/settings.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.2] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Cache unchanged subagent discovery and configuration.** Directory listings, parsed agent files, and parsed subagent settings are reused after metadata validation on each load. Changes invalidate the cache without a timer; sessions, extension initialization, tools, skills, model, and thinking inheritance remain unchanged.
|
|
14
|
+
|
|
10
15
|
## [0.19.0] - 2026-08-25
|
|
11
16
|
|
|
12
17
|
> **⚠️ Breaking — this release requires pi 0.84.0 or newer** (`peerDependencies` moves from `>=0.81.0`). `SubagentWorkflow` needs two host APIs that do not exist below it, and both fail the typecheck rather than degrading quietly — see the `Changed` entry below for which, and why neither was worth reimplementing to hold the old floor. npm flags an older pi at install time.
|
package/README.md
CHANGED
|
@@ -269,6 +269,8 @@ Project-level agents override global ones with the same name, so you can customi
|
|
|
269
269
|
|
|
270
270
|
An unreadable or unparseable agent file is skipped, not fatal — a warning names the file and the error. If it was overriding a same-named agent, a second line names the file that loads instead. Set `strictAgentFiles: true` in `subagents.json` (or `/agents → Settings → Strict agent files`) to fail startup on a broken file instead; mid-session reloads still only warn.
|
|
271
271
|
|
|
272
|
+
Discovery reuses unchanged directory listings and parsed agent files within each Pi process. File and directory metadata are checked on every load, so edits, additions, removals, and atomic replacements are picked up on the next discovery without a timer or restart. Parsed `subagents.json` settings use the same bounded cache; their existing application points are unchanged. Each load still builds fresh agent configurations, and each spawn keeps its own session and extension initialization. Model/thinking inheritance, tools, skills, and prompts are unchanged.
|
|
273
|
+
|
|
272
274
|
### Example: `.pi/agents/auditor.md`
|
|
273
275
|
|
|
274
276
|
```markdown
|
|
@@ -946,6 +948,7 @@ src/
|
|
|
946
948
|
# Agent registry
|
|
947
949
|
default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
|
|
948
950
|
custom-agents.ts # Load user-defined agents from .pi/agents/, .agents/agents/, and global agents
|
|
951
|
+
discovery-cache.ts # Bounded metadata-validated caches for discovery and parsed configuration
|
|
949
952
|
agent-types.ts # Unified agent registry (defaults + user), tool name resolution
|
|
950
953
|
agent-file-toggle.ts # Locate/edit an agent's .md: enabled: toggle, eject to frontmatter
|
|
951
954
|
agent-color.ts # Claude Code/Agency Agents name color parsing and badge rendering
|
package/dist/custom-agents.js
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
8
|
+
import { createDiscoveryCache } from "./discovery-cache.js";
|
|
8
9
|
/**
|
|
9
10
|
* The one thing a declared `name:` may not contain, matching Claude Code
|
|
10
11
|
* exactly: it reserves `:` for plugin-scoped identifiers (`my-plugin:reviewer`)
|
|
@@ -19,6 +20,8 @@ import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
|
19
20
|
* and a file must be able to override one.
|
|
20
21
|
*/
|
|
21
22
|
const RESERVED_IN_TYPE = ":";
|
|
23
|
+
const readAgentDirectory = createDiscoveryCache((path) => readdirSync(path).filter(f => f.endsWith(".md")), 64);
|
|
24
|
+
const readParsedAgent = createDiscoveryCache((path) => parseAgentFrontmatter(readFileSync(path, "utf-8")));
|
|
22
25
|
/**
|
|
23
26
|
* Scan for custom agent .md files from multiple locations.
|
|
24
27
|
* Discovery hierarchy (higher priority wins):
|
|
@@ -55,7 +58,7 @@ function loadFromDir(dir, agents, source, strict) {
|
|
|
55
58
|
return;
|
|
56
59
|
let files;
|
|
57
60
|
try {
|
|
58
|
-
files =
|
|
61
|
+
files = readAgentDirectory(dir);
|
|
59
62
|
}
|
|
60
63
|
catch {
|
|
61
64
|
return;
|
|
@@ -156,7 +159,7 @@ export function parseAgentFrontmatter(content) {
|
|
|
156
159
|
}
|
|
157
160
|
function readAgentFile(path, strict) {
|
|
158
161
|
try {
|
|
159
|
-
return
|
|
162
|
+
return readParsedAgent(path);
|
|
160
163
|
}
|
|
161
164
|
catch (err) {
|
|
162
165
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache successful file parsing or directory listings, not live session state.
|
|
3
|
+
* Validate on every access (no TTL/watchers); never reuse failures. Values stay
|
|
4
|
+
* private to the caller, which must not expose mutable cached objects.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDiscoveryCache<T>(read: (path: string) => T, capacity?: number): (path: string) => T;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/** Metadata includes replacement, same-size edits, and permission changes. */
|
|
4
|
+
function fingerprint(path) {
|
|
5
|
+
const s = statSync(path, { bigint: true });
|
|
6
|
+
return `${s.dev}:${s.ino}:${s.mode}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Cache successful file parsing or directory listings, not live session state.
|
|
10
|
+
* Validate on every access (no TTL/watchers); never reuse failures. Values stay
|
|
11
|
+
* private to the caller, which must not expose mutable cached objects.
|
|
12
|
+
*/
|
|
13
|
+
export function createDiscoveryCache(read, capacity = 256) {
|
|
14
|
+
const entries = new Map();
|
|
15
|
+
return (path) => {
|
|
16
|
+
const key = resolve(path);
|
|
17
|
+
try {
|
|
18
|
+
const before = fingerprint(key);
|
|
19
|
+
const cached = entries.get(key);
|
|
20
|
+
entries.delete(key);
|
|
21
|
+
if (cached?.fingerprint === before) {
|
|
22
|
+
entries.set(key, cached);
|
|
23
|
+
return cached.value;
|
|
24
|
+
}
|
|
25
|
+
const value = read(key);
|
|
26
|
+
// An edit racing the read must not label old content with new metadata.
|
|
27
|
+
if (before === fingerprint(key)) {
|
|
28
|
+
entries.set(key, { fingerprint: before, value });
|
|
29
|
+
if (entries.size > capacity) {
|
|
30
|
+
const oldest = entries.keys().next().value;
|
|
31
|
+
if (oldest !== undefined)
|
|
32
|
+
entries.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
entries.delete(key);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/settings.js
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { NO_FALLBACK } from "./agent-types.js";
|
|
8
|
+
import { createDiscoveryCache } from "./discovery-cache.js";
|
|
8
9
|
const VALID_JOIN_MODES = new Set(["async", "group", "smart"]);
|
|
9
10
|
const VALID_TOOL_DESCRIPTION_MODES = new Set(["full", "compact", "custom"]);
|
|
10
11
|
const VALID_WIDGET_MODES = new Set(["all", "background", "off"]);
|
|
@@ -133,11 +134,12 @@ function projectPath(cwd) {
|
|
|
133
134
|
* exists but can't be parsed emits a warning to stderr so users aren't
|
|
134
135
|
* silently reverted to defaults — and still returns `{}` so startup proceeds.
|
|
135
136
|
*/
|
|
137
|
+
const readParsedSettings = createDiscoveryCache((path) => sanitize(JSON.parse(readFileSync(path, "utf-8"))), 64);
|
|
136
138
|
function readSettingsFile(path) {
|
|
137
139
|
if (!existsSync(path))
|
|
138
140
|
return {};
|
|
139
141
|
try {
|
|
140
|
-
return
|
|
142
|
+
return readParsedSettings(path);
|
|
141
143
|
}
|
|
142
144
|
catch (err) {
|
|
143
145
|
const reason = err instanceof Error ? err.message : String(err);
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opensec-pi-subagents",
|
|
3
|
-
"version": "0.19.
|
|
4
|
-
"description": "Fork of @tintinweb/pi-subagents with parent model
|
|
3
|
+
"version": "0.19.2",
|
|
4
|
+
"description": "Fork of @tintinweb/pi-subagents with parent model/thinking inheritance and cached agent discovery.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"contributors": ["kushalkhemka"],
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/opensec-git/opensec-pi-subagents.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/opensec-git/opensec-pi-subagents#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/opensec-git/opensec-pi-subagents/issues"
|
|
15
|
+
},
|
|
8
16
|
"publishConfig": {
|
|
9
17
|
"access": "public"
|
|
10
18
|
},
|
package/src/custom-agents.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
9
|
+
import { createDiscoveryCache } from "./discovery-cache.js";
|
|
9
10
|
import type { AgentConfig, IsolationMode, MemoryScope, ThinkingLevel } from "./types.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -23,6 +24,14 @@ import type { AgentConfig, IsolationMode, MemoryScope, ThinkingLevel } from "./t
|
|
|
23
24
|
*/
|
|
24
25
|
const RESERVED_IN_TYPE = ":";
|
|
25
26
|
|
|
27
|
+
const readAgentDirectory = createDiscoveryCache(
|
|
28
|
+
(path) => readdirSync(path).filter(f => f.endsWith(".md")),
|
|
29
|
+
64,
|
|
30
|
+
);
|
|
31
|
+
const readParsedAgent = createDiscoveryCache(
|
|
32
|
+
(path) => parseAgentFrontmatter<Record<string, unknown>>(readFileSync(path, "utf-8")),
|
|
33
|
+
);
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Scan for custom agent .md files from multiple locations.
|
|
28
37
|
* Discovery hierarchy (higher priority wins):
|
|
@@ -62,7 +71,7 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
62
71
|
|
|
63
72
|
let files: string[];
|
|
64
73
|
try {
|
|
65
|
-
files =
|
|
74
|
+
files = readAgentDirectory(dir);
|
|
66
75
|
} catch {
|
|
67
76
|
return;
|
|
68
77
|
}
|
|
@@ -172,7 +181,7 @@ export function parseAgentFrontmatter<T extends Record<string, unknown>>(content
|
|
|
172
181
|
|
|
173
182
|
function readAgentFile(path: string, strict: boolean): { frontmatter: Record<string, unknown>; body: string } | undefined {
|
|
174
183
|
try {
|
|
175
|
-
return
|
|
184
|
+
return readParsedAgent(path);
|
|
176
185
|
} catch (err) {
|
|
177
186
|
const reason = err instanceof Error ? err.message : String(err);
|
|
178
187
|
if (strict) throw new Error(`${path}: ${reason}`);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Metadata includes replacement, same-size edits, and permission changes. */
|
|
5
|
+
function fingerprint(path: string): string {
|
|
6
|
+
const s = statSync(path, { bigint: true });
|
|
7
|
+
return `${s.dev}:${s.ino}:${s.mode}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cache successful file parsing or directory listings, not live session state.
|
|
12
|
+
* Validate on every access (no TTL/watchers); never reuse failures. Values stay
|
|
13
|
+
* private to the caller, which must not expose mutable cached objects.
|
|
14
|
+
*/
|
|
15
|
+
export function createDiscoveryCache<T>(read: (path: string) => T, capacity = 256): (path: string) => T {
|
|
16
|
+
const entries = new Map<string, { fingerprint: string; value: T }>();
|
|
17
|
+
return (path) => {
|
|
18
|
+
const key = resolve(path);
|
|
19
|
+
try {
|
|
20
|
+
const before = fingerprint(key);
|
|
21
|
+
const cached = entries.get(key);
|
|
22
|
+
entries.delete(key);
|
|
23
|
+
if (cached?.fingerprint === before) {
|
|
24
|
+
entries.set(key, cached);
|
|
25
|
+
return cached.value;
|
|
26
|
+
}
|
|
27
|
+
const value = read(key);
|
|
28
|
+
// An edit racing the read must not label old content with new metadata.
|
|
29
|
+
if (before === fingerprint(key)) {
|
|
30
|
+
entries.set(key, { fingerprint: before, value });
|
|
31
|
+
if (entries.size > capacity) {
|
|
32
|
+
const oldest = entries.keys().next().value;
|
|
33
|
+
if (oldest !== undefined) entries.delete(oldest);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
entries.delete(key);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { NO_FALLBACK } from "./agent-types.js";
|
|
9
|
+
import { createDiscoveryCache } from "./discovery-cache.js";
|
|
9
10
|
import type { AgentMentionMode, JoinMode, ViewerMarkdownMode, WidgetMode } from "./types.js";
|
|
10
11
|
|
|
11
12
|
export interface SubagentsSettings {
|
|
@@ -477,10 +478,15 @@ function projectPath(cwd: string): string {
|
|
|
477
478
|
* exists but can't be parsed emits a warning to stderr so users aren't
|
|
478
479
|
* silently reverted to defaults — and still returns `{}` so startup proceeds.
|
|
479
480
|
*/
|
|
481
|
+
const readParsedSettings = createDiscoveryCache(
|
|
482
|
+
(path) => sanitize(JSON.parse(readFileSync(path, "utf-8"))),
|
|
483
|
+
64,
|
|
484
|
+
);
|
|
485
|
+
|
|
480
486
|
function readSettingsFile(path: string): SubagentsSettings {
|
|
481
487
|
if (!existsSync(path)) return {};
|
|
482
488
|
try {
|
|
483
|
-
return
|
|
489
|
+
return readParsedSettings(path);
|
|
484
490
|
} catch (err) {
|
|
485
491
|
const reason = err instanceof Error ? err.message : String(err);
|
|
486
492
|
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}: ${reason}`);
|