@tryinget/pi-agent-registry 0.3.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/LICENSE +78 -0
- package/README.md +238 -0
- package/docs/engineering.local.md +90 -0
- package/docs/project/2026-08-27-agent-registry.md +287 -0
- package/docs/project/foundation.md +31 -0
- package/docs/project/vision.md +18 -0
- package/examples/.gitkeep +0 -0
- package/extensions/pi-agent-registry.ts +378 -0
- package/package.json +105 -0
- package/policy/engineering-lane.json +34 -0
- package/policy/security-policy.json +10 -0
- package/prompts/implementation-planning.md +20 -0
- package/prompts/security-review.md +20 -0
- package/scripts/fleet-lint.mjs +82 -0
- package/src/.gitkeep +0 -0
- package/src/agent-skill-resolver.ts +50 -0
- package/src/asc-execution-surface.ts +64 -0
- package/src/dispatch-authorization.ts +237 -0
- package/src/dispatch-contract.ts +89 -0
- package/src/dispatch-receipt.ts +326 -0
- package/src/dispatch-request.ts +135 -0
- package/src/dispatch.ts +498 -0
- package/src/ec-profiles.ts +392 -0
- package/src/fleet-git-snapshot.ts +323 -0
- package/src/fleet-lint-provenance.ts +356 -0
- package/src/fleet-lint-repository.ts +450 -0
- package/src/fleet-lint-skills.ts +131 -0
- package/src/fleet-lint-types.ts +113 -0
- package/src/fleet-lint-utils.ts +66 -0
- package/src/fleet-lint.ts +375 -0
- package/src/fleet-prompt-compiler.ts +155 -0
- package/src/manifest.ts +678 -0
- package/src/registry-discovery.ts +225 -0
- package/src/registry.ts +280 -0
- package/src/sessions-dir.ts +30 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: streaming bounded one-repo-per-agent discovery shared by runtime registry loading and aggregate fleet lint.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing fleet roots, zero-match behavior, missing-manifest visibility, discovery depth, or duplicate roots.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { lstat, opendir, realpath } from "node:fs/promises";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { AGENT_MANIFEST_FILENAME, globToRegExp } from "./manifest.ts";
|
|
10
|
+
|
|
11
|
+
const DISCOVERY_SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
12
|
+
"node_modules",
|
|
13
|
+
".git",
|
|
14
|
+
".cache",
|
|
15
|
+
"dist",
|
|
16
|
+
"build",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export interface DiscoveredAgentRepository {
|
|
20
|
+
root: string;
|
|
21
|
+
manifestPath: string;
|
|
22
|
+
manifestPresent: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FailedAgentRepositoryDiscovery {
|
|
26
|
+
root: string;
|
|
27
|
+
code: "repository.resolve_failed";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class RegistryDiscoveryError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
|
|
33
|
+
constructor(code: string, message: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "RegistryDiscoveryError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function comparePaths(left: string, right: string): number {
|
|
41
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function heapUp(paths: string[], start: number): void {
|
|
45
|
+
let index = start;
|
|
46
|
+
while (index > 0) {
|
|
47
|
+
const parent = Math.floor((index - 1) / 2);
|
|
48
|
+
if (comparePaths(paths[parent], paths[index]) >= 0) return;
|
|
49
|
+
[paths[parent], paths[index]] = [paths[index], paths[parent]];
|
|
50
|
+
index = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function heapDown(paths: string[], start: number): void {
|
|
55
|
+
let index = start;
|
|
56
|
+
while (true) {
|
|
57
|
+
const left = index * 2 + 1;
|
|
58
|
+
const right = left + 1;
|
|
59
|
+
let largest = index;
|
|
60
|
+
if (left < paths.length && comparePaths(paths[left], paths[largest]) > 0) largest = left;
|
|
61
|
+
if (right < paths.length && comparePaths(paths[right], paths[largest]) > 0) largest = right;
|
|
62
|
+
if (largest === index) return;
|
|
63
|
+
[paths[index], paths[largest]] = [paths[largest], paths[index]];
|
|
64
|
+
index = largest;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function insertBounded(
|
|
69
|
+
paths: string[],
|
|
70
|
+
included: Set<string>,
|
|
71
|
+
candidate: string,
|
|
72
|
+
maxRepositories: number,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (included.has(candidate)) {
|
|
75
|
+
throw new RegistryDiscoveryError(
|
|
76
|
+
"fleet.duplicate_candidate",
|
|
77
|
+
"one agent repository was matched more than once by configured roots",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (paths.length < maxRepositories) {
|
|
81
|
+
paths.push(candidate);
|
|
82
|
+
included.add(candidate);
|
|
83
|
+
heapUp(paths, paths.length - 1);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (comparePaths(candidate, paths[0]) >= 0) return false;
|
|
87
|
+
included.delete(paths[0]);
|
|
88
|
+
paths[0] = candidate;
|
|
89
|
+
included.add(candidate);
|
|
90
|
+
heapDown(paths, 0);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function discoverAgentRepositories(
|
|
95
|
+
roots: string[],
|
|
96
|
+
strictMissingRoot: boolean,
|
|
97
|
+
maxRepositories = 5_000,
|
|
98
|
+
): Promise<{
|
|
99
|
+
repositories: DiscoveredAgentRepository[];
|
|
100
|
+
failures: FailedAgentRepositoryDiscovery[];
|
|
101
|
+
omittedCount: number;
|
|
102
|
+
}> {
|
|
103
|
+
if (!Number.isSafeInteger(maxRepositories) || maxRepositories <= 0) {
|
|
104
|
+
throw new RegistryDiscoveryError(
|
|
105
|
+
"fleet.repository_bound_invalid",
|
|
106
|
+
"maxRepositories must be a positive safe integer",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (new Set(roots).size !== roots.length) {
|
|
110
|
+
throw new RegistryDiscoveryError(
|
|
111
|
+
"fleet.duplicate_roots",
|
|
112
|
+
"configured agent registry roots contain duplicates",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const candidateHeap: string[] = [];
|
|
117
|
+
const includedCandidates = new Set<string>();
|
|
118
|
+
let totalMatches = 0;
|
|
119
|
+
for (const configuredRoot of roots) {
|
|
120
|
+
const leafGlob = /[*?[]/u.test(configuredRoot);
|
|
121
|
+
if (!leafGlob) {
|
|
122
|
+
const info = await lstat(configuredRoot).catch(() => undefined);
|
|
123
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) {
|
|
124
|
+
if (strictMissingRoot) {
|
|
125
|
+
throw new RegistryDiscoveryError(
|
|
126
|
+
"fleet.root_invalid",
|
|
127
|
+
"configured agent registry root is not one non-symlink directory",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
totalMatches += 1;
|
|
133
|
+
insertBounded(candidateHeap, includedCandidates, configuredRoot, maxRepositories);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const scanDir = dirname(configuredRoot);
|
|
138
|
+
const directory = await opendir(scanDir).catch(() => undefined);
|
|
139
|
+
if (!directory) {
|
|
140
|
+
if (strictMissingRoot) {
|
|
141
|
+
throw new RegistryDiscoveryError(
|
|
142
|
+
"fleet.root_missing",
|
|
143
|
+
"configured agent registry root does not exist",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const leafPattern = globToRegExp(basename(configuredRoot));
|
|
149
|
+
let rootMatches = 0;
|
|
150
|
+
for await (const entry of directory) {
|
|
151
|
+
if (
|
|
152
|
+
!entry.isDirectory() ||
|
|
153
|
+
entry.name.startsWith(".") ||
|
|
154
|
+
DISCOVERY_SKIP_DIRS.has(entry.name) ||
|
|
155
|
+
!leafPattern.test(entry.name)
|
|
156
|
+
) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
rootMatches += 1;
|
|
160
|
+
totalMatches += 1;
|
|
161
|
+
insertBounded(candidateHeap, includedCandidates, join(scanDir, entry.name), maxRepositories);
|
|
162
|
+
}
|
|
163
|
+
if (strictMissingRoot && rootMatches === 0) {
|
|
164
|
+
throw new RegistryDiscoveryError(
|
|
165
|
+
"fleet.root_zero_match",
|
|
166
|
+
"configured agent registry pattern matched no repositories",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const repositories: DiscoveredAgentRepository[] = [];
|
|
172
|
+
const failures: FailedAgentRepositoryDiscovery[] = [];
|
|
173
|
+
const candidates = candidateHeap.sort(comparePaths);
|
|
174
|
+
const physicalOwners = new Map<string, string>();
|
|
175
|
+
for (const candidate of candidates) {
|
|
176
|
+
const physical = await realpath(candidate).catch(() => undefined);
|
|
177
|
+
if (!physical) {
|
|
178
|
+
failures.push({ root: candidate, code: "repository.resolve_failed" });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const existing = physicalOwners.get(physical);
|
|
182
|
+
if (existing && existing !== candidate) {
|
|
183
|
+
throw new RegistryDiscoveryError(
|
|
184
|
+
"fleet.duplicate_physical_repository",
|
|
185
|
+
"one physical agent repository was configured through multiple logical roots",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
physicalOwners.set(physical, candidate);
|
|
189
|
+
const manifestPath = join(physical, AGENT_MANIFEST_FILENAME);
|
|
190
|
+
const manifestInfo = await lstat(manifestPath).catch(() => undefined);
|
|
191
|
+
repositories.push({
|
|
192
|
+
root: physical,
|
|
193
|
+
manifestPath,
|
|
194
|
+
manifestPresent: Boolean(manifestInfo?.isFile() || manifestInfo?.isSymbolicLink()),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
repositories,
|
|
200
|
+
failures,
|
|
201
|
+
omittedCount: Math.max(0, totalMatches - repositories.length - failures.length),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function discoverAgentManifestPaths(
|
|
206
|
+
roots: string[],
|
|
207
|
+
strictMissingRoot: boolean,
|
|
208
|
+
): Promise<string[]> {
|
|
209
|
+
const discovered = await discoverAgentRepositories(roots, strictMissingRoot);
|
|
210
|
+
if (discovered.failures.length > 0) {
|
|
211
|
+
throw new RegistryDiscoveryError(
|
|
212
|
+
"registry.repository_resolve_failed",
|
|
213
|
+
"one configured agent repository could not be resolved",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (discovered.omittedCount > 0) {
|
|
217
|
+
throw new RegistryDiscoveryError(
|
|
218
|
+
"registry.repository_bound_exceeded",
|
|
219
|
+
"agent registry discovery exceeded its repository bound",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return discovered.repositories
|
|
223
|
+
.filter((entry) => entry.manifestPresent)
|
|
224
|
+
.map((entry) => entry.manifestPath);
|
|
225
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: agent manifest discovery across roots, fail-closed name indexing, and name -> composed launch resolution.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing manifest discovery depth, registry roots configuration, or the resolution contract.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
type EcProfileSource,
|
|
11
|
+
knownEcProfiles,
|
|
12
|
+
loadEcProfiles,
|
|
13
|
+
materializeSkillDirs,
|
|
14
|
+
planSkillSelection,
|
|
15
|
+
} from "./ec-profiles.ts";
|
|
16
|
+
import {
|
|
17
|
+
type AgentManifest,
|
|
18
|
+
assertAgentExtensionsExist,
|
|
19
|
+
defaultUserSkillsRoot,
|
|
20
|
+
expandAgentActivities,
|
|
21
|
+
loadAgentManifest,
|
|
22
|
+
readAgentSystemPrompt,
|
|
23
|
+
resolveAgentExtensions,
|
|
24
|
+
} from "./manifest.ts";
|
|
25
|
+
import { discoverAgentManifestPaths } from "./registry-discovery.ts";
|
|
26
|
+
|
|
27
|
+
export const AGENT_REGISTRY_ROOTS_ENV = "PI_AGENT_REGISTRY_ROOTS";
|
|
28
|
+
export class AgentRegistryError extends Error {
|
|
29
|
+
constructor(message: string) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "AgentRegistryError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AgentRegistryOptions {
|
|
36
|
+
/** Discovery roots (agent-repo patterns or explicit dirs; default: fleet patterns / PI_AGENT_REGISTRY_ROOTS). */
|
|
37
|
+
roots?: string[];
|
|
38
|
+
/** Explicit engineering-core profiles source (default: resolved from env or home). */
|
|
39
|
+
ec?: EcProfileSource;
|
|
40
|
+
/** Overrides the user-level skills root for `skills.extra` resolution. */
|
|
41
|
+
userSkillsRoot?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AgentListing {
|
|
45
|
+
name: string;
|
|
46
|
+
display_name?: string;
|
|
47
|
+
version?: string;
|
|
48
|
+
role?: string;
|
|
49
|
+
creation_task?: string;
|
|
50
|
+
tools: string[];
|
|
51
|
+
skills: { profile?: string; extra?: string[] };
|
|
52
|
+
extensions: string[];
|
|
53
|
+
defaults: { model: string | null; thinking: string };
|
|
54
|
+
activities: string[];
|
|
55
|
+
manifestPath: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ResolvedAgentLaunch {
|
|
59
|
+
name: string;
|
|
60
|
+
/** Composed system prompt: system_prompt_file contents + rendered advisory scope. */
|
|
61
|
+
role?: string;
|
|
62
|
+
creation_task?: string;
|
|
63
|
+
systemPrompt: string;
|
|
64
|
+
/** Comma-separated tool allowlist for the ASC custom profile (read-only default: read). */
|
|
65
|
+
tools: string;
|
|
66
|
+
thinking: string;
|
|
67
|
+
/** null = inherit parent session model. */
|
|
68
|
+
model: string | null;
|
|
69
|
+
/** Resolved child extension allowlist entries. */
|
|
70
|
+
extensions: string[];
|
|
71
|
+
/** Materialized child skill dirs (empty when the agent declares no skills). */
|
|
72
|
+
skillDirs: string[];
|
|
73
|
+
/** Skill names materialized into skillDirs. */
|
|
74
|
+
loadedSkills: string[];
|
|
75
|
+
/** Declared activity template paths (relative to the agent repo root, globs expanded). */
|
|
76
|
+
activities: string[];
|
|
77
|
+
/** Advisory repo scope rendered into the system prompt. */
|
|
78
|
+
scopeRepos: string[];
|
|
79
|
+
/** Advisory forbidden paths rendered into the system prompt. */
|
|
80
|
+
scopeForbidden: string[];
|
|
81
|
+
/** Operator scope note rendered into the system prompt. */
|
|
82
|
+
scopeNote?: string;
|
|
83
|
+
/** Removes materialized skill dirs; ASC owns cleanup once dispatched. */
|
|
84
|
+
cleanup: () => Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AgentRegistry {
|
|
88
|
+
/** Configured discovery roots (agent-repo patterns or explicit dirs). */
|
|
89
|
+
roots: string[];
|
|
90
|
+
ec: EcProfileSource;
|
|
91
|
+
/** Configured user-level skills root used for `skills.extra` resolution. */
|
|
92
|
+
userSkillsRoot: string;
|
|
93
|
+
agents: Map<string, AgentManifest>;
|
|
94
|
+
list(): AgentListing[];
|
|
95
|
+
get(name: string): AgentManifest | undefined;
|
|
96
|
+
resolve(name: string): Promise<ResolvedAgentLaunch>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fleet layout: ONE STANDALONE REPO PER AGENT. The canonical fleet home is
|
|
101
|
+
* the workspace-level `~/ai-society/agents/agent-*` directory (conventions
|
|
102
|
+
* owner: softwareco-agents/docs/agent-registry.md). Company/lane agent homes
|
|
103
|
+
* may exist later as forward-compatible extras; PI_AGENT_REGISTRY_ROOTS
|
|
104
|
+
* overrides discovery entirely. No nesting inside product repos ever —
|
|
105
|
+
* an agent.json is only read at an agent-repo root.
|
|
106
|
+
*/
|
|
107
|
+
const DEFAULT_AGENT_REPO_PATTERNS: readonly string[] = [
|
|
108
|
+
"~/ai-society/agents/agent-*",
|
|
109
|
+
"~/ai-society/core/agent-*",
|
|
110
|
+
"~/ai-society/holdingco/agent-*",
|
|
111
|
+
"~/ai-society/teachingco/agent-*",
|
|
112
|
+
"~/ai-society/healthco/agent-*",
|
|
113
|
+
"~/ai-society/softwareco/owned/agent-*",
|
|
114
|
+
"~/ai-society/softwareco/infra/agent-*",
|
|
115
|
+
"~/ai-society/softwareco/contrib/agent-*",
|
|
116
|
+
"~/ai-society/softwareco/agents/agent-*",
|
|
117
|
+
"~/ai-society/softwareco/fork/agent-*",
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
export function expandTildePath(value: string): string {
|
|
121
|
+
if (value === "~") return homedir();
|
|
122
|
+
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
123
|
+
return resolve(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Discovery roots: PI_AGENT_REGISTRY_ROOTS (colon-separated patterns) or the fleet defaults. */
|
|
127
|
+
export function defaultRegistryRoots(): string[] {
|
|
128
|
+
const raw = process.env[AGENT_REGISTRY_ROOTS_ENV]?.trim();
|
|
129
|
+
if (raw) {
|
|
130
|
+
return raw
|
|
131
|
+
.split(":")
|
|
132
|
+
.map((entry) => entry.trim())
|
|
133
|
+
.filter((entry) => entry.length > 0)
|
|
134
|
+
.map(expandTildePath);
|
|
135
|
+
}
|
|
136
|
+
return DEFAULT_AGENT_REPO_PATTERNS.map(expandTildePath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function createAgentRegistry(options?: AgentRegistryOptions): Promise<AgentRegistry> {
|
|
140
|
+
const roots = options?.roots ?? defaultRegistryRoots();
|
|
141
|
+
const ec = options?.ec ?? (await loadEcProfiles());
|
|
142
|
+
const userSkillsRoot = options?.userSkillsRoot ?? defaultUserSkillsRoot();
|
|
143
|
+
|
|
144
|
+
const envConfigured =
|
|
145
|
+
options?.roots !== undefined || Boolean(process.env[AGENT_REGISTRY_ROOTS_ENV]?.trim());
|
|
146
|
+
const manifestPaths = await discoverAgentManifestPaths(roots, envConfigured).catch((error) => {
|
|
147
|
+
throw new AgentRegistryError(error instanceof Error ? error.message : String(error));
|
|
148
|
+
});
|
|
149
|
+
const agents = new Map<string, AgentManifest>();
|
|
150
|
+
for (const manifestPath of manifestPaths) {
|
|
151
|
+
const manifest = await loadAgentManifest(dirname(manifestPath), {
|
|
152
|
+
ecProfiles: knownEcProfiles(ec),
|
|
153
|
+
});
|
|
154
|
+
const existing = agents.get(manifest.name);
|
|
155
|
+
if (existing) {
|
|
156
|
+
throw new AgentRegistryError(
|
|
157
|
+
`duplicate agent name "${manifest.name}" declared by both ${existing.manifestPath} and ${manifest.manifestPath}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
agents.set(manifest.name, manifest);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
roots,
|
|
165
|
+
ec,
|
|
166
|
+
userSkillsRoot,
|
|
167
|
+
agents,
|
|
168
|
+
list() {
|
|
169
|
+
return [...agents.values()]
|
|
170
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
171
|
+
.map(
|
|
172
|
+
(manifest): AgentListing => ({
|
|
173
|
+
name: manifest.name,
|
|
174
|
+
...(manifest.display_name ? { display_name: manifest.display_name } : {}),
|
|
175
|
+
...(manifest.version ? { version: manifest.version } : {}),
|
|
176
|
+
...(manifest.role ? { role: manifest.role } : {}),
|
|
177
|
+
...(manifest.creation_task ? { creation_task: manifest.creation_task } : {}),
|
|
178
|
+
tools: [...manifest.tools],
|
|
179
|
+
skills: manifest.skills
|
|
180
|
+
? {
|
|
181
|
+
...(manifest.skills.profile ? { profile: manifest.skills.profile } : {}),
|
|
182
|
+
...(manifest.skills.extra ? { extra: [...manifest.skills.extra] } : {}),
|
|
183
|
+
}
|
|
184
|
+
: {},
|
|
185
|
+
extensions: resolveAgentExtensions(manifest),
|
|
186
|
+
defaults: { ...manifest.defaults },
|
|
187
|
+
activities: [...manifest.activities],
|
|
188
|
+
manifestPath: manifest.manifestPath,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
get(name: string) {
|
|
193
|
+
return agents.get(name);
|
|
194
|
+
},
|
|
195
|
+
async resolve(name: string): Promise<ResolvedAgentLaunch> {
|
|
196
|
+
const manifest = agents.get(name);
|
|
197
|
+
if (!manifest) {
|
|
198
|
+
throw new AgentRegistryError(
|
|
199
|
+
`unknown agent: ${name} (registered: ${[...agents.keys()].sort().join(", ") || "none"})`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const [systemPromptContents, expandedActivities] = await Promise.all([
|
|
204
|
+
readAgentSystemPrompt(manifest),
|
|
205
|
+
expandAgentActivities(manifest),
|
|
206
|
+
assertAgentExtensionsExist(manifest),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
let skillDirs: string[] = [];
|
|
210
|
+
let loadedSkills: string[] = [];
|
|
211
|
+
let cleanup: () => Promise<void> = async () => {};
|
|
212
|
+
if (manifest.skills?.profile !== undefined || (manifest.skills?.extra?.length ?? 0) > 0) {
|
|
213
|
+
const selection = planSkillSelection({
|
|
214
|
+
...(manifest.skills?.profile !== undefined ? { profile: manifest.skills.profile } : {}),
|
|
215
|
+
...(manifest.skills?.extra ? { extra: manifest.skills.extra } : {}),
|
|
216
|
+
ec,
|
|
217
|
+
manifestRoot: manifest.root,
|
|
218
|
+
userSkillsRoot,
|
|
219
|
+
});
|
|
220
|
+
const materialized = await materializeSkillDirs(selection, manifest.name);
|
|
221
|
+
skillDirs = [materialized.dir];
|
|
222
|
+
loadedSkills = materialized.skills;
|
|
223
|
+
cleanup = materialized.cleanup;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const scopeSection = renderScopeSection(manifest);
|
|
227
|
+
const systemPrompt = scopeSection
|
|
228
|
+
? `${systemPromptContents.replace(/\s+$/u, "")}\n\n---\n\n${scopeSection}`
|
|
229
|
+
: systemPromptContents;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: manifest.name,
|
|
233
|
+
systemPrompt,
|
|
234
|
+
...(manifest.role ? { role: manifest.role } : {}),
|
|
235
|
+
...(manifest.creation_task ? { creation_task: manifest.creation_task } : {}),
|
|
236
|
+
tools: manifest.tools.join(","),
|
|
237
|
+
thinking: manifest.defaults.thinking,
|
|
238
|
+
model: manifest.defaults.model,
|
|
239
|
+
extensions: resolveAgentExtensions(manifest),
|
|
240
|
+
skillDirs,
|
|
241
|
+
loadedSkills,
|
|
242
|
+
activities: expandedActivities,
|
|
243
|
+
scopeRepos: [...(manifest.scope?.repos ?? [])],
|
|
244
|
+
scopeForbidden: [...(manifest.scope?.forbidden ?? [])],
|
|
245
|
+
...(manifest.scope?.note ? { scopeNote: manifest.scope.note } : {}),
|
|
246
|
+
cleanup,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Render the advisory operating-territory section appended to the system prompt. */
|
|
253
|
+
export function renderScopeSection(manifest: AgentManifest): string {
|
|
254
|
+
const repos = manifest.scope?.repos ?? [];
|
|
255
|
+
const forbidden = manifest.scope?.forbidden ?? [];
|
|
256
|
+
const note = manifest.scope?.note;
|
|
257
|
+
if (repos.length === 0 && forbidden.length === 0 && !note) {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
const lines: string[] = ["## Operating territory (advisory scope)", ""];
|
|
261
|
+
if (note) {
|
|
262
|
+
lines.push(note, "");
|
|
263
|
+
}
|
|
264
|
+
if (repos.length > 0) {
|
|
265
|
+
lines.push("Repository scope (advisory, not a sandbox):");
|
|
266
|
+
for (const repo of repos) {
|
|
267
|
+
lines.push(`- ${repo}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (forbidden.length > 0) {
|
|
271
|
+
if (repos.length > 0) {
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
lines.push("Forbidden paths:");
|
|
275
|
+
for (const entry of forbidden) {
|
|
276
|
+
lines.push(`- ${entry}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: ASC-owned subagent session-root resolution for Fleet Phase-2 standing-agent dispatch.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing where dispatched standing-agent children record ASC sessions.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { loadAscExecutionSurface } from "./asc-execution-surface.ts";
|
|
8
|
+
|
|
9
|
+
export class RegistrySessionsDirError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "RegistrySessionsDirError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fleet Phase 0 quarantined registry-owned session-root resolution; AK 5132
|
|
18
|
+
* (Fleet Phase 2) lifts the quarantine by delegating to ASC's exported
|
|
19
|
+
* contract when the installed ASC provides it. The registry never invents a
|
|
20
|
+
* session root and never re-implements pi-native session directory semantics.
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveRegistrySubagentSessionsDir(cwd: string): Promise<string> {
|
|
23
|
+
const surface = await loadAscExecutionSurface();
|
|
24
|
+
if (!surface) {
|
|
25
|
+
throw new RegistrySessionsDirError(
|
|
26
|
+
"ASC execution surface is unavailable; subagent session-root resolution stays ASC-owned and fails closed",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return surface.resolveSubagentSessionsDir({ cwd }).path;
|
|
30
|
+
}
|