@tangle-network/agent-profile-materialize 0.15.1 → 0.16.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/dist/candidate-types.d.ts +32 -0
- package/dist/candidate-types.js +1 -0
- package/dist/candidate.d.ts +9 -0
- package/dist/candidate.js +279 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +32 -0
- package/dist/control-maps.d.ts +34 -0
- package/dist/control-maps.js +185 -0
- package/dist/execution-controls.d.ts +5 -0
- package/dist/execution-controls.js +186 -0
- package/dist/harness.d.ts +81 -0
- package/dist/harness.js +167 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +261 -0
- package/dist/index.d.ts +23 -623
- package/dist/index.js +14 -3733
- package/dist/mcp.d.ts +22 -0
- package/dist/mcp.js +244 -0
- package/dist/plan-application.d.ts +10 -0
- package/dist/plan-application.js +152 -0
- package/dist/plan-digest.d.ts +18 -0
- package/dist/plan-digest.js +90 -0
- package/dist/plan-json.d.ts +18 -0
- package/dist/plan-json.js +91 -0
- package/dist/plan-normalize.d.ts +13 -0
- package/dist/plan-normalize.js +156 -0
- package/dist/plan-prompt.d.ts +38 -0
- package/dist/plan-prompt.js +49 -0
- package/dist/plan-support.d.ts +9 -0
- package/dist/plan-support.js +22 -0
- package/dist/plan-types.d.ts +294 -0
- package/dist/plan-types.js +106 -0
- package/dist/prime-agent-dir.d.ts +47 -0
- package/dist/prime-agent-dir.js +164 -0
- package/dist/prime-profile.d.ts +88 -0
- package/dist/prime-profile.js +284 -0
- package/dist/profile-materializer.d.ts +12 -0
- package/dist/profile-materializer.js +59 -0
- package/dist/profile-plan.d.ts +14 -0
- package/dist/profile-plan.js +86 -0
- package/dist/profile-prompt.d.ts +4 -0
- package/dist/profile-prompt.js +105 -0
- package/dist/profile-support.d.ts +8 -0
- package/dist/profile-support.js +6 -0
- package/dist/resource-lowering.d.ts +7 -0
- package/dist/resource-lowering.js +103 -0
- package/dist/resource-resolution.d.ts +27 -0
- package/dist/resource-resolution.js +241 -0
- package/dist/subagents.d.ts +6 -0
- package/dist/subagents.js +133 -0
- package/dist/workspace-apply.d.ts +12 -0
- package/dist/workspace-apply.js +334 -0
- package/dist/workspace-files.d.ts +8 -0
- package/dist/workspace-files.js +46 -0
- package/package.json +3 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { materializeCommand } from "./commands.js";
|
|
2
|
+
import { HARNESS_SKILL_DIRS } from "./harness.js";
|
|
3
|
+
import { normalizeSkillMd, resolveInlineRef } from "./resource-resolution.js";
|
|
4
|
+
import { codexAgentRoleName, registerCodexAgent } from "./subagents.js";
|
|
5
|
+
import { DEFAULT_FILE_MODE, EXECUTABLE_FILE_MODE, normalizeWorkspaceRelativePath, } from "./workspace-files.js";
|
|
6
|
+
export function lowerProfileResources(profile, harness, builder) {
|
|
7
|
+
const { plan, addExplicitResource, unsupported } = builder;
|
|
8
|
+
const fileMountPaths = new Set();
|
|
9
|
+
for (const mount of profile.resources?.files ?? []) {
|
|
10
|
+
const relPath = normalizeWorkspaceRelativePath(mount.path);
|
|
11
|
+
if (fileMountPaths.has(relPath)) {
|
|
12
|
+
throw new Error(`Duplicate workspace file mount: ${relPath}`);
|
|
13
|
+
}
|
|
14
|
+
fileMountPaths.add(relPath);
|
|
15
|
+
if (mount.executable !== undefined &&
|
|
16
|
+
typeof mount.executable !== "boolean") {
|
|
17
|
+
throw new Error(`Workspace file executable must be a boolean: ${relPath}`);
|
|
18
|
+
}
|
|
19
|
+
const resource = resolveInlineRef(mount.resource);
|
|
20
|
+
if (!resource) {
|
|
21
|
+
unsupported("files", `file mount "${relPath}" is not inline (github refs need async pre-resolution)`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
addExplicitResource("files", relPath, resource.content, mount.executable ? EXECUTABLE_FILE_MODE : DEFAULT_FILE_MODE);
|
|
25
|
+
}
|
|
26
|
+
for (const ref of profile.resources?.tools ?? []) {
|
|
27
|
+
const resource = resolveInlineRef(ref);
|
|
28
|
+
if (!resource) {
|
|
29
|
+
unsupported("tools", "tool ref is not inline (github refs need async pre-resolution)");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (harness !== "opencode") {
|
|
33
|
+
unsupported("tools", `${harness}: no verified cwd-native file tool path; use MCP or a native extension`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
addExplicitResource("tools", `.opencode/tools/${resource.name}`, resource.content);
|
|
37
|
+
}
|
|
38
|
+
for (const ref of profile.resources?.agents ?? []) {
|
|
39
|
+
const resource = resolveInlineRef(ref);
|
|
40
|
+
if (!resource) {
|
|
41
|
+
unsupported("subagents", "agent ref is not inline (github refs need async pre-resolution)");
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const directory = agentResourceDirectory(harness);
|
|
45
|
+
if (!directory) {
|
|
46
|
+
unsupported("subagents", `${harness}: agent definitions have no verified cwd-native resource path`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const relPath = `${directory}/${resource.name}`;
|
|
50
|
+
addExplicitResource("subagents", relPath, resource.content);
|
|
51
|
+
if (harness === "codex") {
|
|
52
|
+
const roleName = codexAgentRoleName(resource.name);
|
|
53
|
+
if (!roleName) {
|
|
54
|
+
unsupported("subagents", `codex: agent resource "${resource.name}" must use a lowercase role name`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
registerCodexAgent(plan, roleName, relPath, roleName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function lowerProfileSkills(profile, harness, builder) {
|
|
63
|
+
const { addExplicitResource, unsupported } = builder;
|
|
64
|
+
for (const ref of profile.resources?.skills ?? []) {
|
|
65
|
+
const resource = resolveInlineRef(ref);
|
|
66
|
+
if (!resource) {
|
|
67
|
+
unsupported("skills", "skill ref is not inline (github refs need async pre-resolution)");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const directory = HARNESS_SKILL_DIRS[harness];
|
|
71
|
+
if (!directory) {
|
|
72
|
+
unsupported("skills", `${harness}: no cwd skill dir (skills live in a user/global dir); skill "${resource.name}" not mounted`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
addExplicitResource("skills", `${directory}/${resource.name}/SKILL.md`, normalizeSkillMd(resource.name, resource.content));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function lowerProfileCommands(profile, harness, builder) {
|
|
79
|
+
const { addExplicitResource, unsupported } = builder;
|
|
80
|
+
for (const ref of profile.resources?.commands ?? []) {
|
|
81
|
+
const resource = resolveInlineRef(ref);
|
|
82
|
+
if (!resource) {
|
|
83
|
+
unsupported("commands", "command ref is not inline (github needs pre-resolution)");
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
materializeCommand(harness, resource.name, resource.content, (path, content, mode) => addExplicitResource("commands", path, content, mode), unsupported);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function agentResourceDirectory(harness) {
|
|
90
|
+
switch (harness) {
|
|
91
|
+
case "claude-code":
|
|
92
|
+
case "nanoclaw":
|
|
93
|
+
return ".claude/agents";
|
|
94
|
+
case "codex":
|
|
95
|
+
return ".codex/agents";
|
|
96
|
+
case "opencode":
|
|
97
|
+
return ".opencode/agents";
|
|
98
|
+
case "gemini":
|
|
99
|
+
return ".gemini/agents";
|
|
100
|
+
default:
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Resolve inline bytes already present on the profile. Remote resources must
|
|
2
|
+
* be resolved by {@link resolveAgentProfileResources} before materialization.
|
|
3
|
+
*
|
|
4
|
+
* FAIL-CLOSED: a ref that declares `kind: "inline"` is asserting its bytes are
|
|
5
|
+
* already resolved, so missing/non-string `content` is a caller bug, not an
|
|
6
|
+
* empty file — silently coercing it (`String(content ?? "")`) would produce a
|
|
7
|
+
* file that "looks provisioned, loads nothing", exactly the trap this module's
|
|
8
|
+
* docstring warns against. */
|
|
9
|
+
export declare function resolveInlineRef(ref: AgentProfileResourceRef): {
|
|
10
|
+
name: string;
|
|
11
|
+
content: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export interface ResolveAgentProfileResourcesOptions {
|
|
14
|
+
/** Repository used when a GitHub resource omits its own `repository`. */
|
|
15
|
+
repository?: string;
|
|
16
|
+
fetch?: typeof globalThis.fetch;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve every GitHub resource to exact inline bytes before materialization.
|
|
21
|
+
* The profile's default harness does not affect resource resolution.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveAgentProfileResources(inputProfile: AgentProfile, options?: ResolveAgentProfileResourcesOptions): Promise<AgentProfile>;
|
|
24
|
+
/** Normalize any skill markdown (VB frontmatter `skill:`/`description:`, or raw) into a
|
|
25
|
+
* harness SKILL.md with `name:`/`description:` frontmatter the harnesses require. */
|
|
26
|
+
export declare function normalizeSkillMd(name: string, raw: string): string;
|
|
27
|
+
import type { AgentProfile, AgentProfileResourceRef } from "@tangle-network/agent-interface";
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
function describeUnresolvedRef(r) {
|
|
2
|
+
const name = typeof r.name === "string" ? r.name : undefined;
|
|
3
|
+
const path = typeof r.path === "string" ? r.path : undefined;
|
|
4
|
+
if (name && path)
|
|
5
|
+
return `"${name}" (${path})`;
|
|
6
|
+
if (name)
|
|
7
|
+
return `"${name}"`;
|
|
8
|
+
if (path)
|
|
9
|
+
return `"${path}"`;
|
|
10
|
+
return "resource";
|
|
11
|
+
}
|
|
12
|
+
/** Resolve inline bytes already present on the profile. Remote resources must
|
|
13
|
+
* be resolved by {@link resolveAgentProfileResources} before materialization.
|
|
14
|
+
*
|
|
15
|
+
* FAIL-CLOSED: a ref that declares `kind: "inline"` is asserting its bytes are
|
|
16
|
+
* already resolved, so missing/non-string `content` is a caller bug, not an
|
|
17
|
+
* empty file — silently coercing it (`String(content ?? "")`) would produce a
|
|
18
|
+
* file that "looks provisioned, loads nothing", exactly the trap this module's
|
|
19
|
+
* docstring warns against. */
|
|
20
|
+
export function resolveInlineRef(ref) {
|
|
21
|
+
const r = ref;
|
|
22
|
+
if (typeof r.content === "string") {
|
|
23
|
+
return { name: String(r.name ?? r.path ?? "resource"), content: r.content };
|
|
24
|
+
}
|
|
25
|
+
if (r.kind === "inline") {
|
|
26
|
+
throw new Error(`Profile resource ${describeUnresolvedRef(r)} declares kind "inline" but has no string content`);
|
|
27
|
+
}
|
|
28
|
+
return null; // github / unresolved
|
|
29
|
+
}
|
|
30
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
31
|
+
const collapse = (s) => s.replace(/\s+/g, " ").trim();
|
|
32
|
+
const DEFAULT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
33
|
+
const MAX_RESOURCE_BYTES = 10 * 1024 * 1024;
|
|
34
|
+
const MAX_PROFILE_RESOURCE_BYTES = 32 * 1024 * 1024;
|
|
35
|
+
const MAX_PROFILE_RESOURCE_REFS = 128;
|
|
36
|
+
const MAX_CONCURRENT_RESOURCE_FETCHES = 8;
|
|
37
|
+
async function readResourceBody(response, description, consumeBytes) {
|
|
38
|
+
if (!response.body)
|
|
39
|
+
return "";
|
|
40
|
+
const reader = response.body.getReader();
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let totalBytes = 0;
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done)
|
|
47
|
+
break;
|
|
48
|
+
if (!value)
|
|
49
|
+
continue;
|
|
50
|
+
totalBytes += value.byteLength;
|
|
51
|
+
if (totalBytes > MAX_RESOURCE_BYTES) {
|
|
52
|
+
await reader.cancel().catch(() => undefined);
|
|
53
|
+
throw new Error(`GitHub profile resource exceeds ${MAX_RESOURCE_BYTES} bytes: ${description}`);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
consumeBytes(value.byteLength);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await reader.cancel().catch(() => undefined);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
chunks.push(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
reader.releaseLock();
|
|
67
|
+
}
|
|
68
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)), totalBytes).toString("utf8");
|
|
69
|
+
}
|
|
70
|
+
async function mapWithConcurrency(values, concurrency, controller, transform) {
|
|
71
|
+
const results = new Array(values.length);
|
|
72
|
+
let nextIndex = 0;
|
|
73
|
+
let firstFailure;
|
|
74
|
+
const worker = async () => {
|
|
75
|
+
while (!firstFailure) {
|
|
76
|
+
const index = nextIndex++;
|
|
77
|
+
if (index >= values.length)
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
results[index] = await transform(values[index]);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!firstFailure) {
|
|
84
|
+
firstFailure = { error };
|
|
85
|
+
controller.abort(error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => worker()));
|
|
91
|
+
if (firstFailure)
|
|
92
|
+
throw firstFailure.error;
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve every GitHub resource to exact inline bytes before materialization.
|
|
97
|
+
* The profile's default harness does not affect resource resolution.
|
|
98
|
+
*/
|
|
99
|
+
export async function resolveAgentProfileResources(inputProfile, options = {}) {
|
|
100
|
+
const profile = agentProfileSchema.parse(inputProfile);
|
|
101
|
+
const resources = profile.resources;
|
|
102
|
+
if (!resources)
|
|
103
|
+
return profile;
|
|
104
|
+
const refs = [
|
|
105
|
+
...(resources.files?.map((mount) => mount.resource) ?? []),
|
|
106
|
+
...(resources.tools ?? []),
|
|
107
|
+
...(resources.skills ?? []),
|
|
108
|
+
...(resources.agents ?? []),
|
|
109
|
+
...(resources.commands ?? []),
|
|
110
|
+
...(typeof resources.instructions === "object"
|
|
111
|
+
? [resources.instructions]
|
|
112
|
+
: []),
|
|
113
|
+
];
|
|
114
|
+
if (refs.length > MAX_PROFILE_RESOURCE_REFS) {
|
|
115
|
+
throw new Error(`AgentProfile may contain at most ${MAX_PROFILE_RESOURCE_REFS} resource references`);
|
|
116
|
+
}
|
|
117
|
+
const batchController = new AbortController();
|
|
118
|
+
let fetchedBytes = 0;
|
|
119
|
+
const consumeFetchedBytes = (bytes) => {
|
|
120
|
+
fetchedBytes += bytes;
|
|
121
|
+
if (fetchedBytes > MAX_PROFILE_RESOURCE_BYTES) {
|
|
122
|
+
throw new Error(`GitHub profile resources exceed ${MAX_PROFILE_RESOURCE_BYTES} total bytes`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const resolveRef = async (ref) => {
|
|
126
|
+
if (ref.kind === "inline")
|
|
127
|
+
return ref;
|
|
128
|
+
const repository = ref.repository ?? options.repository;
|
|
129
|
+
if (!repository) {
|
|
130
|
+
throw new Error(`GitHub profile resource "${ref.path}" requires repository`);
|
|
131
|
+
}
|
|
132
|
+
const [owner, repo, ...extraRepositoryParts] = repository.split("/");
|
|
133
|
+
if (!owner ||
|
|
134
|
+
!repo ||
|
|
135
|
+
extraRepositoryParts.length > 0 ||
|
|
136
|
+
!/^[A-Za-z0-9_.-]+$/.test(owner) ||
|
|
137
|
+
!/^[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
138
|
+
throw new Error(`GitHub profile resource repository must be owner/repo: ${repository}`);
|
|
139
|
+
}
|
|
140
|
+
const resourcePath = normalizeWorkspaceRelativePath(ref.path);
|
|
141
|
+
const gitRef = ref.ref ?? "main";
|
|
142
|
+
if (!gitRef ||
|
|
143
|
+
hasControlCharacter(gitRef) ||
|
|
144
|
+
gitRef.split("/").some((segment) => !segment || segment === "..")) {
|
|
145
|
+
throw new Error(`Invalid GitHub profile resource ref: ${gitRef}`);
|
|
146
|
+
}
|
|
147
|
+
const encoded = (value) => value.split("/").map(encodeURIComponent).join("/");
|
|
148
|
+
const url = `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encoded(gitRef)}/${encoded(resourcePath)}`;
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const abortRequest = () => controller.abort(batchController.signal.reason);
|
|
151
|
+
batchController.signal.addEventListener("abort", abortRequest, {
|
|
152
|
+
once: true,
|
|
153
|
+
});
|
|
154
|
+
const timeoutError = new Error(`GitHub profile resource timed out: ${repository}/${resourcePath}@${gitRef}`);
|
|
155
|
+
const timeout = setTimeout(() => controller.abort(timeoutError), options.timeoutMs ?? DEFAULT_RESOURCE_FETCH_TIMEOUT_MS);
|
|
156
|
+
try {
|
|
157
|
+
const fetchResource = options.fetch ?? globalThis.fetch;
|
|
158
|
+
const response = await fetchResource(url, { signal: controller.signal });
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`Failed to fetch GitHub profile resource ${repository}/${resourcePath}@${gitRef}: ${response.status} ${response.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
163
|
+
if (Number.isFinite(contentLength) &&
|
|
164
|
+
contentLength > MAX_RESOURCE_BYTES) {
|
|
165
|
+
throw new Error(`GitHub profile resource exceeds ${MAX_RESOURCE_BYTES} bytes: ${repository}/${resourcePath}`);
|
|
166
|
+
}
|
|
167
|
+
const content = await readResourceBody(response, `${repository}/${resourcePath}`, consumeFetchedBytes);
|
|
168
|
+
return {
|
|
169
|
+
kind: "inline",
|
|
170
|
+
name: ref.name ?? basename(resourcePath),
|
|
171
|
+
content,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (controller.signal.aborted && controller.signal.reason !== undefined) {
|
|
176
|
+
throw controller.signal.reason;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
batchController.signal.removeEventListener("abort", abortRequest);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const resolvedRefs = await mapWithConcurrency(refs, MAX_CONCURRENT_RESOURCE_FETCHES, batchController, resolveRef);
|
|
186
|
+
let resolvedIndex = 0;
|
|
187
|
+
const takeRef = () => resolvedRefs[resolvedIndex++];
|
|
188
|
+
const takeRefs = (values) => values?.map(takeRef);
|
|
189
|
+
const resolvedResources = {
|
|
190
|
+
...resources,
|
|
191
|
+
files: resources.files === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: resources.files.map((mount) => ({
|
|
194
|
+
...mount,
|
|
195
|
+
resource: takeRef(),
|
|
196
|
+
})),
|
|
197
|
+
tools: takeRefs(resources.tools),
|
|
198
|
+
skills: takeRefs(resources.skills),
|
|
199
|
+
agents: takeRefs(resources.agents),
|
|
200
|
+
commands: takeRefs(resources.commands),
|
|
201
|
+
instructions: typeof resources.instructions === "object"
|
|
202
|
+
? takeRef()
|
|
203
|
+
: resources.instructions,
|
|
204
|
+
};
|
|
205
|
+
return agentProfileSchema.parse({ ...profile, resources: resolvedResources });
|
|
206
|
+
}
|
|
207
|
+
/** Normalize any skill markdown (VB frontmatter `skill:`/`description:`, or raw) into a
|
|
208
|
+
* harness SKILL.md with `name:`/`description:` frontmatter the harnesses require. */
|
|
209
|
+
export function normalizeSkillMd(name, raw) {
|
|
210
|
+
const m = raw.match(FRONTMATTER_RE);
|
|
211
|
+
const fm = m?.[1] ?? "";
|
|
212
|
+
const body = (m?.[2] ?? raw).trim();
|
|
213
|
+
let desc = `Skill ${name}. Use when the task matches its domain.`;
|
|
214
|
+
if (fm) {
|
|
215
|
+
const lines = fm.split("\n");
|
|
216
|
+
const i = lines.findIndex((l) => /^description:/.test(l));
|
|
217
|
+
if (i >= 0) {
|
|
218
|
+
const inline = (lines[i] ?? "").replace(/^description:\s*/, "").trim();
|
|
219
|
+
if (inline && ![">", "|", ">-", "|-"].includes(inline))
|
|
220
|
+
desc = collapse(inline);
|
|
221
|
+
else {
|
|
222
|
+
const buf = [];
|
|
223
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
224
|
+
const ln = lines[j] ?? "";
|
|
225
|
+
if (/^\s+\S/.test(ln))
|
|
226
|
+
buf.push(ln.trim());
|
|
227
|
+
else if (ln.trim() === "")
|
|
228
|
+
continue;
|
|
229
|
+
else
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (buf.length)
|
|
233
|
+
desc = collapse(buf.join(" "));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return `---\nname: ${name}\ndescription: ${JSON.stringify(desc)}\n---\n\n${body}\n`;
|
|
238
|
+
}
|
|
239
|
+
import { basename } from "node:path";
|
|
240
|
+
import { agentProfileSchema } from "@tangle-network/agent-interface";
|
|
241
|
+
import { hasControlCharacter, normalizeWorkspaceRelativePath, } from "./workspace-files.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function materializeSubagent(h: HarnessId, name: string, sa: AgentSubagentProfile, plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
|
|
2
|
+
export declare function codexAgentRoleName(resourceName: string): string | undefined;
|
|
3
|
+
export declare function registerCodexAgent(plan: WorkspacePlan, name: string, configFile: string, description: string): void;
|
|
4
|
+
import type { AgentSubagentProfile } from "@tangle-network/agent-interface";
|
|
5
|
+
import type { AddPlanFile } from "./plan-json.js";
|
|
6
|
+
import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export function materializeSubagent(h, name, sa, plan, add, unsupported) {
|
|
2
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) {
|
|
3
|
+
unsupported("subagents", `${h}: subagent name "${name}" must use lowercase letters, digits, underscores, or hyphens`);
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
switch (h) {
|
|
7
|
+
case "claude-code":
|
|
8
|
+
case "nanoclaw": {
|
|
9
|
+
const fm = ["---", `name: ${name}`];
|
|
10
|
+
fm.push(`description: ${JSON.stringify(sa.description ?? name)}`);
|
|
11
|
+
if (sa.model)
|
|
12
|
+
fm.push(`model: ${JSON.stringify(sa.model)}`);
|
|
13
|
+
if (sa.tools) {
|
|
14
|
+
const tools = Object.entries(sa.tools);
|
|
15
|
+
const allowed = tools
|
|
16
|
+
.filter(([, enabled]) => enabled)
|
|
17
|
+
.map(([tool]) => tool);
|
|
18
|
+
const denied = tools
|
|
19
|
+
.filter(([, enabled]) => !enabled)
|
|
20
|
+
.map(([tool]) => tool);
|
|
21
|
+
if (allowed.length > 0)
|
|
22
|
+
fm.push(`tools: ${JSON.stringify(allowed)}`);
|
|
23
|
+
if (denied.length > 0)
|
|
24
|
+
fm.push(`disallowedTools: ${JSON.stringify(denied)}`);
|
|
25
|
+
}
|
|
26
|
+
if (sa.maxSteps !== undefined)
|
|
27
|
+
fm.push(`maxTurns: ${sa.maxSteps}`);
|
|
28
|
+
reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model", "tools", "maxSteps"]), unsupported);
|
|
29
|
+
fm.push("---", "", sa.prompt ?? sa.description ?? name);
|
|
30
|
+
add(`.claude/agents/${name}.md`, `${fm.join("\n")}\n`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "opencode": {
|
|
34
|
+
const fm = [
|
|
35
|
+
"---",
|
|
36
|
+
`description: ${JSON.stringify(sa.description ?? name)}`,
|
|
37
|
+
"mode: subagent",
|
|
38
|
+
];
|
|
39
|
+
if (sa.model)
|
|
40
|
+
fm.push(`model: ${JSON.stringify(sa.model)}`);
|
|
41
|
+
if (sa.tools)
|
|
42
|
+
fm.push(`tools: ${JSON.stringify(sa.tools)}`);
|
|
43
|
+
if (sa.permissions)
|
|
44
|
+
fm.push(`permission: ${JSON.stringify(sa.permissions)}`);
|
|
45
|
+
if (sa.maxSteps !== undefined)
|
|
46
|
+
fm.push(`steps: ${sa.maxSteps}`);
|
|
47
|
+
reportUnsupportedSubagentFields(h, name, sa, new Set([
|
|
48
|
+
"description",
|
|
49
|
+
"prompt",
|
|
50
|
+
"model",
|
|
51
|
+
"tools",
|
|
52
|
+
"permissions",
|
|
53
|
+
"maxSteps",
|
|
54
|
+
]), unsupported);
|
|
55
|
+
fm.push("---", "", sa.prompt ?? "");
|
|
56
|
+
add(`.opencode/agents/${name}.md`, `${fm.join("\n")}\n`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "gemini": {
|
|
60
|
+
const tools = sa.tools
|
|
61
|
+
? Object.entries(sa.tools)
|
|
62
|
+
.filter(([, enabled]) => enabled)
|
|
63
|
+
.map(([tool]) => tool)
|
|
64
|
+
: undefined;
|
|
65
|
+
reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model", "tools", "maxSteps"]), unsupported);
|
|
66
|
+
add(`.gemini/agents/${name}.md`, [
|
|
67
|
+
"---",
|
|
68
|
+
`name: ${name}`,
|
|
69
|
+
`description: ${JSON.stringify(sa.description ?? name)}`,
|
|
70
|
+
"kind: local",
|
|
71
|
+
...(sa.model ? [`model: ${JSON.stringify(sa.model)}`] : []),
|
|
72
|
+
...(tools ? [`tools: ${JSON.stringify(tools)}`] : []),
|
|
73
|
+
...(sa.maxSteps !== undefined ? [`max_turns: ${sa.maxSteps}`] : []),
|
|
74
|
+
"---",
|
|
75
|
+
"",
|
|
76
|
+
sa.prompt ?? "",
|
|
77
|
+
"",
|
|
78
|
+
].join("\n"));
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "codex": {
|
|
82
|
+
const relPath = `.codex/agents/${name}.toml`;
|
|
83
|
+
const lines = [
|
|
84
|
+
`name = ${tomlStr(name)}`,
|
|
85
|
+
`description = ${tomlStr(sa.description ?? name)}`,
|
|
86
|
+
`developer_instructions = ${tomlStr(sa.prompt ?? sa.description ?? name)}`,
|
|
87
|
+
];
|
|
88
|
+
if (sa.model)
|
|
89
|
+
lines.push(`model = ${tomlStr(sa.model)}`);
|
|
90
|
+
reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model"]), unsupported);
|
|
91
|
+
add(relPath, `${lines.join("\n")}\n`);
|
|
92
|
+
registerCodexAgent(plan, name, relPath, sa.description ?? name);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "prime":
|
|
96
|
+
// Prime subagents ARE expressible — as entries in the continual-harness
|
|
97
|
+
// store at <agentDir>/harness/harness_state.json (fork refinement.ts:269-279,
|
|
98
|
+
// re-read on every system-prompt build: agent-session.ts:7548-7554) — but
|
|
99
|
+
// that store lives under the executor-owned agent dir, which no
|
|
100
|
+
// workspace-relative plan file can reach. Executors that own the agent
|
|
101
|
+
// dir seed it with seedPrimeProfileSubagents; the plan stays fail-closed
|
|
102
|
+
// so a plan-forwarding caller can never believe the roster is in force.
|
|
103
|
+
unsupported("subagents", `prime: subagents live in the agent-dir continual-harness store (<agentDir>/harness/harness_state.json), outside any workspace plan — seed "${name}" with seedPrimeProfileSubagents from the executor that owns the agent dir`);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
unsupported("subagents", `${h}: subagents have no cwd file (config/flag/global only); "${name}" not mounted`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function codexAgentRoleName(resourceName) {
|
|
110
|
+
const name = resourceName.endsWith(".toml")
|
|
111
|
+
? resourceName.slice(0, -".toml".length)
|
|
112
|
+
: resourceName;
|
|
113
|
+
return /^[a-z0-9][a-z0-9_-]*$/.test(name) ? name : undefined;
|
|
114
|
+
}
|
|
115
|
+
export function registerCodexAgent(plan, name, configFile, description) {
|
|
116
|
+
plan.flags.push("-c", `agents.${name}.description=${tomlStr(description)}`, "-c", `agents.${name}.config_file=${tomlStr(configFile)}`);
|
|
117
|
+
}
|
|
118
|
+
function reportUnsupportedSubagentFields(harness, name, subagent, supported, unsupported) {
|
|
119
|
+
for (const field of [
|
|
120
|
+
"description",
|
|
121
|
+
"prompt",
|
|
122
|
+
"model",
|
|
123
|
+
"tools",
|
|
124
|
+
"permissions",
|
|
125
|
+
"maxSteps",
|
|
126
|
+
"metadata",
|
|
127
|
+
]) {
|
|
128
|
+
if (subagent[field] !== undefined && !supported.has(field)) {
|
|
129
|
+
unsupported("subagents", `${harness}: subagent "${name}" does not support ${field}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
import { tomlStr } from "./plan-json.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function applyResolvedWorkspacePlan(publicPlan: WorkspacePlan, resolvedPlan: ResolvedWorkspacePlan, workspaceDir: string, options: ApplyWorkspacePlanOptions, publicPlanDigest?: string): WorkspacePlanApplication;
|
|
2
|
+
export interface ResolvedWorkspacePlan {
|
|
3
|
+
files: NormalizedPlanFile[];
|
|
4
|
+
env: Record<string, string>;
|
|
5
|
+
flags: string[];
|
|
6
|
+
systemPrompt?: string;
|
|
7
|
+
appendSystemPrompt?: string;
|
|
8
|
+
systemPromptBinding?: WorkspacePlanSystemPromptBinding;
|
|
9
|
+
}
|
|
10
|
+
export declare function safeWorkspacePlanReceipt(plan: WorkspacePlan, written: string[], workspacePlanDigest?: string): WorkspacePlanReceipt;
|
|
11
|
+
import { type NormalizedPlanFile } from "./plan-normalize.js";
|
|
12
|
+
import type { ApplyWorkspacePlanOptions, WorkspacePlan, WorkspacePlanApplication, WorkspacePlanReceipt, WorkspacePlanSystemPromptBinding } from "./plan-types.js";
|