agentopology 0.1.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 +191 -0
- package/README.md +450 -0
- package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
- package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/analyzer/__tests__/analyzer.test.js +258 -0
- package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
- package/dist/analyzer/index.d.ts +44 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +308 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
- package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/bindings.test.js +2089 -0
- package/dist/bindings/__tests__/bindings.test.js.map +1 -0
- package/dist/bindings/claude-code.d.ts +12 -0
- package/dist/bindings/claude-code.d.ts.map +1 -0
- package/dist/bindings/claude-code.js +2063 -0
- package/dist/bindings/claude-code.js.map +1 -0
- package/dist/bindings/codex.d.ts +18 -0
- package/dist/bindings/codex.d.ts.map +1 -0
- package/dist/bindings/codex.js +1633 -0
- package/dist/bindings/codex.js.map +1 -0
- package/dist/bindings/copilot-cli.d.ts +20 -0
- package/dist/bindings/copilot-cli.d.ts.map +1 -0
- package/dist/bindings/copilot-cli.js +1515 -0
- package/dist/bindings/copilot-cli.js.map +1 -0
- package/dist/bindings/cursor.d.ts +17 -0
- package/dist/bindings/cursor.d.ts.map +1 -0
- package/dist/bindings/cursor.js +1715 -0
- package/dist/bindings/cursor.js.map +1 -0
- package/dist/bindings/gemini-cli.d.ts +18 -0
- package/dist/bindings/gemini-cli.d.ts.map +1 -0
- package/dist/bindings/gemini-cli.js +1327 -0
- package/dist/bindings/gemini-cli.js.map +1 -0
- package/dist/bindings/index.d.ts +20 -0
- package/dist/bindings/index.d.ts.map +1 -0
- package/dist/bindings/index.js +33 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/bindings/kiro.d.ts +17 -0
- package/dist/bindings/kiro.d.ts.map +1 -0
- package/dist/bindings/kiro.js +1624 -0
- package/dist/bindings/kiro.js.map +1 -0
- package/dist/bindings/openclaw.d.ts +18 -0
- package/dist/bindings/openclaw.d.ts.map +1 -0
- package/dist/bindings/openclaw.js +1578 -0
- package/dist/bindings/openclaw.js.map +1 -0
- package/dist/bindings/types.d.ts +33 -0
- package/dist/bindings/types.d.ts.map +1 -0
- package/dist/bindings/types.js +23 -0
- package/dist/bindings/types.js.map +1 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +679 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/docs/__tests__/docs.test.d.ts +2 -0
- package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
- package/dist/docs/__tests__/docs.test.js +438 -0
- package/dist/docs/__tests__/docs.test.js.map +1 -0
- package/dist/docs/content.d.ts +16 -0
- package/dist/docs/content.d.ts.map +1 -0
- package/dist/docs/content.js +3273 -0
- package/dist/docs/content.js.map +1 -0
- package/dist/docs/index.d.ts +17 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +117 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
- package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
- package/dist/exporters/__tests__/exporters.test.js +731 -0
- package/dist/exporters/__tests__/exporters.test.js.map +1 -0
- package/dist/exporters/index.d.ts +13 -0
- package/dist/exporters/index.d.ts.map +1 -0
- package/dist/exporters/index.js +18 -0
- package/dist/exporters/index.js.map +1 -0
- package/dist/exporters/json.d.ts +8 -0
- package/dist/exporters/json.d.ts.map +1 -0
- package/dist/exporters/json.js +25 -0
- package/dist/exporters/json.js.map +1 -0
- package/dist/exporters/markdown.d.ts +9 -0
- package/dist/exporters/markdown.d.ts.map +1 -0
- package/dist/exporters/markdown.js +686 -0
- package/dist/exporters/markdown.js.map +1 -0
- package/dist/exporters/mermaid.d.ts +11 -0
- package/dist/exporters/mermaid.d.ts.map +1 -0
- package/dist/exporters/mermaid.js +161 -0
- package/dist/exporters/mermaid.js.map +1 -0
- package/dist/exporters/types.d.ts +22 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +10 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/import/__tests__/import.test.d.ts +2 -0
- package/dist/import/__tests__/import.test.d.ts.map +1 -0
- package/dist/import/__tests__/import.test.js +781 -0
- package/dist/import/__tests__/import.test.js.map +1 -0
- package/dist/import/claude-code.d.ts +38 -0
- package/dist/import/claude-code.d.ts.map +1 -0
- package/dist/import/claude-code.js +897 -0
- package/dist/import/claude-code.js.map +1 -0
- package/dist/import/index.d.ts +18 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +28 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/serializer.d.ts +11 -0
- package/dist/import/serializer.d.ts.map +1 -0
- package/dist/import/serializer.js +960 -0
- package/dist/import/serializer.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/__tests__/parser.test.d.ts +2 -0
- package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
- package/dist/parser/__tests__/parser.test.js +6514 -0
- package/dist/parser/__tests__/parser.test.js.map +1 -0
- package/dist/parser/ast.d.ts +708 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +12 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/index.d.ts +253 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +1985 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/lexer.d.ts +100 -0
- package/dist/parser/lexer.d.ts.map +1 -0
- package/dist/parser/lexer.js +304 -0
- package/dist/parser/lexer.js.map +1 -0
- package/dist/parser/validator.d.ts +42 -0
- package/dist/parser/validator.d.ts.map +1 -0
- package/dist/parser/validator.js +2568 -0
- package/dist/parser/validator.js.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
- package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.js +295 -0
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
- package/dist/scaffold/incremental.d.ts +43 -0
- package/dist/scaffold/incremental.d.ts.map +1 -0
- package/dist/scaffold/incremental.js +185 -0
- package/dist/scaffold/incremental.js.map +1 -0
- package/dist/scaffold/index.d.ts +13 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +13 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/manifest.d.ts +30 -0
- package/dist/scaffold/manifest.d.ts.map +1 -0
- package/dist/scaffold/manifest.js +71 -0
- package/dist/scaffold/manifest.js.map +1 -0
- package/dist/scaffold/merge.d.ts +52 -0
- package/dist/scaffold/merge.d.ts.map +1 -0
- package/dist/scaffold/merge.js +134 -0
- package/dist/scaffold/merge.js.map +1 -0
- package/dist/scaffold/types.d.ts +51 -0
- package/dist/scaffold/types.d.ts.map +1 -0
- package/dist/scaffold/types.js +10 -0
- package/dist/scaffold/types.js.map +1 -0
- package/dist/sync/index.d.ts +34 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +392 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/visualizer/index.d.ts +17 -0
- package/dist/visualizer/index.d.ts.map +1 -0
- package/dist/visualizer/index.js +1976 -0
- package/dist/visualizer/index.js.map +1 -0
- package/examples/code-review.at +167 -0
- package/examples/data-processing.at +296 -0
- package/examples/openclaw-assistant.at +142 -0
- package/examples/scheduled-monitor.at +193 -0
- package/examples/simple-pipeline.at +163 -0
- package/package.json +77 -0
- package/skill/SKILL.md +403 -0
- package/skill/scripts/emit.ts +853 -0
- package/skill/scripts/visualize.ts +89 -0
- package/spec/grammar.md +1466 -0
- package/spec/reserved-keywords.md +147 -0
- package/spec/validation.md +535 -0
|
@@ -0,0 +1,2063 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code binding — reference implementation.
|
|
3
|
+
*
|
|
4
|
+
* Generates the `.claude/` directory structure, `.mcp.json`, and supporting
|
|
5
|
+
* scripts from a parsed {@link TopologyAST}.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { deduplicateFiles } from "./types.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Convert .at MCP tool notation (mcp.server.method) to Claude Code format (mcp__server__method).
|
|
15
|
+
* Non-MCP tool names pass through unchanged.
|
|
16
|
+
*/
|
|
17
|
+
function convertMcpToolName(tool) {
|
|
18
|
+
if (!tool.startsWith("mcp."))
|
|
19
|
+
return tool;
|
|
20
|
+
// mcp.server-name.method_name → mcp__server-name__method_name
|
|
21
|
+
// Only replace the first two dots (mcp + server separator)
|
|
22
|
+
const parts = tool.split(".");
|
|
23
|
+
if (parts.length >= 3) {
|
|
24
|
+
return `mcp__${parts[1]}__${parts.slice(2).join(".")}`;
|
|
25
|
+
}
|
|
26
|
+
return tool;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Downshift all markdown headings by one level so they nest properly
|
|
30
|
+
* inside ## Instructions. ## → ###, ### → ####, etc.
|
|
31
|
+
*/
|
|
32
|
+
function downshiftHeadings(text) {
|
|
33
|
+
return text.replace(/^(#{2,6})\s/gm, (_, hashes) => `#${hashes} `);
|
|
34
|
+
}
|
|
35
|
+
/** Convert a kebab-case id to Title Case. */
|
|
36
|
+
function toTitle(id) {
|
|
37
|
+
return id
|
|
38
|
+
.split(/[-_]/)
|
|
39
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Wrap content in a YAML frontmatter block.
|
|
44
|
+
*
|
|
45
|
+
* Supports scalar values, booleans, and arrays.
|
|
46
|
+
* Arrays are rendered as multi-line YAML lists:
|
|
47
|
+
* key:
|
|
48
|
+
* - item1
|
|
49
|
+
* - item2
|
|
50
|
+
*/
|
|
51
|
+
function frontmatter(fields) {
|
|
52
|
+
const lines = ["---"];
|
|
53
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
54
|
+
if (value === undefined || value === null)
|
|
55
|
+
continue;
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
if (value.length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
lines.push(`${key}:`);
|
|
60
|
+
for (const item of value) {
|
|
61
|
+
lines.push(` - ${item}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (typeof value === "boolean") {
|
|
65
|
+
lines.push(`${key}: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push(`${key}: ${value}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
lines.push("---");
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
/** Generate a shell script stub with a header comment. */
|
|
75
|
+
function shellStub(description) {
|
|
76
|
+
return [
|
|
77
|
+
"#!/usr/bin/env bash",
|
|
78
|
+
`# ${description}`,
|
|
79
|
+
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
80
|
+
'set -euo pipefail',
|
|
81
|
+
"",
|
|
82
|
+
'echo "TODO: implement this script"',
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
/** Generate an empty directory marker (a .gitkeep file). */
|
|
87
|
+
function gitkeep(dirPath) {
|
|
88
|
+
return { path: `${dirPath}/.gitkeep`, content: "", category: "machine" };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Map topology permission strings to Claude Code permissionMode values.
|
|
92
|
+
* Returns undefined when the permission mode should be omitted (default/autonomous).
|
|
93
|
+
*/
|
|
94
|
+
function mapPermissionMode(perm) {
|
|
95
|
+
// Claude Code native values — pass through
|
|
96
|
+
const nativeValues = ["plan", "auto", "confirm", "bypassPermissions"];
|
|
97
|
+
if (nativeValues.includes(perm)) {
|
|
98
|
+
// "auto" is the default, omit it
|
|
99
|
+
if (perm === "auto")
|
|
100
|
+
return undefined;
|
|
101
|
+
return perm;
|
|
102
|
+
}
|
|
103
|
+
// Topology-level semantic values
|
|
104
|
+
switch (perm) {
|
|
105
|
+
case "supervised":
|
|
106
|
+
return "plan";
|
|
107
|
+
case "autonomous":
|
|
108
|
+
return undefined; // default, omit
|
|
109
|
+
case "interactive":
|
|
110
|
+
return "default";
|
|
111
|
+
case "unrestricted":
|
|
112
|
+
return "bypassPermissions";
|
|
113
|
+
default:
|
|
114
|
+
return perm; // unknown — pass through as-is
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Format a SchemaType to a human-readable string. Optionally resolves refs. */
|
|
118
|
+
function formatSchemaType(t, schemas) {
|
|
119
|
+
switch (t.kind) {
|
|
120
|
+
case "primitive":
|
|
121
|
+
return t.value;
|
|
122
|
+
case "array":
|
|
123
|
+
return `${formatSchemaType(t.itemType, schemas)}[]`;
|
|
124
|
+
case "enum":
|
|
125
|
+
return t.values.join(" | ");
|
|
126
|
+
case "ref": {
|
|
127
|
+
if (schemas) {
|
|
128
|
+
const resolved = schemas.find((s) => s.id === t.name);
|
|
129
|
+
if (resolved) {
|
|
130
|
+
const fields = resolved.fields.map((f) => `${f.name}${f.optional ? "?" : ""}: ${formatSchemaType(f.type, schemas)}`);
|
|
131
|
+
return `${t.name} { ${fields.join(", ")} }`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return t.name;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Format a SchemaFieldDef to a readable line. */
|
|
139
|
+
function formatSchemaField(f) {
|
|
140
|
+
const opt = f.optional ? "?" : "";
|
|
141
|
+
return `- ${f.name}${opt}: ${formatSchemaType(f.type)}`;
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Section generators
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
/** Generate script stubs for each ActionNode. */
|
|
147
|
+
function generateActionNodes(ast) {
|
|
148
|
+
const files = [];
|
|
149
|
+
const name = ast.topology.name;
|
|
150
|
+
for (const node of ast.nodes) {
|
|
151
|
+
if (node.type !== "action")
|
|
152
|
+
continue;
|
|
153
|
+
const action = node;
|
|
154
|
+
const lines = ["#!/usr/bin/env bash"];
|
|
155
|
+
lines.push(`# Action: ${action.id} — ${action.description ?? action.label}`);
|
|
156
|
+
lines.push("# Auto-generated by agentopology scaffold — edit as needed.");
|
|
157
|
+
lines.push('set -euo pipefail');
|
|
158
|
+
lines.push("");
|
|
159
|
+
if (action.timeout) {
|
|
160
|
+
lines.push(`# Timeout: ${action.timeout}`);
|
|
161
|
+
}
|
|
162
|
+
if (action.onFail) {
|
|
163
|
+
lines.push(`# On failure: ${action.onFail}`);
|
|
164
|
+
}
|
|
165
|
+
if (action.join) {
|
|
166
|
+
lines.push(`# Join semantics: ${action.join}`);
|
|
167
|
+
}
|
|
168
|
+
switch (action.kind) {
|
|
169
|
+
case "external":
|
|
170
|
+
lines.push(`# External source: ${action.source ?? "unknown"}`);
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(`echo "TODO: implement external action — fetch from ${action.source ?? "source"}"`);
|
|
173
|
+
break;
|
|
174
|
+
case "inline":
|
|
175
|
+
if (action.commands && action.commands.length > 0) {
|
|
176
|
+
lines.push("");
|
|
177
|
+
for (const cmd of action.commands) {
|
|
178
|
+
lines.push(cmd);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push('echo "TODO: implement inline action"');
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case "git":
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("# Git operation");
|
|
189
|
+
if (action.commands && action.commands.length > 0) {
|
|
190
|
+
for (const cmd of action.commands) {
|
|
191
|
+
lines.push(cmd);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
lines.push('echo "TODO: implement git action"');
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case "report":
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push("# Report generation");
|
|
201
|
+
lines.push('echo "TODO: implement report action"');
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(`echo "TODO: implement ${action.kind ?? "unknown"} action"`);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
lines.push("");
|
|
209
|
+
files.push({
|
|
210
|
+
path: `.claude/skills/${name}/scripts/action-${action.id}.sh`,
|
|
211
|
+
content: lines.join("\n"),
|
|
212
|
+
category: "script",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return files;
|
|
216
|
+
}
|
|
217
|
+
/** Generate AGENT.md files for each agent node. */
|
|
218
|
+
function generateAgents(ast) {
|
|
219
|
+
const files = [];
|
|
220
|
+
for (const node of ast.nodes) {
|
|
221
|
+
if (node.type !== "agent")
|
|
222
|
+
continue;
|
|
223
|
+
const agent = node;
|
|
224
|
+
// Build frontmatter fields
|
|
225
|
+
const fm = {};
|
|
226
|
+
fm.name = agent.id;
|
|
227
|
+
// Description: prefer agent.description, then role description from roles block.
|
|
228
|
+
// For manual agents, omit description to prevent Claude Code auto-delegation.
|
|
229
|
+
if (agent.invocation !== "manual") {
|
|
230
|
+
const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
|
|
231
|
+
if (desc) {
|
|
232
|
+
fm.description = `"${desc}"`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (agent.model)
|
|
236
|
+
fm.model = agent.model;
|
|
237
|
+
if (agent.maxTurns != null)
|
|
238
|
+
fm.maxTurns = String(agent.maxTurns);
|
|
239
|
+
if (agent.tools && agent.tools.length > 0)
|
|
240
|
+
fm.tools = agent.tools.map(convertMcpToolName);
|
|
241
|
+
if (agent.disallowedTools && agent.disallowedTools.length > 0)
|
|
242
|
+
fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName);
|
|
243
|
+
if (agent.mcpServers && agent.mcpServers.length > 0)
|
|
244
|
+
fm.mcpServers = agent.mcpServers;
|
|
245
|
+
if (agent.background === true)
|
|
246
|
+
fm.background = true;
|
|
247
|
+
if (agent.permissions) {
|
|
248
|
+
const mapped = mapPermissionMode(agent.permissions);
|
|
249
|
+
if (mapped)
|
|
250
|
+
fm.permissionMode = mapped;
|
|
251
|
+
}
|
|
252
|
+
if (agent.isolation)
|
|
253
|
+
fm.isolation = agent.isolation;
|
|
254
|
+
// Note: sandbox and fallback-chain are not recognized by Claude Code frontmatter
|
|
255
|
+
// but are included as informational metadata for other tooling.
|
|
256
|
+
if (agent.sandbox != null) {
|
|
257
|
+
fm.sandbox = typeof agent.sandbox === "boolean" ? agent.sandbox : agent.sandbox;
|
|
258
|
+
}
|
|
259
|
+
if (agent.fallbackChain && agent.fallbackChain.length > 0) {
|
|
260
|
+
fm["fallback-chain"] = agent.fallbackChain.join(", ");
|
|
261
|
+
}
|
|
262
|
+
// invocation: manual — Claude Code doesn't parse this field. Instead, omit
|
|
263
|
+
// description to prevent auto-delegation for manual agents.
|
|
264
|
+
if (agent.skills && agent.skills.length > 0)
|
|
265
|
+
fm.skills = agent.skills;
|
|
266
|
+
// scale is NOT a Claude Code frontmatter field — rendered only in body as prompt text
|
|
267
|
+
// Merge claude-code extension fields into frontmatter
|
|
268
|
+
if (agent.extensions?.["claude-code"]) {
|
|
269
|
+
for (const [k, v] of Object.entries(agent.extensions["claude-code"])) {
|
|
270
|
+
if (typeof v === "string")
|
|
271
|
+
fm[k] = v;
|
|
272
|
+
else if (typeof v === "boolean")
|
|
273
|
+
fm[k] = v;
|
|
274
|
+
else if (Array.isArray(v))
|
|
275
|
+
fm[k] = v;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Build frontmatter string — may include hooks YAML block
|
|
279
|
+
let fmStr = frontmatter(fm);
|
|
280
|
+
// Per-agent hooks → Claude Code AGENT.md frontmatter hooks
|
|
281
|
+
if (agent.hooks && agent.hooks.length > 0) {
|
|
282
|
+
const topologyName = ast.topology.name;
|
|
283
|
+
const hooksByEvent = {};
|
|
284
|
+
for (const hook of agent.hooks) {
|
|
285
|
+
if (!hooksByEvent[hook.on])
|
|
286
|
+
hooksByEvent[hook.on] = [];
|
|
287
|
+
hooksByEvent[hook.on].push(hook);
|
|
288
|
+
}
|
|
289
|
+
const hookLines = ["hooks:"];
|
|
290
|
+
for (const [eventName, hooks] of Object.entries(hooksByEvent)) {
|
|
291
|
+
hookLines.push(` ${eventName}:`);
|
|
292
|
+
for (const hook of hooks) {
|
|
293
|
+
const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
294
|
+
const command = `bash .claude/skills/${topologyName}/scripts/${agent.id}-${scriptName}`;
|
|
295
|
+
if (hook.matcher) {
|
|
296
|
+
hookLines.push(` - matcher: "${hook.matcher}"`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
hookLines.push(` -`);
|
|
300
|
+
}
|
|
301
|
+
hookLines.push(` hooks:`);
|
|
302
|
+
hookLines.push(` - type: ${hook.type ?? "command"}`);
|
|
303
|
+
hookLines.push(` command: "${command}"`);
|
|
304
|
+
if (hook.timeout) {
|
|
305
|
+
hookLines.push(` timeout: ${hook.timeout}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Insert hooks before the closing ---
|
|
310
|
+
fmStr = fmStr.replace(/\n---$/, "\n" + hookLines.join("\n") + "\n---");
|
|
311
|
+
}
|
|
312
|
+
// Build body
|
|
313
|
+
const sections = [fmStr, ""];
|
|
314
|
+
const displayName = agent.label || toTitle(agent.id);
|
|
315
|
+
// Role section — only when role text adds info beyond the prompt
|
|
316
|
+
const roleText = ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
|
|
317
|
+
if (roleText && !agent.prompt) {
|
|
318
|
+
sections.push("## Role");
|
|
319
|
+
sections.push(roleText);
|
|
320
|
+
sections.push("");
|
|
321
|
+
}
|
|
322
|
+
sections.push("## Instructions");
|
|
323
|
+
sections.push("");
|
|
324
|
+
if (agent.prompt) {
|
|
325
|
+
sections.push(downshiftHeadings(agent.prompt));
|
|
326
|
+
}
|
|
327
|
+
else if (agent.description) {
|
|
328
|
+
sections.push(agent.description);
|
|
329
|
+
if (roleText)
|
|
330
|
+
sections.push(`\n${roleText}`);
|
|
331
|
+
}
|
|
332
|
+
else if (roleText) {
|
|
333
|
+
sections.push(roleText);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
sections.push("Complete the assigned task.");
|
|
337
|
+
}
|
|
338
|
+
sections.push("");
|
|
339
|
+
// Reads section
|
|
340
|
+
if (agent.reads && agent.reads.length > 0) {
|
|
341
|
+
sections.push("## Reads");
|
|
342
|
+
for (const r of agent.reads) {
|
|
343
|
+
sections.push(`- ${r}`);
|
|
344
|
+
}
|
|
345
|
+
sections.push("");
|
|
346
|
+
}
|
|
347
|
+
// Writes section
|
|
348
|
+
if (agent.writes && agent.writes.length > 0) {
|
|
349
|
+
sections.push("## Writes");
|
|
350
|
+
for (const w of agent.writes) {
|
|
351
|
+
sections.push(`- ${w}`);
|
|
352
|
+
}
|
|
353
|
+
sections.push("");
|
|
354
|
+
}
|
|
355
|
+
// Outputs section
|
|
356
|
+
if (agent.outputs) {
|
|
357
|
+
sections.push("## Outputs");
|
|
358
|
+
for (const [field, values] of Object.entries(agent.outputs)) {
|
|
359
|
+
sections.push(`- ${field}: ${values.join(" | ")}`);
|
|
360
|
+
}
|
|
361
|
+
sections.push("");
|
|
362
|
+
}
|
|
363
|
+
// Timeout instruction
|
|
364
|
+
if (agent.timeout) {
|
|
365
|
+
sections.push(`You have a maximum of ${agent.timeout} to complete your work.`);
|
|
366
|
+
sections.push("");
|
|
367
|
+
}
|
|
368
|
+
// On-fail instruction
|
|
369
|
+
if (agent.onFail) {
|
|
370
|
+
sections.push(`If you fail: ${agent.onFail}`);
|
|
371
|
+
sections.push("");
|
|
372
|
+
}
|
|
373
|
+
// Retry configuration
|
|
374
|
+
if (agent.retry != null) {
|
|
375
|
+
if (typeof agent.retry === "number") {
|
|
376
|
+
sections.push(`Retry up to ${agent.retry} times on failure.`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const r = agent.retry;
|
|
380
|
+
const parts = [`max ${r.max} attempts`];
|
|
381
|
+
if (r.backoff)
|
|
382
|
+
parts.push(`${r.backoff} backoff`);
|
|
383
|
+
if (r.interval)
|
|
384
|
+
parts.push(`interval ${r.interval}`);
|
|
385
|
+
if (r.maxInterval)
|
|
386
|
+
parts.push(`max interval ${r.maxInterval}`);
|
|
387
|
+
if (r.jitter)
|
|
388
|
+
parts.push(`with jitter`);
|
|
389
|
+
if (r.nonRetryable && r.nonRetryable.length > 0) {
|
|
390
|
+
parts.push(`non-retryable: ${r.nonRetryable.join(", ")}`);
|
|
391
|
+
}
|
|
392
|
+
sections.push(`Retry strategy: ${parts.join(", ")}.`);
|
|
393
|
+
}
|
|
394
|
+
sections.push("");
|
|
395
|
+
}
|
|
396
|
+
// Model Configuration section
|
|
397
|
+
const hasModelConfig = agent.temperature != null ||
|
|
398
|
+
agent.maxTokens != null ||
|
|
399
|
+
agent.topP != null ||
|
|
400
|
+
agent.topK != null ||
|
|
401
|
+
agent.stop != null ||
|
|
402
|
+
agent.seed != null;
|
|
403
|
+
if (hasModelConfig) {
|
|
404
|
+
sections.push("## Model Configuration");
|
|
405
|
+
if (agent.temperature != null)
|
|
406
|
+
sections.push(`- Temperature: ${agent.temperature}`);
|
|
407
|
+
if (agent.maxTokens != null)
|
|
408
|
+
sections.push(`- Max tokens: ${agent.maxTokens}`);
|
|
409
|
+
if (agent.topP != null)
|
|
410
|
+
sections.push(`- Top-p: ${agent.topP}`);
|
|
411
|
+
if (agent.topK != null)
|
|
412
|
+
sections.push(`- Top-k: ${agent.topK}`);
|
|
413
|
+
if (agent.stop)
|
|
414
|
+
sections.push(`- Stop sequences: ${agent.stop.join(", ")}`);
|
|
415
|
+
if (agent.seed != null)
|
|
416
|
+
sections.push(`- Seed: ${agent.seed}`);
|
|
417
|
+
sections.push("");
|
|
418
|
+
}
|
|
419
|
+
// Thinking
|
|
420
|
+
if (agent.thinking) {
|
|
421
|
+
let thinkingLine = `Use ${agent.thinking} level reasoning.`;
|
|
422
|
+
if (agent.thinkingBudget != null) {
|
|
423
|
+
thinkingLine = `Use ${agent.thinking} level reasoning (budget: ${agent.thinkingBudget} tokens).`;
|
|
424
|
+
}
|
|
425
|
+
sections.push(thinkingLine);
|
|
426
|
+
sections.push("");
|
|
427
|
+
}
|
|
428
|
+
// Output format
|
|
429
|
+
if (agent.outputFormat) {
|
|
430
|
+
sections.push(`Output format: ${agent.outputFormat}`);
|
|
431
|
+
sections.push("");
|
|
432
|
+
}
|
|
433
|
+
// Log level
|
|
434
|
+
if (agent.logLevel) {
|
|
435
|
+
sections.push(`Log level: ${agent.logLevel}`);
|
|
436
|
+
sections.push("");
|
|
437
|
+
}
|
|
438
|
+
// Join semantics
|
|
439
|
+
if (agent.join) {
|
|
440
|
+
sections.push(`Wait strategy: ${agent.join} (wait for upstream agents)`);
|
|
441
|
+
sections.push("");
|
|
442
|
+
}
|
|
443
|
+
// Circuit breaker
|
|
444
|
+
if (agent.circuitBreaker) {
|
|
445
|
+
const cb = agent.circuitBreaker;
|
|
446
|
+
sections.push("## Circuit Breaker");
|
|
447
|
+
sections.push(`- Failure threshold: ${cb.threshold}`);
|
|
448
|
+
sections.push(`- Window: ${cb.window}`);
|
|
449
|
+
sections.push(`- Cooldown: ${cb.cooldown}`);
|
|
450
|
+
sections.push("");
|
|
451
|
+
}
|
|
452
|
+
// Compensates
|
|
453
|
+
if (agent.compensates) {
|
|
454
|
+
sections.push(`This agent compensates (can undo) the work of: ${agent.compensates}`);
|
|
455
|
+
sections.push("");
|
|
456
|
+
}
|
|
457
|
+
// Input Schema
|
|
458
|
+
if (agent.inputSchema && agent.inputSchema.length > 0) {
|
|
459
|
+
sections.push("## Input Schema");
|
|
460
|
+
for (const field of agent.inputSchema) {
|
|
461
|
+
sections.push(formatSchemaField(field));
|
|
462
|
+
}
|
|
463
|
+
sections.push("");
|
|
464
|
+
}
|
|
465
|
+
// Output Schema
|
|
466
|
+
if (agent.outputSchema && agent.outputSchema.length > 0) {
|
|
467
|
+
sections.push("## Output Schema");
|
|
468
|
+
for (const field of agent.outputSchema) {
|
|
469
|
+
sections.push(formatSchemaField(field));
|
|
470
|
+
}
|
|
471
|
+
sections.push("");
|
|
472
|
+
}
|
|
473
|
+
// Rate limit
|
|
474
|
+
if (agent.rateLimit) {
|
|
475
|
+
sections.push(`Rate limit: ${agent.rateLimit}`);
|
|
476
|
+
sections.push("");
|
|
477
|
+
}
|
|
478
|
+
// Artifacts: produces/consumes
|
|
479
|
+
if ((agent.produces && agent.produces.length > 0) || (agent.consumes && agent.consumes.length > 0)) {
|
|
480
|
+
sections.push("## Artifacts");
|
|
481
|
+
if (agent.produces && agent.produces.length > 0) {
|
|
482
|
+
sections.push(`Produces: ${agent.produces.join(", ")}`);
|
|
483
|
+
}
|
|
484
|
+
if (agent.consumes && agent.consumes.length > 0) {
|
|
485
|
+
sections.push(`Consumes: ${agent.consumes.join(", ")}`);
|
|
486
|
+
}
|
|
487
|
+
sections.push("");
|
|
488
|
+
}
|
|
489
|
+
// Variants
|
|
490
|
+
if (agent.variants && agent.variants.length > 0) {
|
|
491
|
+
sections.push("## Variants");
|
|
492
|
+
for (const v of agent.variants) {
|
|
493
|
+
const parts = [`**${v.id}** (weight: ${v.weight})`];
|
|
494
|
+
if (v.model)
|
|
495
|
+
parts.push(`model: ${v.model}`);
|
|
496
|
+
if (v.temperature != null)
|
|
497
|
+
parts.push(`temperature: ${v.temperature}`);
|
|
498
|
+
sections.push(`- ${parts.join(", ")}`);
|
|
499
|
+
if (v.prompt) {
|
|
500
|
+
sections.push(` Prompt: ${v.prompt}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
sections.push("");
|
|
504
|
+
}
|
|
505
|
+
// Scale
|
|
506
|
+
if (agent.scale) {
|
|
507
|
+
const s = agent.scale;
|
|
508
|
+
sections.push("## Scale");
|
|
509
|
+
sections.push(`- Mode: ${s.mode}`);
|
|
510
|
+
sections.push(`- By: ${s.by}`);
|
|
511
|
+
sections.push(`- Min: ${s.min}`);
|
|
512
|
+
sections.push(`- Max: ${s.max}`);
|
|
513
|
+
if (s.batchSize != null)
|
|
514
|
+
sections.push(`- Batch size: ${s.batchSize}`);
|
|
515
|
+
sections.push("");
|
|
516
|
+
}
|
|
517
|
+
if (agent.behavior) {
|
|
518
|
+
sections.push(`Behavior: ${agent.behavior}`);
|
|
519
|
+
}
|
|
520
|
+
if (agent.skip) {
|
|
521
|
+
sections.push(`Skip when: ${agent.skip}`);
|
|
522
|
+
}
|
|
523
|
+
// Inject group conversation protocol if this agent is a group member
|
|
524
|
+
const groupProto = groupProtocols.get(agent.id);
|
|
525
|
+
if (groupProto) {
|
|
526
|
+
sections.push(groupProto);
|
|
527
|
+
}
|
|
528
|
+
// Inject group transcript note if this agent receives group output
|
|
529
|
+
const downstreamNote = groupDownstream.get(agent.id);
|
|
530
|
+
if (downstreamNote) {
|
|
531
|
+
sections.push(downstreamNote);
|
|
532
|
+
}
|
|
533
|
+
files.push({
|
|
534
|
+
path: `.claude/agents/${agent.id}/AGENT.md`,
|
|
535
|
+
content: sections.join("\n") + "\n",
|
|
536
|
+
category: "agent",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return files;
|
|
540
|
+
}
|
|
541
|
+
/** Generate AGENT.md files for each human node. */
|
|
542
|
+
function generateHumanNodes(ast) {
|
|
543
|
+
const files = [];
|
|
544
|
+
for (const node of ast.nodes) {
|
|
545
|
+
if (node.type !== "human")
|
|
546
|
+
continue;
|
|
547
|
+
const human = node;
|
|
548
|
+
const sections = [];
|
|
549
|
+
sections.push(`# ${toTitle(human.id)} (Human-in-the-Loop)`);
|
|
550
|
+
sections.push("");
|
|
551
|
+
if (human.description) {
|
|
552
|
+
sections.push(human.description);
|
|
553
|
+
sections.push("");
|
|
554
|
+
}
|
|
555
|
+
if (human.timeout) {
|
|
556
|
+
sections.push(`Timeout: ${human.timeout}`);
|
|
557
|
+
}
|
|
558
|
+
if (human.onTimeout) {
|
|
559
|
+
sections.push(`On timeout: ${human.onTimeout}`);
|
|
560
|
+
}
|
|
561
|
+
sections.push("");
|
|
562
|
+
files.push({
|
|
563
|
+
path: `.claude/agents/${human.id}/AGENT.md`,
|
|
564
|
+
content: sections.join("\n") + "\n",
|
|
565
|
+
category: "machine",
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return files;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Generate group chat artifacts:
|
|
572
|
+
* - Orchestrator AGENT.md — spawns members sequentially
|
|
573
|
+
* - Shared transcript file — the communication channel
|
|
574
|
+
* - Group config JSON — metadata for reference
|
|
575
|
+
* - Group protocol section injected into each member's AGENT.md (via groupProtocols map)
|
|
576
|
+
*
|
|
577
|
+
* Architecture: file-based group chat.
|
|
578
|
+
* Instead of passing context through prompts, agents communicate via a shared
|
|
579
|
+
* transcript file (`.claude/groups/<id>/transcript.md`). Each agent:
|
|
580
|
+
* 1. Reads the transcript to see all previous messages
|
|
581
|
+
* 2. Appends its response under the current round heading
|
|
582
|
+
* The orchestrator spawns agents sequentially — execution order IS the lock.
|
|
583
|
+
*/
|
|
584
|
+
/** Map of agent-id → group protocol instructions to inject into their AGENT.md. */
|
|
585
|
+
const groupProtocols = new Map();
|
|
586
|
+
/** Map of agent-id → group transcript note for downstream agents (receivers of group output). */
|
|
587
|
+
const groupDownstream = new Map();
|
|
588
|
+
function generateGroupNodes(ast) {
|
|
589
|
+
const files = [];
|
|
590
|
+
for (const node of ast.nodes) {
|
|
591
|
+
if (node.type !== "group")
|
|
592
|
+
continue;
|
|
593
|
+
const group = node;
|
|
594
|
+
const selectionMode = (group.speakerSelection ?? "sequential").replace(/^"|"$/g, "");
|
|
595
|
+
const rounds = group.maxRounds ?? 1;
|
|
596
|
+
const transcriptPath = `.claude/groups/${group.id}/transcript.md`;
|
|
597
|
+
// --- 1. Group config JSON ---
|
|
598
|
+
const config = {
|
|
599
|
+
id: group.id,
|
|
600
|
+
members: group.members,
|
|
601
|
+
speakerSelection: selectionMode,
|
|
602
|
+
maxRounds: rounds,
|
|
603
|
+
transcriptPath,
|
|
604
|
+
};
|
|
605
|
+
if (group.termination)
|
|
606
|
+
config.termination = group.termination;
|
|
607
|
+
if (group.timeout)
|
|
608
|
+
config.timeout = group.timeout;
|
|
609
|
+
files.push({
|
|
610
|
+
path: `.claude/groups/${group.id}/config.json`,
|
|
611
|
+
content: JSON.stringify(config, null, 2) + "\n",
|
|
612
|
+
category: "machine",
|
|
613
|
+
});
|
|
614
|
+
// --- 2. Transcript template ---
|
|
615
|
+
const transcriptLines = [];
|
|
616
|
+
transcriptLines.push(`# ${group.label || toTitle(group.id)}`);
|
|
617
|
+
transcriptLines.push("");
|
|
618
|
+
if (group.description) {
|
|
619
|
+
transcriptLines.push(group.description);
|
|
620
|
+
transcriptLines.push("");
|
|
621
|
+
}
|
|
622
|
+
transcriptLines.push(`**Participants:** ${group.members.join(", ")}`);
|
|
623
|
+
transcriptLines.push(`**Rounds:** ${rounds}`);
|
|
624
|
+
if (selectionMode !== "sequential") {
|
|
625
|
+
transcriptLines.push(`**Speaker order:** ${selectionMode}`);
|
|
626
|
+
}
|
|
627
|
+
transcriptLines.push("");
|
|
628
|
+
transcriptLines.push("---");
|
|
629
|
+
transcriptLines.push("");
|
|
630
|
+
transcriptLines.push("<!-- Topic will be written here by the orchestrator -->");
|
|
631
|
+
transcriptLines.push("");
|
|
632
|
+
files.push({
|
|
633
|
+
path: transcriptPath,
|
|
634
|
+
content: transcriptLines.join("\n"),
|
|
635
|
+
category: "agent",
|
|
636
|
+
});
|
|
637
|
+
// --- 3. Register protocol for each member agent ---
|
|
638
|
+
for (const member of group.members) {
|
|
639
|
+
const protocol = [
|
|
640
|
+
"",
|
|
641
|
+
"## Group Conversation Protocol",
|
|
642
|
+
"",
|
|
643
|
+
`You are participating in a group conversation via a shared transcript file: \`${transcriptPath}\``,
|
|
644
|
+
"",
|
|
645
|
+
"**Before responding:**",
|
|
646
|
+
`1. Read \`${transcriptPath}\` to see the full conversation so far`,
|
|
647
|
+
"2. Review what other participants have said",
|
|
648
|
+
"",
|
|
649
|
+
"**Your response:**",
|
|
650
|
+
`1. Append your response to \`${transcriptPath}\` under the current round heading`,
|
|
651
|
+
`2. Start with a \`### ${member}\` heading on its own line, then write your response below it`,
|
|
652
|
+
"3. Respond directly to what previous speakers said — engage with their arguments",
|
|
653
|
+
"4. Keep your response focused and concise",
|
|
654
|
+
"",
|
|
655
|
+
`**Do NOT** overwrite or replace the file contents. Only append to the end.`,
|
|
656
|
+
"",
|
|
657
|
+
].join("\n");
|
|
658
|
+
groupProtocols.set(member, protocol);
|
|
659
|
+
}
|
|
660
|
+
// --- 3b. Register downstream agents (agents receiving group output via flow edges) ---
|
|
661
|
+
for (const edge of ast.edges) {
|
|
662
|
+
if (edge.from === group.id && !group.members.includes(edge.to)) {
|
|
663
|
+
const note = [
|
|
664
|
+
"",
|
|
665
|
+
"## Group Conversation Input",
|
|
666
|
+
"",
|
|
667
|
+
`This agent receives the output of a group conversation. The full debate/discussion transcript is available at: \`${transcriptPath}\``,
|
|
668
|
+
"",
|
|
669
|
+
`**Read \`${transcriptPath}\` first** to see the complete conversation before making your evaluation or response.`,
|
|
670
|
+
"",
|
|
671
|
+
].join("\n");
|
|
672
|
+
groupDownstream.set(edge.to, note);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// --- 4. Build the orchestration body (shared between skill and agent) ---
|
|
676
|
+
const bodyLines = [];
|
|
677
|
+
if (group.description) {
|
|
678
|
+
bodyLines.push(group.description);
|
|
679
|
+
bodyLines.push("");
|
|
680
|
+
}
|
|
681
|
+
bodyLines.push("You orchestrate a group conversation using a shared transcript file. Each participant reads the transcript, sees what others wrote, and appends their response.");
|
|
682
|
+
bodyLines.push("");
|
|
683
|
+
bodyLines.push(`**Transcript file:** \`${transcriptPath}\``);
|
|
684
|
+
bodyLines.push("");
|
|
685
|
+
// Participant table
|
|
686
|
+
bodyLines.push("### Participants");
|
|
687
|
+
bodyLines.push("");
|
|
688
|
+
for (const member of group.members) {
|
|
689
|
+
const memberNode = ast.nodes.find((n) => n.id === member);
|
|
690
|
+
const memberDesc = memberNode?.type === "agent"
|
|
691
|
+
? memberNode.description ?? memberNode.label
|
|
692
|
+
: memberNode?.label ?? member;
|
|
693
|
+
const memberModel = memberNode?.type === "agent"
|
|
694
|
+
? memberNode.model : undefined;
|
|
695
|
+
const modelNote = memberModel ? ` (model: ${memberModel})` : "";
|
|
696
|
+
bodyLines.push(`- **${member}**${modelNote}: ${memberDesc}`);
|
|
697
|
+
}
|
|
698
|
+
bodyLines.push("");
|
|
699
|
+
// Step-by-step orchestration
|
|
700
|
+
bodyLines.push("### Step-by-step process");
|
|
701
|
+
bodyLines.push("");
|
|
702
|
+
bodyLines.push(`1. Write the topic/prompt at the top of \`${transcriptPath}\` (replace the placeholder comment)`);
|
|
703
|
+
bodyLines.push("");
|
|
704
|
+
const totalTurns = rounds * group.members.length;
|
|
705
|
+
let step = 2;
|
|
706
|
+
for (let round = 1; round <= rounds; round++) {
|
|
707
|
+
bodyLines.push(`**Round ${round}:**`);
|
|
708
|
+
bodyLines.push("");
|
|
709
|
+
bodyLines.push(`${step}. Append \`## Round ${round}\` to the transcript`);
|
|
710
|
+
step++;
|
|
711
|
+
for (const member of group.members) {
|
|
712
|
+
const memberNode = ast.nodes.find((n) => n.id === member);
|
|
713
|
+
const memberModel = memberNode?.type === "agent"
|
|
714
|
+
? memberNode.model : undefined;
|
|
715
|
+
const modelParam = memberModel ? `, model: ${memberModel}` : "";
|
|
716
|
+
bodyLines.push(`${step}. Spawn **${member}** (subagent_type: "${member}"${modelParam}). The agent will read the transcript, see what others wrote, and append its response.`);
|
|
717
|
+
step++;
|
|
718
|
+
}
|
|
719
|
+
bodyLines.push("");
|
|
720
|
+
}
|
|
721
|
+
bodyLines.push(`${step}. After all ${totalTurns} turns, read the final transcript and return it as your output.`);
|
|
722
|
+
bodyLines.push("");
|
|
723
|
+
if (group.termination) {
|
|
724
|
+
bodyLines.push(`**Termination condition:** ${group.termination} (but always complete the configured ${rounds} round(s) unless this condition is met earlier)`);
|
|
725
|
+
}
|
|
726
|
+
if (group.timeout) {
|
|
727
|
+
bodyLines.push(`**Timeout:** ${group.timeout}`);
|
|
728
|
+
}
|
|
729
|
+
bodyLines.push("");
|
|
730
|
+
bodyLines.push("### Important");
|
|
731
|
+
bodyLines.push("");
|
|
732
|
+
bodyLines.push("- Spawn agents **one at a time, sequentially** — never in parallel");
|
|
733
|
+
bodyLines.push("- Each agent reads the transcript BEFORE responding, so it sees all previous messages");
|
|
734
|
+
bodyLines.push("- The transcript file is the shared state — do not pass conversation history in prompts");
|
|
735
|
+
bodyLines.push("- Wait for each agent to finish before spawning the next one");
|
|
736
|
+
bodyLines.push("");
|
|
737
|
+
const orchestrationBody = bodyLines.join("\n");
|
|
738
|
+
// --- 5. Skill (primary — runs in main session, has Agent tool access) ---
|
|
739
|
+
const skillFm = {};
|
|
740
|
+
skillFm.name = group.id;
|
|
741
|
+
if (group.description)
|
|
742
|
+
skillFm.description = `"${group.description}"`;
|
|
743
|
+
skillFm["user-invocable"] = true;
|
|
744
|
+
const skillSections = [];
|
|
745
|
+
skillSections.push(frontmatter(skillFm));
|
|
746
|
+
skillSections.push("");
|
|
747
|
+
skillSections.push(`# ${group.label || toTitle(group.id)}`);
|
|
748
|
+
skillSections.push("");
|
|
749
|
+
skillSections.push(orchestrationBody);
|
|
750
|
+
files.push({
|
|
751
|
+
path: `.claude/skills/${group.id}/SKILL.md`,
|
|
752
|
+
content: skillSections.join("\n") + "\n",
|
|
753
|
+
category: "agent",
|
|
754
|
+
});
|
|
755
|
+
// --- 6. Agent (fallback — for when spawned as subagent, may not have Agent tool) ---
|
|
756
|
+
const agentFm = {};
|
|
757
|
+
agentFm.name = group.id;
|
|
758
|
+
if (group.description)
|
|
759
|
+
agentFm.description = `"${group.description}"`;
|
|
760
|
+
const agentSections = [];
|
|
761
|
+
agentSections.push(frontmatter(agentFm));
|
|
762
|
+
agentSections.push("");
|
|
763
|
+
agentSections.push("## Instructions");
|
|
764
|
+
agentSections.push("");
|
|
765
|
+
agentSections.push(orchestrationBody);
|
|
766
|
+
agentSections.push("");
|
|
767
|
+
agentSections.push("> **Note:** If the Agent tool is not available, simulate the group conversation by writing each participant's response yourself, following the round structure above. Write all responses to the transcript file.");
|
|
768
|
+
agentSections.push("");
|
|
769
|
+
files.push({
|
|
770
|
+
path: `.claude/agents/${group.id}/AGENT.md`,
|
|
771
|
+
content: agentSections.join("\n") + "\n",
|
|
772
|
+
category: "agent",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return files;
|
|
776
|
+
}
|
|
777
|
+
/** Generate the main SKILL.md for the topology (orchestrator + flow + gates). */
|
|
778
|
+
function generateTopologySkill(ast) {
|
|
779
|
+
const files = [];
|
|
780
|
+
const name = ast.topology.name;
|
|
781
|
+
// Build frontmatter
|
|
782
|
+
const fm = {};
|
|
783
|
+
fm.name = name;
|
|
784
|
+
if (ast.topology.description) {
|
|
785
|
+
fm.description = `"${ast.topology.description}"`;
|
|
786
|
+
}
|
|
787
|
+
fm.version = `"${ast.topology.version}"`;
|
|
788
|
+
fm.topology = name;
|
|
789
|
+
if (ast.topology.patterns.length > 0) {
|
|
790
|
+
fm.patterns = ast.topology.patterns;
|
|
791
|
+
}
|
|
792
|
+
// Entry: first trigger's command file
|
|
793
|
+
if (ast.triggers.length > 0) {
|
|
794
|
+
fm.entry = `commands/${ast.triggers[0].name}.md`;
|
|
795
|
+
}
|
|
796
|
+
// Add skill-level fields from the first matching skill (if skills exist)
|
|
797
|
+
// For the main topology skill, look for a skill with the same name as the topology
|
|
798
|
+
const mainSkill = ast.skills.find((s) => s.id === name);
|
|
799
|
+
if (mainSkill) {
|
|
800
|
+
if (mainSkill.disableModelInvocation != null) {
|
|
801
|
+
fm["disable-model-invocation"] = mainSkill.disableModelInvocation;
|
|
802
|
+
}
|
|
803
|
+
if (mainSkill.userInvocable != null) {
|
|
804
|
+
fm["user-invocable"] = mainSkill.userInvocable;
|
|
805
|
+
}
|
|
806
|
+
if (mainSkill.context)
|
|
807
|
+
fm.context = mainSkill.context;
|
|
808
|
+
if (mainSkill.agent)
|
|
809
|
+
fm.agent = mainSkill.agent;
|
|
810
|
+
if (mainSkill.allowedTools && mainSkill.allowedTools.length > 0) {
|
|
811
|
+
fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const sections = [];
|
|
815
|
+
sections.push(frontmatter(fm));
|
|
816
|
+
sections.push("");
|
|
817
|
+
sections.push(`# ${toTitle(name)} Topology Skill`);
|
|
818
|
+
sections.push("");
|
|
819
|
+
if (ast.topology.description) {
|
|
820
|
+
sections.push(ast.topology.description);
|
|
821
|
+
sections.push("");
|
|
822
|
+
}
|
|
823
|
+
sections.push(`Version: ${ast.topology.version}`);
|
|
824
|
+
if (ast.topology.patterns.length > 0) {
|
|
825
|
+
sections.push(`Patterns: ${ast.topology.patterns.join(", ")}`);
|
|
826
|
+
}
|
|
827
|
+
sections.push("");
|
|
828
|
+
// Topology metadata
|
|
829
|
+
if (ast.topology.foundations && ast.topology.foundations.length > 0) {
|
|
830
|
+
sections.push(`Foundations: ${ast.topology.foundations.join(", ")}`);
|
|
831
|
+
}
|
|
832
|
+
if (ast.topology.advanced && ast.topology.advanced.length > 0) {
|
|
833
|
+
sections.push(`Advanced: ${ast.topology.advanced.join(", ")}`);
|
|
834
|
+
}
|
|
835
|
+
if (ast.topology.domain) {
|
|
836
|
+
sections.push(`Domain: ${ast.topology.domain}`);
|
|
837
|
+
}
|
|
838
|
+
if (ast.topology.timeout) {
|
|
839
|
+
sections.push(`Timeout: ${ast.topology.timeout}`);
|
|
840
|
+
}
|
|
841
|
+
if (ast.topology.durable) {
|
|
842
|
+
sections.push(`Durable: yes`);
|
|
843
|
+
}
|
|
844
|
+
if (ast.topology.foundations || ast.topology.advanced || ast.topology.domain || ast.topology.timeout || ast.topology.durable) {
|
|
845
|
+
sections.push("");
|
|
846
|
+
}
|
|
847
|
+
// Fragment check — skip main skill generation for fragments
|
|
848
|
+
if (ast.isFragment) {
|
|
849
|
+
sections.push("*This is a fragment topology — it is meant to be included, not run standalone.*");
|
|
850
|
+
sections.push("");
|
|
851
|
+
}
|
|
852
|
+
// Composition: params
|
|
853
|
+
if (ast.params && ast.params.length > 0) {
|
|
854
|
+
sections.push("## Parameters");
|
|
855
|
+
sections.push("");
|
|
856
|
+
for (const p of ast.params) {
|
|
857
|
+
const req = p.required ? " (required)" : "";
|
|
858
|
+
const def = p.default != null ? ` = ${p.default}` : "";
|
|
859
|
+
sections.push(`- **${p.name}**: ${p.type}${req}${def}`);
|
|
860
|
+
}
|
|
861
|
+
sections.push("");
|
|
862
|
+
}
|
|
863
|
+
// Composition: interface endpoints
|
|
864
|
+
if (ast.interfaceEndpoints) {
|
|
865
|
+
sections.push("## Interface");
|
|
866
|
+
sections.push("");
|
|
867
|
+
sections.push(`- Entry: ${ast.interfaceEndpoints.entry}`);
|
|
868
|
+
sections.push(`- Exit: ${ast.interfaceEndpoints.exit}`);
|
|
869
|
+
sections.push("");
|
|
870
|
+
}
|
|
871
|
+
// Composition: imports
|
|
872
|
+
if (ast.imports && ast.imports.length > 0) {
|
|
873
|
+
sections.push("## Imports");
|
|
874
|
+
sections.push("");
|
|
875
|
+
for (const imp of ast.imports) {
|
|
876
|
+
let line = `- **${imp.alias}** from \`${imp.source}\``;
|
|
877
|
+
if (imp.sha256)
|
|
878
|
+
line += ` (sha256: ${imp.sha256})`;
|
|
879
|
+
if (imp.registry)
|
|
880
|
+
line += ` [registry: ${imp.registryPackage}@${imp.registryVersion}]`;
|
|
881
|
+
sections.push(line);
|
|
882
|
+
if (Object.keys(imp.params).length > 0) {
|
|
883
|
+
for (const [k, v] of Object.entries(imp.params)) {
|
|
884
|
+
sections.push(` - ${k}: ${v}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
sections.push("");
|
|
889
|
+
}
|
|
890
|
+
// Composition: includes
|
|
891
|
+
if (ast.includes && ast.includes.length > 0) {
|
|
892
|
+
sections.push("## Includes");
|
|
893
|
+
sections.push("");
|
|
894
|
+
for (const inc of ast.includes) {
|
|
895
|
+
sections.push(`- ${inc.source}`);
|
|
896
|
+
}
|
|
897
|
+
sections.push("");
|
|
898
|
+
}
|
|
899
|
+
// Error handler
|
|
900
|
+
if (ast.topology.errorHandler) {
|
|
901
|
+
sections.push("## Error Handling");
|
|
902
|
+
sections.push("");
|
|
903
|
+
sections.push(`Uncaught errors are handled by: **${ast.topology.errorHandler}**`);
|
|
904
|
+
sections.push("");
|
|
905
|
+
}
|
|
906
|
+
// Orchestrator section
|
|
907
|
+
const orch = ast.nodes.find((n) => n.type === "orchestrator");
|
|
908
|
+
if (orch) {
|
|
909
|
+
const orchNode = orch;
|
|
910
|
+
sections.push("## Orchestrator");
|
|
911
|
+
sections.push("");
|
|
912
|
+
sections.push(`Model: ${orchNode.model}`);
|
|
913
|
+
if (orchNode.handles?.length > 0) {
|
|
914
|
+
sections.push(`Handles: ${orchNode.handles.join(", ")}`);
|
|
915
|
+
}
|
|
916
|
+
if (orchNode.generates) {
|
|
917
|
+
sections.push(`Generates: ${orchNode.generates}`);
|
|
918
|
+
}
|
|
919
|
+
if (orchNode.outputs) {
|
|
920
|
+
sections.push("");
|
|
921
|
+
sections.push("### Outputs");
|
|
922
|
+
for (const [field, values] of Object.entries(orchNode.outputs)) {
|
|
923
|
+
sections.push(`- ${field}: ${values.join(" | ")}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
sections.push("");
|
|
927
|
+
}
|
|
928
|
+
// Flow section (enhanced with error edges, join, weight, race, tolerance, reflection)
|
|
929
|
+
if (ast.edges.length > 0) {
|
|
930
|
+
sections.push("## Flow");
|
|
931
|
+
sections.push("");
|
|
932
|
+
for (const edge of ast.edges) {
|
|
933
|
+
const arrow = edge.isError ? `-x${edge.errorType ? `[${edge.errorType}]` : ""}>` : "->";
|
|
934
|
+
let line = `${edge.from} ${arrow} ${edge.to}`;
|
|
935
|
+
if (edge.condition) {
|
|
936
|
+
// Clean up condition: parser may include trailing "] [max N" artifacts
|
|
937
|
+
const cleanCond = edge.condition
|
|
938
|
+
.replace(/\]\s*\[max\s+\d+\s*$/i, "")
|
|
939
|
+
.replace(/\]\s*$/, "")
|
|
940
|
+
.trim();
|
|
941
|
+
line += ` [when ${cleanCond}]`;
|
|
942
|
+
}
|
|
943
|
+
if (edge.maxIterations)
|
|
944
|
+
line += ` [max ${edge.maxIterations}]`;
|
|
945
|
+
if (edge.per)
|
|
946
|
+
line += ` [per ${edge.per}]`;
|
|
947
|
+
if (edge.weight != null)
|
|
948
|
+
line += ` [weight ${edge.weight}]`;
|
|
949
|
+
if (edge.race)
|
|
950
|
+
line += ` [race]`;
|
|
951
|
+
if (edge.tolerance != null)
|
|
952
|
+
line += ` [tolerance: ${edge.tolerance}]`;
|
|
953
|
+
if (edge.wait)
|
|
954
|
+
line += ` [wait ${edge.wait}]`;
|
|
955
|
+
if (edge.reflection)
|
|
956
|
+
line += ` [reflection]`;
|
|
957
|
+
sections.push(`- ${line}`);
|
|
958
|
+
}
|
|
959
|
+
sections.push("");
|
|
960
|
+
}
|
|
961
|
+
// Gates section
|
|
962
|
+
const gates = ast.nodes.filter((n) => n.type === "gate");
|
|
963
|
+
if (gates.length > 0) {
|
|
964
|
+
sections.push("## Gates");
|
|
965
|
+
sections.push("");
|
|
966
|
+
for (const gate of gates) {
|
|
967
|
+
sections.push(`### ${toTitle(gate.id)}`);
|
|
968
|
+
if (gate.after)
|
|
969
|
+
sections.push(`After: ${gate.after}`);
|
|
970
|
+
if (gate.before)
|
|
971
|
+
sections.push(`Before: ${gate.before}`);
|
|
972
|
+
if (gate.run)
|
|
973
|
+
sections.push(`Run: ${gate.run}`);
|
|
974
|
+
if (gate.checks && gate.checks.length > 0) {
|
|
975
|
+
sections.push(`Checks: ${gate.checks.join(", ")}`);
|
|
976
|
+
}
|
|
977
|
+
if (gate.onFail)
|
|
978
|
+
sections.push(`On fail: ${gate.onFail}`);
|
|
979
|
+
if (gate.retry)
|
|
980
|
+
sections.push(`Retry: ${gate.retry}`);
|
|
981
|
+
if (gate.timeout)
|
|
982
|
+
sections.push(`Timeout: ${gate.timeout}`);
|
|
983
|
+
sections.push("");
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// Schedules section
|
|
987
|
+
if (ast.schedules.length > 0) {
|
|
988
|
+
sections.push("## Schedules");
|
|
989
|
+
sections.push("");
|
|
990
|
+
for (const job of ast.schedules) {
|
|
991
|
+
const expr = job.cron ? `cron: ${job.cron}` : job.every ? `every: ${job.every}` : "";
|
|
992
|
+
const target = job.agent ? `agent: ${job.agent}` : job.action ? `action: ${job.action}` : "";
|
|
993
|
+
sections.push(`- **${job.id}**: ${expr}${target ? ` -> ${target}` : ""}${job.enabled === false ? " (disabled)" : ""}`);
|
|
994
|
+
}
|
|
995
|
+
sections.push("");
|
|
996
|
+
}
|
|
997
|
+
// Interfaces section
|
|
998
|
+
if (ast.interfaces.length > 0) {
|
|
999
|
+
sections.push("## Interfaces");
|
|
1000
|
+
sections.push("");
|
|
1001
|
+
for (const iface of ast.interfaces) {
|
|
1002
|
+
const typePart = iface.type ? ` (${iface.type})` : "";
|
|
1003
|
+
const configKeys = Object.keys(iface.config);
|
|
1004
|
+
const configPart = configKeys.length > 0 ? ` — ${configKeys.map((k) => `${k}: ${iface.config[k]}`).join(", ")}` : "";
|
|
1005
|
+
sections.push(`- **${iface.id}**${typePart}${configPart}`);
|
|
1006
|
+
}
|
|
1007
|
+
sections.push("");
|
|
1008
|
+
}
|
|
1009
|
+
// Depth levels
|
|
1010
|
+
if (ast.depth && ast.depth.levels && ast.depth.levels.length > 0) {
|
|
1011
|
+
sections.push("## Depth Levels");
|
|
1012
|
+
sections.push("");
|
|
1013
|
+
if (ast.depth.factors.length > 0) {
|
|
1014
|
+
sections.push(`Factors: ${ast.depth.factors.join(", ")}`);
|
|
1015
|
+
}
|
|
1016
|
+
for (const level of ast.depth.levels) {
|
|
1017
|
+
let line = `- **Level ${level.level}**: ${level.label}`;
|
|
1018
|
+
if (level.omit.length > 0)
|
|
1019
|
+
line += ` (omit: ${level.omit.join(", ")})`;
|
|
1020
|
+
sections.push(line);
|
|
1021
|
+
}
|
|
1022
|
+
sections.push("");
|
|
1023
|
+
}
|
|
1024
|
+
// Batch settings
|
|
1025
|
+
if (ast.batch && Object.keys(ast.batch).length > 0) {
|
|
1026
|
+
sections.push("## Batch");
|
|
1027
|
+
sections.push("");
|
|
1028
|
+
for (const [key, value] of Object.entries(ast.batch)) {
|
|
1029
|
+
sections.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
1030
|
+
}
|
|
1031
|
+
sections.push("");
|
|
1032
|
+
}
|
|
1033
|
+
// Environments
|
|
1034
|
+
if (ast.environments && Object.keys(ast.environments).length > 0) {
|
|
1035
|
+
sections.push("## Environments");
|
|
1036
|
+
sections.push("");
|
|
1037
|
+
for (const [envName, envConfig] of Object.entries(ast.environments)) {
|
|
1038
|
+
sections.push(`### ${envName}`);
|
|
1039
|
+
for (const [key, value] of Object.entries(envConfig)) {
|
|
1040
|
+
sections.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
1041
|
+
}
|
|
1042
|
+
sections.push("");
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// Fallback chain from settings
|
|
1046
|
+
const fallbackChain = ast.settings?.["fallback-chain"];
|
|
1047
|
+
if (fallbackChain) {
|
|
1048
|
+
sections.push("## Fallback Chain");
|
|
1049
|
+
sections.push("");
|
|
1050
|
+
if (Array.isArray(fallbackChain)) {
|
|
1051
|
+
sections.push(`Models: ${fallbackChain.join(" -> ")}`);
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
sections.push(`Fallback chain: ${fallbackChain}`);
|
|
1055
|
+
}
|
|
1056
|
+
sections.push("");
|
|
1057
|
+
}
|
|
1058
|
+
// Metering
|
|
1059
|
+
if (ast.metering) {
|
|
1060
|
+
sections.push("## Metering");
|
|
1061
|
+
sections.push("");
|
|
1062
|
+
sections.push(`- Track: ${ast.metering.track.join(", ")}`);
|
|
1063
|
+
sections.push(`- Per: ${ast.metering.per.join(", ")}`);
|
|
1064
|
+
sections.push(`- Output: ${ast.metering.output}`);
|
|
1065
|
+
sections.push(`- Format: ${ast.metering.format}`);
|
|
1066
|
+
sections.push(`- Pricing: ${ast.metering.pricing}`);
|
|
1067
|
+
sections.push("");
|
|
1068
|
+
}
|
|
1069
|
+
// Memory
|
|
1070
|
+
const memKeys = Object.keys(ast.memory);
|
|
1071
|
+
if (memKeys.length > 0) {
|
|
1072
|
+
sections.push("## Memory");
|
|
1073
|
+
sections.push("");
|
|
1074
|
+
for (const key of memKeys) {
|
|
1075
|
+
const mem = ast.memory[key];
|
|
1076
|
+
sections.push(`### ${key}`);
|
|
1077
|
+
for (const [k, v] of Object.entries(mem)) {
|
|
1078
|
+
if (Array.isArray(v)) {
|
|
1079
|
+
sections.push(`- ${k}: ${v.join(", ")}`);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
sections.push(`- ${k}: ${v}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
sections.push("");
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
files.push({
|
|
1089
|
+
path: `.claude/skills/${name}/SKILL.md`,
|
|
1090
|
+
content: sections.join("\n") + "\n",
|
|
1091
|
+
category: "agent",
|
|
1092
|
+
});
|
|
1093
|
+
return files;
|
|
1094
|
+
}
|
|
1095
|
+
/** Generate skill-level files from the skills block. */
|
|
1096
|
+
function generateSkills(ast) {
|
|
1097
|
+
const files = [];
|
|
1098
|
+
for (const skill of ast.skills) {
|
|
1099
|
+
const sections = [];
|
|
1100
|
+
// Build skill frontmatter
|
|
1101
|
+
const sfm = {};
|
|
1102
|
+
sfm.name = skill.id;
|
|
1103
|
+
if (skill.description)
|
|
1104
|
+
sfm.description = `"${skill.description}"`;
|
|
1105
|
+
if (skill.disableModelInvocation != null) {
|
|
1106
|
+
sfm["disable-model-invocation"] = skill.disableModelInvocation;
|
|
1107
|
+
}
|
|
1108
|
+
if (skill.userInvocable != null) {
|
|
1109
|
+
sfm["user-invocable"] = skill.userInvocable;
|
|
1110
|
+
}
|
|
1111
|
+
if (skill.context)
|
|
1112
|
+
sfm.context = skill.context;
|
|
1113
|
+
if (skill.agent)
|
|
1114
|
+
sfm.agent = skill.agent;
|
|
1115
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
1116
|
+
sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName);
|
|
1117
|
+
}
|
|
1118
|
+
// Only emit frontmatter if there are fields beyond just the name
|
|
1119
|
+
if (Object.keys(sfm).length > 1) {
|
|
1120
|
+
sections.push(frontmatter(sfm));
|
|
1121
|
+
sections.push("");
|
|
1122
|
+
}
|
|
1123
|
+
sections.push(`# ${toTitle(skill.id)} Skill`);
|
|
1124
|
+
sections.push("");
|
|
1125
|
+
if (skill.description) {
|
|
1126
|
+
sections.push(skill.description);
|
|
1127
|
+
sections.push("");
|
|
1128
|
+
}
|
|
1129
|
+
if (skill.scripts && skill.scripts.length > 0) {
|
|
1130
|
+
sections.push("## Scripts");
|
|
1131
|
+
for (const s of skill.scripts) {
|
|
1132
|
+
sections.push(`- ${s}`);
|
|
1133
|
+
}
|
|
1134
|
+
sections.push("");
|
|
1135
|
+
}
|
|
1136
|
+
if (skill.domains && skill.domains.length > 0) {
|
|
1137
|
+
sections.push("## Domains");
|
|
1138
|
+
for (const d of skill.domains) {
|
|
1139
|
+
sections.push(`- ${d}`);
|
|
1140
|
+
}
|
|
1141
|
+
sections.push("");
|
|
1142
|
+
}
|
|
1143
|
+
if (skill.references && skill.references.length > 0) {
|
|
1144
|
+
sections.push("## References");
|
|
1145
|
+
for (const r of skill.references) {
|
|
1146
|
+
sections.push(`- ${r}`);
|
|
1147
|
+
}
|
|
1148
|
+
sections.push("");
|
|
1149
|
+
}
|
|
1150
|
+
if (skill.prompt) {
|
|
1151
|
+
sections.push("## Prompt");
|
|
1152
|
+
sections.push(`See: ${skill.prompt}`);
|
|
1153
|
+
sections.push("");
|
|
1154
|
+
}
|
|
1155
|
+
files.push({
|
|
1156
|
+
path: `.claude/skills/${skill.id}/SKILL.md`,
|
|
1157
|
+
content: sections.join("\n") + "\n",
|
|
1158
|
+
category: "agent",
|
|
1159
|
+
});
|
|
1160
|
+
// Domain and reference directory markers
|
|
1161
|
+
if (skill.domains && skill.domains.length > 0) {
|
|
1162
|
+
files.push(gitkeep(`.claude/skills/${skill.id}/domains`));
|
|
1163
|
+
}
|
|
1164
|
+
if (skill.references && skill.references.length > 0) {
|
|
1165
|
+
files.push(gitkeep(`.claude/skills/${skill.id}/references`));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return files;
|
|
1169
|
+
}
|
|
1170
|
+
/** Generate memory structure files (domains, references, metrics, workspace). */
|
|
1171
|
+
function generateMemory(ast) {
|
|
1172
|
+
const files = [];
|
|
1173
|
+
const name = ast.topology.name;
|
|
1174
|
+
const memory = ast.memory;
|
|
1175
|
+
if (memory.domains) {
|
|
1176
|
+
files.push(gitkeep(`.claude/skills/${name}/domains`));
|
|
1177
|
+
}
|
|
1178
|
+
if (memory.references) {
|
|
1179
|
+
files.push(gitkeep(`.claude/skills/${name}/references`));
|
|
1180
|
+
}
|
|
1181
|
+
if (memory.metrics) {
|
|
1182
|
+
files.push({
|
|
1183
|
+
path: `.claude/skills/${name}/metrics.jsonl`,
|
|
1184
|
+
content: "",
|
|
1185
|
+
category: "machine",
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
if (memory.workspace) {
|
|
1189
|
+
files.push(gitkeep(`.claude/skills/${name}/runs`));
|
|
1190
|
+
}
|
|
1191
|
+
return files;
|
|
1192
|
+
}
|
|
1193
|
+
/** Generate gate script stubs. */
|
|
1194
|
+
function generateGateScripts(ast) {
|
|
1195
|
+
const files = [];
|
|
1196
|
+
const name = ast.topology.name;
|
|
1197
|
+
for (const node of ast.nodes) {
|
|
1198
|
+
if (node.type !== "gate")
|
|
1199
|
+
continue;
|
|
1200
|
+
const gate = node;
|
|
1201
|
+
if (gate.run) {
|
|
1202
|
+
// Extract script name from the run command
|
|
1203
|
+
const scriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1204
|
+
files.push({
|
|
1205
|
+
path: `.claude/skills/${name}/scripts/${scriptName}`,
|
|
1206
|
+
content: shellStub(`Gate: ${gate.id} — ${gate.run}`),
|
|
1207
|
+
category: "script",
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return files;
|
|
1212
|
+
}
|
|
1213
|
+
/** Generate gate wrapper scripts that filter Task calls to the correct agent. */
|
|
1214
|
+
function generateGateWrapperScripts(ast) {
|
|
1215
|
+
const files = [];
|
|
1216
|
+
const name = ast.topology.name;
|
|
1217
|
+
for (const node of ast.nodes) {
|
|
1218
|
+
if (node.type !== "gate")
|
|
1219
|
+
continue;
|
|
1220
|
+
const gate = node;
|
|
1221
|
+
if (!gate.run)
|
|
1222
|
+
continue;
|
|
1223
|
+
if (gate.behavior === "advisory")
|
|
1224
|
+
continue;
|
|
1225
|
+
const gateScriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1226
|
+
const onFailExit = gate.onFail === "halt" ? 'exit 1' : 'echo "Gate failed — bounce-back requested (advisory)"';
|
|
1227
|
+
const contentLines = [
|
|
1228
|
+
"#!/usr/bin/env bash",
|
|
1229
|
+
`# Gate wrapper: ${gate.id}`,
|
|
1230
|
+
`# Enforces gate "${gate.id}" on Task tool calls.`,
|
|
1231
|
+
`# Auto-generated by agentopology scaffold — edit as needed.`,
|
|
1232
|
+
"set -euo pipefail",
|
|
1233
|
+
"",
|
|
1234
|
+
"# Gate runs after: " + (gate.after || "any"),
|
|
1235
|
+
"# Gate runs before: " + (gate.before || "any"),
|
|
1236
|
+
`# On failure: ${gate.onFail || "halt"}`,
|
|
1237
|
+
];
|
|
1238
|
+
if (gate.timeout) {
|
|
1239
|
+
contentLines.push(`# Timeout: ${gate.timeout}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (gate.retry != null && gate.retry > 0) {
|
|
1242
|
+
contentLines.push("");
|
|
1243
|
+
contentLines.push(`MAX_RETRIES=${gate.retry}`);
|
|
1244
|
+
contentLines.push("ATTEMPT=0");
|
|
1245
|
+
contentLines.push("GATE_RESULT=1");
|
|
1246
|
+
contentLines.push("");
|
|
1247
|
+
contentLines.push("while [ $ATTEMPT -le $MAX_RETRIES ] && [ $GATE_RESULT -ne 0 ]; do");
|
|
1248
|
+
contentLines.push(" ATTEMPT=$((ATTEMPT + 1))");
|
|
1249
|
+
if (gate.timeout) {
|
|
1250
|
+
contentLines.push(` timeout ${gate.timeout} bash .claude/skills/${name}/scripts/${gateScriptName} && GATE_RESULT=0 || GATE_RESULT=$?`);
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
contentLines.push(` bash .claude/skills/${name}/scripts/${gateScriptName} && GATE_RESULT=0 || GATE_RESULT=$?`);
|
|
1254
|
+
}
|
|
1255
|
+
contentLines.push(' if [ $GATE_RESULT -ne 0 ] && [ $ATTEMPT -le $MAX_RETRIES ]; then');
|
|
1256
|
+
contentLines.push(` echo "Gate '${gate.id}' attempt $ATTEMPT failed, retrying..."`);
|
|
1257
|
+
contentLines.push(" sleep 1");
|
|
1258
|
+
contentLines.push(" fi");
|
|
1259
|
+
contentLines.push("done");
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
contentLines.push("");
|
|
1263
|
+
contentLines.push("# Run the gate check script");
|
|
1264
|
+
contentLines.push(`GATE_RESULT=0`);
|
|
1265
|
+
if (gate.timeout) {
|
|
1266
|
+
contentLines.push(`timeout ${gate.timeout} bash .claude/skills/${name}/scripts/${gateScriptName} || GATE_RESULT=$?`);
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
contentLines.push(`bash .claude/skills/${name}/scripts/${gateScriptName} || GATE_RESULT=$?`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
contentLines.push("");
|
|
1273
|
+
contentLines.push(`if [ "$GATE_RESULT" -ne 0 ]; then`);
|
|
1274
|
+
contentLines.push(` echo "Gate '${gate.id}' FAILED (exit code $GATE_RESULT)"`);
|
|
1275
|
+
contentLines.push(` ${onFailExit}`);
|
|
1276
|
+
contentLines.push("fi");
|
|
1277
|
+
contentLines.push("");
|
|
1278
|
+
const content = contentLines.join("\n");
|
|
1279
|
+
files.push({
|
|
1280
|
+
path: `.claude/skills/${name}/scripts/gate-${gate.id}.sh`,
|
|
1281
|
+
content,
|
|
1282
|
+
category: "script",
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
return files;
|
|
1286
|
+
}
|
|
1287
|
+
/** Generate hook script stubs for every hook's `run` field. */
|
|
1288
|
+
function generateHookScripts(ast) {
|
|
1289
|
+
const files = [];
|
|
1290
|
+
const name = ast.topology.name;
|
|
1291
|
+
for (const hook of ast.hooks) {
|
|
1292
|
+
if (!hook.run)
|
|
1293
|
+
continue;
|
|
1294
|
+
// Extract script filename from the run command
|
|
1295
|
+
const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1296
|
+
files.push({
|
|
1297
|
+
path: `.claude/skills/${name}/scripts/${scriptName}`,
|
|
1298
|
+
content: [
|
|
1299
|
+
"#!/usr/bin/env bash",
|
|
1300
|
+
`# Hook: ${hook.name} — fires on ${hook.on}`,
|
|
1301
|
+
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1302
|
+
'set -euo pipefail',
|
|
1303
|
+
"",
|
|
1304
|
+
`echo "TODO: implement ${hook.name} hook"`,
|
|
1305
|
+
"",
|
|
1306
|
+
].join("\n"),
|
|
1307
|
+
category: "script",
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
return files;
|
|
1311
|
+
}
|
|
1312
|
+
/** Generate per-agent hook script stubs. */
|
|
1313
|
+
function generateAgentHookScripts(ast) {
|
|
1314
|
+
const files = [];
|
|
1315
|
+
const name = ast.topology.name;
|
|
1316
|
+
for (const node of ast.nodes) {
|
|
1317
|
+
if (node.type !== "agent")
|
|
1318
|
+
continue;
|
|
1319
|
+
const agent = node;
|
|
1320
|
+
if (!agent.hooks || agent.hooks.length === 0)
|
|
1321
|
+
continue;
|
|
1322
|
+
for (const hook of agent.hooks) {
|
|
1323
|
+
if (!hook.run)
|
|
1324
|
+
continue;
|
|
1325
|
+
const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1326
|
+
files.push({
|
|
1327
|
+
path: `.claude/skills/${name}/scripts/${agent.id}-${scriptName}`,
|
|
1328
|
+
content: [
|
|
1329
|
+
"#!/usr/bin/env bash",
|
|
1330
|
+
`# Hook: ${hook.name} — fires on ${hook.on} (scoped to agent: ${agent.id})`,
|
|
1331
|
+
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1332
|
+
'set -euo pipefail',
|
|
1333
|
+
"",
|
|
1334
|
+
`echo "TODO: implement ${hook.name} hook for ${agent.id}"`,
|
|
1335
|
+
"",
|
|
1336
|
+
].join("\n"),
|
|
1337
|
+
category: "script",
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return files;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Generate hook configuration in .claude/settings.json.
|
|
1345
|
+
*
|
|
1346
|
+
* Hooks are grouped by event name as an object:
|
|
1347
|
+
* { "hooks": { "PostToolUse": [ { hooks: [...], matcher? } ], ... } }
|
|
1348
|
+
*/
|
|
1349
|
+
function generateSettings(ast) {
|
|
1350
|
+
const settings = {};
|
|
1351
|
+
const topologyName = ast.topology.name;
|
|
1352
|
+
// Hooks section — grouped by event name
|
|
1353
|
+
const hooksByEvent = {};
|
|
1354
|
+
for (const hook of ast.hooks) {
|
|
1355
|
+
const eventName = hook.on;
|
|
1356
|
+
if (!hooksByEvent[eventName]) {
|
|
1357
|
+
hooksByEvent[eventName] = [];
|
|
1358
|
+
}
|
|
1359
|
+
// Build the script command with proper path prefix
|
|
1360
|
+
const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1361
|
+
const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
|
|
1362
|
+
const hookEntry = {
|
|
1363
|
+
hooks: [
|
|
1364
|
+
{
|
|
1365
|
+
type: hook.type ?? "command",
|
|
1366
|
+
command,
|
|
1367
|
+
...(hook.timeout ? { timeout: hook.timeout } : {}),
|
|
1368
|
+
},
|
|
1369
|
+
],
|
|
1370
|
+
};
|
|
1371
|
+
// Only add matcher when it has a value
|
|
1372
|
+
if (hook.matcher) {
|
|
1373
|
+
hookEntry.matcher = hook.matcher;
|
|
1374
|
+
}
|
|
1375
|
+
hooksByEvent[eventName].push(hookEntry);
|
|
1376
|
+
}
|
|
1377
|
+
// Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
|
|
1378
|
+
// This is the Claude Code native approach: hooks in the agent file are scoped
|
|
1379
|
+
// to that agent automatically.
|
|
1380
|
+
// Gate enforcement — compile gates with run: to PreToolUse hooks
|
|
1381
|
+
const gates = ast.nodes.filter((n) => n.type === "gate");
|
|
1382
|
+
for (const gate of gates) {
|
|
1383
|
+
if (!gate.run)
|
|
1384
|
+
continue;
|
|
1385
|
+
if (gate.behavior === "advisory")
|
|
1386
|
+
continue; // advisory gates stay in markdown only
|
|
1387
|
+
const eventName = "PreToolUse";
|
|
1388
|
+
if (!hooksByEvent[eventName])
|
|
1389
|
+
hooksByEvent[eventName] = [];
|
|
1390
|
+
const scriptName = `gate-${gate.id}.sh`;
|
|
1391
|
+
const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
|
|
1392
|
+
const hookEntry = {
|
|
1393
|
+
hooks: [
|
|
1394
|
+
{
|
|
1395
|
+
type: "command",
|
|
1396
|
+
command,
|
|
1397
|
+
},
|
|
1398
|
+
],
|
|
1399
|
+
matcher: "Task", // Gates fire on Task tool (agent spawning)
|
|
1400
|
+
};
|
|
1401
|
+
hooksByEvent[eventName].push(hookEntry);
|
|
1402
|
+
}
|
|
1403
|
+
// Write hooksByEvent to settings if any hooks or gates were registered
|
|
1404
|
+
if (Object.keys(hooksByEvent).length > 0) {
|
|
1405
|
+
settings.hooks = hooksByEvent;
|
|
1406
|
+
}
|
|
1407
|
+
// Environment variables
|
|
1408
|
+
const envSettings = generateEnvToSettings(ast);
|
|
1409
|
+
if (envSettings) {
|
|
1410
|
+
settings.env = envSettings;
|
|
1411
|
+
}
|
|
1412
|
+
// Permissions section from settings block
|
|
1413
|
+
const perms = ast.settings;
|
|
1414
|
+
if (perms) {
|
|
1415
|
+
const allow = perms.allow;
|
|
1416
|
+
const deny = perms.deny;
|
|
1417
|
+
const ask = perms.ask;
|
|
1418
|
+
if (allow && allow.length > 0)
|
|
1419
|
+
settings.allow = allow;
|
|
1420
|
+
if (deny && deny.length > 0)
|
|
1421
|
+
settings.deny = deny;
|
|
1422
|
+
if (ask && ask.length > 0)
|
|
1423
|
+
settings.ask = ask;
|
|
1424
|
+
}
|
|
1425
|
+
// Defaults documentation
|
|
1426
|
+
if (ast.defaults) {
|
|
1427
|
+
const d = ast.defaults;
|
|
1428
|
+
const defaultsDoc = {};
|
|
1429
|
+
if (d.temperature != null)
|
|
1430
|
+
defaultsDoc.temperature = d.temperature;
|
|
1431
|
+
if (d.maxTokens != null)
|
|
1432
|
+
defaultsDoc.maxTokens = d.maxTokens;
|
|
1433
|
+
if (d.topP != null)
|
|
1434
|
+
defaultsDoc.topP = d.topP;
|
|
1435
|
+
if (d.topK != null)
|
|
1436
|
+
defaultsDoc.topK = d.topK;
|
|
1437
|
+
if (d.stop)
|
|
1438
|
+
defaultsDoc.stop = d.stop;
|
|
1439
|
+
if (d.seed != null)
|
|
1440
|
+
defaultsDoc.seed = d.seed;
|
|
1441
|
+
if (d.thinking)
|
|
1442
|
+
defaultsDoc.thinking = d.thinking;
|
|
1443
|
+
if (d.thinkingBudget != null)
|
|
1444
|
+
defaultsDoc.thinkingBudget = d.thinkingBudget;
|
|
1445
|
+
if (d.outputFormat)
|
|
1446
|
+
defaultsDoc.outputFormat = d.outputFormat;
|
|
1447
|
+
if (d.timeout)
|
|
1448
|
+
defaultsDoc.timeout = d.timeout;
|
|
1449
|
+
if (d.logLevel)
|
|
1450
|
+
defaultsDoc.logLevel = d.logLevel;
|
|
1451
|
+
if (Object.keys(defaultsDoc).length > 0) {
|
|
1452
|
+
settings._defaults = defaultsDoc;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
// Observability documentation
|
|
1456
|
+
if (ast.observability) {
|
|
1457
|
+
const o = ast.observability;
|
|
1458
|
+
const obsDoc = {
|
|
1459
|
+
enabled: o.enabled,
|
|
1460
|
+
level: o.level,
|
|
1461
|
+
exporter: o.exporter,
|
|
1462
|
+
sampleRate: o.sampleRate,
|
|
1463
|
+
};
|
|
1464
|
+
if (o.endpoint)
|
|
1465
|
+
obsDoc.endpoint = o.endpoint;
|
|
1466
|
+
if (o.service)
|
|
1467
|
+
obsDoc.service = o.service;
|
|
1468
|
+
obsDoc.capture = o.capture;
|
|
1469
|
+
obsDoc.spans = o.spans;
|
|
1470
|
+
settings._observability = obsDoc;
|
|
1471
|
+
}
|
|
1472
|
+
// Top-level extensions for claude-code
|
|
1473
|
+
if (ast.extensions?.["claude-code"]) {
|
|
1474
|
+
for (const [k, v] of Object.entries(ast.extensions["claude-code"])) {
|
|
1475
|
+
settings[k] = v;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (Object.keys(settings).length === 0)
|
|
1479
|
+
return null;
|
|
1480
|
+
return {
|
|
1481
|
+
path: ".claude/settings.json",
|
|
1482
|
+
content: JSON.stringify(settings, null, 2) + "\n",
|
|
1483
|
+
category: "shared-config",
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
/** Generate .mcp.json from mcp-servers block. Always includes env field. */
|
|
1487
|
+
function generateMcpJson(ast) {
|
|
1488
|
+
if (Object.keys(ast.mcpServers).length === 0)
|
|
1489
|
+
return null;
|
|
1490
|
+
const mcpConfig = { mcpServers: {} };
|
|
1491
|
+
const servers = mcpConfig.mcpServers;
|
|
1492
|
+
for (const [name, config] of Object.entries(ast.mcpServers)) {
|
|
1493
|
+
const entry = {};
|
|
1494
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1495
|
+
if (key === "command" || key === "url") {
|
|
1496
|
+
entry[key] = value;
|
|
1497
|
+
}
|
|
1498
|
+
else if (key === "args" && Array.isArray(value)) {
|
|
1499
|
+
entry.args = value;
|
|
1500
|
+
}
|
|
1501
|
+
else if (key === "env" && typeof value === "object" && value !== null) {
|
|
1502
|
+
entry.env = value;
|
|
1503
|
+
}
|
|
1504
|
+
// Skip unknown fields — parser may leak env vars to top level
|
|
1505
|
+
}
|
|
1506
|
+
// Only include servers that have a command or url (filter parser artifacts)
|
|
1507
|
+
if (!("command" in entry) && !("url" in entry))
|
|
1508
|
+
continue;
|
|
1509
|
+
// Always include env field for Claude Code .mcp.json
|
|
1510
|
+
if (!("env" in entry)) {
|
|
1511
|
+
entry.env = {};
|
|
1512
|
+
}
|
|
1513
|
+
servers[name] = entry;
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
path: ".mcp.json",
|
|
1517
|
+
content: JSON.stringify(mcpConfig, null, 2) + "\n",
|
|
1518
|
+
category: "shared-config",
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
/** Generate metering script stub. */
|
|
1522
|
+
function generateMetering(ast) {
|
|
1523
|
+
if (!ast.metering)
|
|
1524
|
+
return null;
|
|
1525
|
+
const m = ast.metering;
|
|
1526
|
+
const lines = [
|
|
1527
|
+
"#!/usr/bin/env bash",
|
|
1528
|
+
"# Metering / cost tracking script",
|
|
1529
|
+
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1530
|
+
'set -euo pipefail',
|
|
1531
|
+
"",
|
|
1532
|
+
`# Track: ${m.track.join(", ")}`,
|
|
1533
|
+
`# Per: ${m.per.join(", ")}`,
|
|
1534
|
+
`# Output: ${m.output} (${m.format})`,
|
|
1535
|
+
`# Pricing: ${m.pricing}`,
|
|
1536
|
+
"",
|
|
1537
|
+
'echo "TODO: implement metrics collection"',
|
|
1538
|
+
"",
|
|
1539
|
+
];
|
|
1540
|
+
return {
|
|
1541
|
+
path: "scripts/collect-metrics.sh",
|
|
1542
|
+
content: lines.join("\n"),
|
|
1543
|
+
category: "script",
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
/** Generate tool script stubs. */
|
|
1547
|
+
function generateToolScripts(ast) {
|
|
1548
|
+
const files = [];
|
|
1549
|
+
const name = ast.topology.name;
|
|
1550
|
+
for (const tool of ast.toolDefs) {
|
|
1551
|
+
const ext = tool.lang === "python" ? "py" : tool.lang === "node" ? "js" : "sh";
|
|
1552
|
+
const shebang = tool.lang === "python"
|
|
1553
|
+
? "#!/usr/bin/env python3"
|
|
1554
|
+
: tool.lang === "node"
|
|
1555
|
+
? "#!/usr/bin/env node"
|
|
1556
|
+
: "#!/usr/bin/env bash";
|
|
1557
|
+
const lines = [
|
|
1558
|
+
shebang,
|
|
1559
|
+
`# Tool: ${tool.id} — ${tool.description}`,
|
|
1560
|
+
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1561
|
+
"",
|
|
1562
|
+
];
|
|
1563
|
+
if (tool.lang === "bash" || !tool.lang) {
|
|
1564
|
+
lines.push('set -euo pipefail');
|
|
1565
|
+
lines.push("");
|
|
1566
|
+
}
|
|
1567
|
+
if (tool.args && tool.args.length > 0) {
|
|
1568
|
+
lines.push(`# Args: ${tool.args.join(", ")}`);
|
|
1569
|
+
lines.push("");
|
|
1570
|
+
}
|
|
1571
|
+
if (tool.lang === "python") {
|
|
1572
|
+
lines.push(`print("TODO: implement ${tool.id}")`);
|
|
1573
|
+
}
|
|
1574
|
+
else if (tool.lang === "node") {
|
|
1575
|
+
lines.push(`console.log("TODO: implement ${tool.id}");`);
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
lines.push(`echo "TODO: implement ${tool.id}"`);
|
|
1579
|
+
}
|
|
1580
|
+
lines.push("");
|
|
1581
|
+
files.push({
|
|
1582
|
+
path: `.claude/skills/${name}/scripts/${tool.id}.${ext}`,
|
|
1583
|
+
content: lines.join("\n"),
|
|
1584
|
+
category: "script",
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
return files;
|
|
1588
|
+
}
|
|
1589
|
+
/** Generate schema documentation files. */
|
|
1590
|
+
function generateSchemas(ast) {
|
|
1591
|
+
const files = [];
|
|
1592
|
+
const name = ast.topology.name;
|
|
1593
|
+
if (!ast.schemas || ast.schemas.length === 0)
|
|
1594
|
+
return files;
|
|
1595
|
+
for (const schema of ast.schemas) {
|
|
1596
|
+
const sections = [];
|
|
1597
|
+
sections.push(`# Schema: ${schema.id}`);
|
|
1598
|
+
sections.push("");
|
|
1599
|
+
sections.push("## Fields");
|
|
1600
|
+
sections.push("");
|
|
1601
|
+
for (const field of schema.fields) {
|
|
1602
|
+
const opt = field.optional ? " (optional)" : "";
|
|
1603
|
+
sections.push(`- **${field.name}**${opt}: ${formatSchemaType(field.type, ast.schemas)}`);
|
|
1604
|
+
}
|
|
1605
|
+
sections.push("");
|
|
1606
|
+
files.push({
|
|
1607
|
+
path: `.claude/skills/${name}/schemas/${schema.id}.md`,
|
|
1608
|
+
content: sections.join("\n") + "\n",
|
|
1609
|
+
category: "machine",
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
return files;
|
|
1613
|
+
}
|
|
1614
|
+
/** Generate schedule scripts and crontab fragment. */
|
|
1615
|
+
function generateScheduleScripts(ast) {
|
|
1616
|
+
const files = [];
|
|
1617
|
+
if (!ast.schedules || ast.schedules.length === 0)
|
|
1618
|
+
return files;
|
|
1619
|
+
const cronLines = [
|
|
1620
|
+
"# Crontab fragment — auto-generated by agentopology scaffold",
|
|
1621
|
+
"# Install with: crontab -l | cat - crontab.fragment | crontab -",
|
|
1622
|
+
"",
|
|
1623
|
+
];
|
|
1624
|
+
for (const job of ast.schedules) {
|
|
1625
|
+
const target = job.agent ? `agent ${job.agent}` : job.action ? `action ${job.action}` : "unknown";
|
|
1626
|
+
const scriptPath = `scripts/schedule-${job.id}.sh`;
|
|
1627
|
+
// Generate the shell script
|
|
1628
|
+
const lines = [
|
|
1629
|
+
"#!/usr/bin/env bash",
|
|
1630
|
+
`# Schedule: ${job.id} — invokes ${target}`,
|
|
1631
|
+
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1632
|
+
'set -euo pipefail',
|
|
1633
|
+
"",
|
|
1634
|
+
];
|
|
1635
|
+
if (job.agent) {
|
|
1636
|
+
lines.push(`# Invoke agent: ${job.agent}`);
|
|
1637
|
+
lines.push(`claude --agent ${job.agent}`);
|
|
1638
|
+
}
|
|
1639
|
+
else if (job.action) {
|
|
1640
|
+
lines.push(`# Invoke action: ${job.action}`);
|
|
1641
|
+
lines.push(`bash scripts/action-${job.action}.sh`);
|
|
1642
|
+
}
|
|
1643
|
+
if (job.enabled === false) {
|
|
1644
|
+
lines.unshift("# DISABLED — this schedule is not enabled");
|
|
1645
|
+
}
|
|
1646
|
+
lines.push("");
|
|
1647
|
+
files.push({ path: scriptPath, content: lines.join("\n"), category: "script" });
|
|
1648
|
+
// Add crontab entry
|
|
1649
|
+
let cronExpr = job.cron ?? "# every: " + (job.every ?? "unknown");
|
|
1650
|
+
if (!job.cron && job.every) {
|
|
1651
|
+
// Convert common "every" expressions to cron
|
|
1652
|
+
const everyMap = {
|
|
1653
|
+
"1h": "0 * * * *",
|
|
1654
|
+
"1 hour": "0 * * * *",
|
|
1655
|
+
"hourly": "0 * * * *",
|
|
1656
|
+
"1d": "0 0 * * *",
|
|
1657
|
+
"1 day": "0 0 * * *",
|
|
1658
|
+
"daily": "0 0 * * *",
|
|
1659
|
+
"1w": "0 0 * * 0",
|
|
1660
|
+
"weekly": "0 0 * * 0",
|
|
1661
|
+
};
|
|
1662
|
+
cronExpr = everyMap[job.every.toLowerCase()] ?? `# every: ${job.every}`;
|
|
1663
|
+
}
|
|
1664
|
+
const enabledPrefix = job.enabled === false ? "# DISABLED: " : "";
|
|
1665
|
+
cronLines.push(`${enabledPrefix}${cronExpr} bash ${scriptPath} # ${job.id}`);
|
|
1666
|
+
}
|
|
1667
|
+
cronLines.push("");
|
|
1668
|
+
files.push({ path: "crontab.fragment", content: cronLines.join("\n"), category: "machine" });
|
|
1669
|
+
return files;
|
|
1670
|
+
}
|
|
1671
|
+
/** Generate .claude/commands/X.md for each trigger. */
|
|
1672
|
+
function generateCommandFiles(ast) {
|
|
1673
|
+
const files = [];
|
|
1674
|
+
for (const trigger of ast.triggers) {
|
|
1675
|
+
const sections = [];
|
|
1676
|
+
// Frontmatter
|
|
1677
|
+
const fm = {};
|
|
1678
|
+
if (ast.topology.description) {
|
|
1679
|
+
fm.description = `"${ast.topology.description}"`;
|
|
1680
|
+
}
|
|
1681
|
+
sections.push(frontmatter(fm));
|
|
1682
|
+
sections.push("");
|
|
1683
|
+
// Header
|
|
1684
|
+
sections.push(`# /${trigger.name}`);
|
|
1685
|
+
sections.push("");
|
|
1686
|
+
sections.push(trigger.pattern);
|
|
1687
|
+
sections.push("");
|
|
1688
|
+
// Arguments
|
|
1689
|
+
if (trigger.argument) {
|
|
1690
|
+
sections.push("## Arguments");
|
|
1691
|
+
sections.push(`- ${trigger.argument}: extracted from the command pattern`);
|
|
1692
|
+
sections.push("");
|
|
1693
|
+
}
|
|
1694
|
+
// Pipeline (from edges, enhanced)
|
|
1695
|
+
if (ast.edges.length > 0) {
|
|
1696
|
+
sections.push("## Pipeline");
|
|
1697
|
+
for (const edge of ast.edges) {
|
|
1698
|
+
const arrow = edge.isError ? `-x${edge.errorType ? `[${edge.errorType}]` : ""}>` : "->";
|
|
1699
|
+
let line = `${edge.from} ${arrow} ${edge.to}`;
|
|
1700
|
+
if (edge.condition) {
|
|
1701
|
+
const cleanCond = edge.condition
|
|
1702
|
+
.replace(/\]\s*\[max\s+\d+\s*$/i, "")
|
|
1703
|
+
.replace(/\]\s*$/, "")
|
|
1704
|
+
.trim();
|
|
1705
|
+
line += ` [when ${cleanCond}]`;
|
|
1706
|
+
}
|
|
1707
|
+
if (edge.maxIterations)
|
|
1708
|
+
line += ` [max ${edge.maxIterations}]`;
|
|
1709
|
+
if (edge.weight != null)
|
|
1710
|
+
line += ` [weight ${edge.weight}]`;
|
|
1711
|
+
if (edge.race)
|
|
1712
|
+
line += ` [race]`;
|
|
1713
|
+
if (edge.tolerance != null)
|
|
1714
|
+
line += ` [tolerance: ${edge.tolerance}]`;
|
|
1715
|
+
if (edge.wait)
|
|
1716
|
+
line += ` [wait ${edge.wait}]`;
|
|
1717
|
+
if (edge.reflection)
|
|
1718
|
+
line += ` [reflection]`;
|
|
1719
|
+
sections.push(`- ${line}`);
|
|
1720
|
+
}
|
|
1721
|
+
sections.push("");
|
|
1722
|
+
}
|
|
1723
|
+
// Agents table
|
|
1724
|
+
const agents = ast.nodes.filter((n) => n.type === "agent");
|
|
1725
|
+
if (agents.length > 0) {
|
|
1726
|
+
sections.push("## Agents");
|
|
1727
|
+
sections.push("| Agent | Phase | Model | Role |");
|
|
1728
|
+
sections.push("|-------|-------|-------|------|");
|
|
1729
|
+
for (const agent of agents) {
|
|
1730
|
+
const phase = agent.phase != null ? String(agent.phase) : "-";
|
|
1731
|
+
const model = agent.model ?? "-";
|
|
1732
|
+
const role = agent.role ?? "-";
|
|
1733
|
+
sections.push(`| ${agent.id} | ${phase} | ${model} | ${role} |`);
|
|
1734
|
+
}
|
|
1735
|
+
sections.push("");
|
|
1736
|
+
}
|
|
1737
|
+
files.push({
|
|
1738
|
+
path: `.claude/commands/${trigger.name}.md`,
|
|
1739
|
+
content: sections.join("\n") + "\n",
|
|
1740
|
+
category: "machine",
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
return files;
|
|
1744
|
+
}
|
|
1745
|
+
/** Generate workspace-protocol.md if workspace memory has a protocol field. */
|
|
1746
|
+
function generateWorkspaceProtocol(ast) {
|
|
1747
|
+
const name = ast.topology.name;
|
|
1748
|
+
const workspace = ast.memory.workspace;
|
|
1749
|
+
if (!workspace || !workspace.protocol)
|
|
1750
|
+
return null;
|
|
1751
|
+
const protocol = workspace.protocol;
|
|
1752
|
+
const sections = [];
|
|
1753
|
+
sections.push("# Workspace Protocol");
|
|
1754
|
+
sections.push("");
|
|
1755
|
+
// Directory structure from workspace.structure
|
|
1756
|
+
const structure = workspace.structure;
|
|
1757
|
+
if (structure && Array.isArray(structure)) {
|
|
1758
|
+
sections.push("## Directory Structure");
|
|
1759
|
+
for (const item of structure) {
|
|
1760
|
+
sections.push(`- ${item}`);
|
|
1761
|
+
}
|
|
1762
|
+
sections.push("");
|
|
1763
|
+
}
|
|
1764
|
+
// Read/Write rules from agent nodes
|
|
1765
|
+
const agents = ast.nodes.filter((n) => n.type === "agent");
|
|
1766
|
+
if (agents.length > 0) {
|
|
1767
|
+
sections.push("## Read/Write Rules");
|
|
1768
|
+
for (const agent of agents) {
|
|
1769
|
+
const reads = agent.reads ?? [];
|
|
1770
|
+
const writes = agent.writes ?? [];
|
|
1771
|
+
if (reads.length > 0 || writes.length > 0) {
|
|
1772
|
+
sections.push(`### ${toTitle(agent.id)}`);
|
|
1773
|
+
if (reads.length > 0) {
|
|
1774
|
+
sections.push(`- Reads: ${reads.join(", ")}`);
|
|
1775
|
+
}
|
|
1776
|
+
if (writes.length > 0) {
|
|
1777
|
+
sections.push(`- Writes: ${writes.join(", ")}`);
|
|
1778
|
+
}
|
|
1779
|
+
sections.push("");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return {
|
|
1784
|
+
path: `.claude/skills/${name}/${protocol}`,
|
|
1785
|
+
content: sections.join("\n") + "\n",
|
|
1786
|
+
category: "machine",
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Generate context/instructions file inside `.claude/`.
|
|
1791
|
+
*
|
|
1792
|
+
* If `ast.context.file` is set and points inside `.claude/`, that path is
|
|
1793
|
+
* used. Otherwise the file is written to `.claude/CONTEXT.md`.
|
|
1794
|
+
*
|
|
1795
|
+
* **Important**: We never generate a file at the project-root `CLAUDE.md`
|
|
1796
|
+
* because that is the user's own instructions file and overwriting it would
|
|
1797
|
+
* destroy their content.
|
|
1798
|
+
*/
|
|
1799
|
+
function generateContextFile(ast) {
|
|
1800
|
+
const DEFAULT_PATH = ".claude/CONTEXT.md";
|
|
1801
|
+
let fileName = ast.context.file ?? DEFAULT_PATH;
|
|
1802
|
+
// Safety: never overwrite the user's root-level CLAUDE.md
|
|
1803
|
+
if (fileName === "CLAUDE.md" || fileName === "./CLAUDE.md") {
|
|
1804
|
+
fileName = DEFAULT_PATH;
|
|
1805
|
+
}
|
|
1806
|
+
const sections = [];
|
|
1807
|
+
sections.push(`# ${toTitle(ast.topology.name)}`);
|
|
1808
|
+
sections.push("");
|
|
1809
|
+
if (ast.topology.description) {
|
|
1810
|
+
sections.push(ast.topology.description);
|
|
1811
|
+
sections.push("");
|
|
1812
|
+
}
|
|
1813
|
+
sections.push(`Version: ${ast.topology.version}`);
|
|
1814
|
+
if (ast.topology.patterns.length > 0) {
|
|
1815
|
+
sections.push(`Patterns: ${ast.topology.patterns.join(", ")}`);
|
|
1816
|
+
}
|
|
1817
|
+
sections.push("");
|
|
1818
|
+
// Include references
|
|
1819
|
+
if (ast.context.includes && ast.context.includes.length > 0) {
|
|
1820
|
+
sections.push("## Includes");
|
|
1821
|
+
sections.push("");
|
|
1822
|
+
for (const inc of ast.context.includes) {
|
|
1823
|
+
sections.push(`- ${inc}`);
|
|
1824
|
+
}
|
|
1825
|
+
sections.push("");
|
|
1826
|
+
}
|
|
1827
|
+
// Defaults block
|
|
1828
|
+
if (ast.defaults) {
|
|
1829
|
+
const d = ast.defaults;
|
|
1830
|
+
sections.push("## Defaults");
|
|
1831
|
+
sections.push("");
|
|
1832
|
+
if (d.temperature != null)
|
|
1833
|
+
sections.push(`- Temperature: ${d.temperature}`);
|
|
1834
|
+
if (d.maxTokens != null)
|
|
1835
|
+
sections.push(`- Max tokens: ${d.maxTokens}`);
|
|
1836
|
+
if (d.topP != null)
|
|
1837
|
+
sections.push(`- Top-p: ${d.topP}`);
|
|
1838
|
+
if (d.topK != null)
|
|
1839
|
+
sections.push(`- Top-k: ${d.topK}`);
|
|
1840
|
+
if (d.stop)
|
|
1841
|
+
sections.push(`- Stop sequences: ${d.stop.join(", ")}`);
|
|
1842
|
+
if (d.seed != null)
|
|
1843
|
+
sections.push(`- Seed: ${d.seed}`);
|
|
1844
|
+
if (d.thinking)
|
|
1845
|
+
sections.push(`- Thinking: ${d.thinking}`);
|
|
1846
|
+
if (d.thinkingBudget != null)
|
|
1847
|
+
sections.push(`- Thinking budget: ${d.thinkingBudget}`);
|
|
1848
|
+
if (d.outputFormat)
|
|
1849
|
+
sections.push(`- Output format: ${d.outputFormat}`);
|
|
1850
|
+
if (d.timeout)
|
|
1851
|
+
sections.push(`- Timeout: ${d.timeout}`);
|
|
1852
|
+
if (d.logLevel)
|
|
1853
|
+
sections.push(`- Log level: ${d.logLevel}`);
|
|
1854
|
+
sections.push("");
|
|
1855
|
+
}
|
|
1856
|
+
// Providers
|
|
1857
|
+
if (ast.providers && ast.providers.length > 0) {
|
|
1858
|
+
sections.push("## Providers");
|
|
1859
|
+
sections.push("");
|
|
1860
|
+
for (const provider of ast.providers) {
|
|
1861
|
+
let line = `- **${provider.name}**`;
|
|
1862
|
+
if (provider.default)
|
|
1863
|
+
line += " (default)";
|
|
1864
|
+
if (provider.models.length > 0)
|
|
1865
|
+
line += ` — models: ${provider.models.join(", ")}`;
|
|
1866
|
+
sections.push(line);
|
|
1867
|
+
if (provider.baseUrl)
|
|
1868
|
+
sections.push(` - Base URL: ${provider.baseUrl}`);
|
|
1869
|
+
if (provider.apiKey)
|
|
1870
|
+
sections.push(` - API key: ${provider.apiKey}`);
|
|
1871
|
+
if (provider.auth) {
|
|
1872
|
+
sections.push(` - Auth: ${provider.auth.type}`);
|
|
1873
|
+
if (provider.auth.issuer)
|
|
1874
|
+
sections.push(` - Issuer: ${provider.auth.issuer}`);
|
|
1875
|
+
if (provider.auth.audience)
|
|
1876
|
+
sections.push(` - Audience: ${provider.auth.audience}`);
|
|
1877
|
+
if (provider.auth.scopes)
|
|
1878
|
+
sections.push(` - Scopes: ${provider.auth.scopes.join(", ")}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
sections.push("");
|
|
1882
|
+
}
|
|
1883
|
+
// Checkpoint config
|
|
1884
|
+
if (ast.checkpoint) {
|
|
1885
|
+
const cp = ast.checkpoint;
|
|
1886
|
+
sections.push("## Checkpoint");
|
|
1887
|
+
sections.push("");
|
|
1888
|
+
sections.push(`- Backend: ${cp.backend}`);
|
|
1889
|
+
sections.push(`- Strategy: ${cp.strategy}`);
|
|
1890
|
+
if (cp.connection)
|
|
1891
|
+
sections.push(`- Connection: ${cp.connection}`);
|
|
1892
|
+
if (cp.ttl)
|
|
1893
|
+
sections.push(`- TTL: ${cp.ttl}`);
|
|
1894
|
+
if (cp.replay) {
|
|
1895
|
+
sections.push(`- Replay: ${cp.replay.enabled ? "enabled" : "disabled"}`);
|
|
1896
|
+
if (cp.replay.maxHistory != null)
|
|
1897
|
+
sections.push(` - Max history: ${cp.replay.maxHistory}`);
|
|
1898
|
+
if (cp.replay.branch != null)
|
|
1899
|
+
sections.push(` - Branching: ${cp.replay.branch}`);
|
|
1900
|
+
}
|
|
1901
|
+
sections.push("");
|
|
1902
|
+
}
|
|
1903
|
+
// Artifacts registry
|
|
1904
|
+
if (ast.artifacts && ast.artifacts.length > 0) {
|
|
1905
|
+
sections.push("## Artifacts");
|
|
1906
|
+
sections.push("");
|
|
1907
|
+
for (const a of ast.artifacts) {
|
|
1908
|
+
let line = `- **${a.id}** (${a.type})`;
|
|
1909
|
+
if (a.path)
|
|
1910
|
+
line += ` — path: ${a.path}`;
|
|
1911
|
+
if (a.retention)
|
|
1912
|
+
line += `, retention: ${a.retention}`;
|
|
1913
|
+
if (a.dependsOn && a.dependsOn.length > 0)
|
|
1914
|
+
line += `, depends on: ${a.dependsOn.join(", ")}`;
|
|
1915
|
+
sections.push(line);
|
|
1916
|
+
}
|
|
1917
|
+
sections.push("");
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
path: fileName,
|
|
1921
|
+
content: sections.join("\n") + "\n",
|
|
1922
|
+
category: "machine",
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Merge `ast.env` into the settings.json output under an `env` key.
|
|
1927
|
+
*
|
|
1928
|
+
* When `ast.providers` includes an "anthropic" provider with an api-key,
|
|
1929
|
+
* that env var is included automatically (enabling API-key mode instead
|
|
1930
|
+
* of subscription mode).
|
|
1931
|
+
*
|
|
1932
|
+
* Returns the env record if non-empty, otherwise null.
|
|
1933
|
+
*/
|
|
1934
|
+
function generateEnvToSettings(ast) {
|
|
1935
|
+
const env = {};
|
|
1936
|
+
for (const [key, val] of Object.entries(ast.env)) {
|
|
1937
|
+
if (typeof val === "string") {
|
|
1938
|
+
env[key] = val;
|
|
1939
|
+
}
|
|
1940
|
+
else {
|
|
1941
|
+
const sv = val;
|
|
1942
|
+
if (sv.secretRef) {
|
|
1943
|
+
// Secret reference — emit placeholder with comment-style URI
|
|
1944
|
+
env[key] = `\${${key}}`; // placeholder
|
|
1945
|
+
env[`# ${key}_SECRET`] = sv.secretRef.uri;
|
|
1946
|
+
}
|
|
1947
|
+
else if (sv.sensitive) {
|
|
1948
|
+
// Sensitive value — never emit the actual value
|
|
1949
|
+
env[key] = `\${${key}}`;
|
|
1950
|
+
}
|
|
1951
|
+
else if (sv.encrypted) {
|
|
1952
|
+
// Encrypted value — emit placeholder with warning
|
|
1953
|
+
env[key] = `\${${key}}`;
|
|
1954
|
+
env[`# ${key}_ENCRYPTED`] = "WARNING: value is SOPS-encrypted, decrypt before use";
|
|
1955
|
+
}
|
|
1956
|
+
else {
|
|
1957
|
+
env[key] = sv.value;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// If an anthropic provider exists, scaffold the API key env var
|
|
1962
|
+
if (ast.providers && ast.providers.length > 0) {
|
|
1963
|
+
const anthropicProvider = ast.providers.find((p) => p.name === "anthropic");
|
|
1964
|
+
if (anthropicProvider?.apiKey) {
|
|
1965
|
+
// Extract env var name from ${ENV_VAR} format
|
|
1966
|
+
const envVarMatch = anthropicProvider.apiKey.match(/^\$\{(.+)\}$/);
|
|
1967
|
+
if (envVarMatch) {
|
|
1968
|
+
env[envVarMatch[1]] = anthropicProvider.apiKey;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (Object.keys(env).length === 0)
|
|
1973
|
+
return null;
|
|
1974
|
+
return env;
|
|
1975
|
+
}
|
|
1976
|
+
/** Generate trigger documentation in the skill. */
|
|
1977
|
+
function generateTriggers(ast) {
|
|
1978
|
+
if (ast.triggers.length === 0)
|
|
1979
|
+
return "";
|
|
1980
|
+
const lines = ["## Triggers", ""];
|
|
1981
|
+
for (const trigger of ast.triggers) {
|
|
1982
|
+
lines.push(`### /${trigger.name}`);
|
|
1983
|
+
lines.push(`Pattern: \`${trigger.pattern}\``);
|
|
1984
|
+
if (trigger.argument)
|
|
1985
|
+
lines.push(`Argument: ${trigger.argument}`);
|
|
1986
|
+
lines.push("");
|
|
1987
|
+
}
|
|
1988
|
+
return lines.join("\n");
|
|
1989
|
+
}
|
|
1990
|
+
// ---------------------------------------------------------------------------
|
|
1991
|
+
// Binding export
|
|
1992
|
+
// ---------------------------------------------------------------------------
|
|
1993
|
+
/** Claude Code binding — the reference binding implementation. */
|
|
1994
|
+
export const claudeCodeBinding = {
|
|
1995
|
+
name: "claude-code",
|
|
1996
|
+
description: "Anthropic Claude Code CLI — generates .claude/ directory structure, .mcp.json, and scripts.",
|
|
1997
|
+
scaffold(ast) {
|
|
1998
|
+
// Fragment check — skip main skill generation for fragments
|
|
1999
|
+
if (ast.isFragment) {
|
|
2000
|
+
const files = [];
|
|
2001
|
+
// For fragments, only generate the topology skill doc (which notes it's a fragment)
|
|
2002
|
+
files.push(...generateTopologySkill(ast));
|
|
2003
|
+
files.push(...generateAgents(ast));
|
|
2004
|
+
files.push(...generateSchemas(ast));
|
|
2005
|
+
return deduplicateFiles(files);
|
|
2006
|
+
}
|
|
2007
|
+
const files = [];
|
|
2008
|
+
// Action nodes
|
|
2009
|
+
files.push(...generateActionNodes(ast));
|
|
2010
|
+
// Group nodes — MUST run before agents so group protocols are registered
|
|
2011
|
+
files.push(...generateGroupNodes(ast));
|
|
2012
|
+
// Agent files — may include group conversation protocol sections
|
|
2013
|
+
files.push(...generateAgents(ast));
|
|
2014
|
+
// Human nodes
|
|
2015
|
+
files.push(...generateHumanNodes(ast));
|
|
2016
|
+
// Main topology skill (with frontmatter)
|
|
2017
|
+
const skillFiles = generateTopologySkill(ast);
|
|
2018
|
+
// Append trigger info to the main skill
|
|
2019
|
+
if (ast.triggers.length > 0 && skillFiles.length > 0) {
|
|
2020
|
+
skillFiles[0].content += generateTriggers(ast);
|
|
2021
|
+
}
|
|
2022
|
+
files.push(...skillFiles);
|
|
2023
|
+
// Named skills
|
|
2024
|
+
files.push(...generateSkills(ast));
|
|
2025
|
+
// Schemas
|
|
2026
|
+
files.push(...generateSchemas(ast));
|
|
2027
|
+
// Memory directories
|
|
2028
|
+
files.push(...generateMemory(ast));
|
|
2029
|
+
// Gate scripts
|
|
2030
|
+
files.push(...generateGateScripts(ast));
|
|
2031
|
+
files.push(...generateGateWrapperScripts(ast));
|
|
2032
|
+
// Hook scripts (global)
|
|
2033
|
+
files.push(...generateHookScripts(ast));
|
|
2034
|
+
// Hook scripts (per-agent)
|
|
2035
|
+
files.push(...generateAgentHookScripts(ast));
|
|
2036
|
+
// Tool scripts
|
|
2037
|
+
files.push(...generateToolScripts(ast));
|
|
2038
|
+
// Schedule scripts
|
|
2039
|
+
files.push(...generateScheduleScripts(ast));
|
|
2040
|
+
// Command files
|
|
2041
|
+
files.push(...generateCommandFiles(ast));
|
|
2042
|
+
// Workspace protocol
|
|
2043
|
+
const wsProtocol = generateWorkspaceProtocol(ast);
|
|
2044
|
+
if (wsProtocol)
|
|
2045
|
+
files.push(wsProtocol);
|
|
2046
|
+
// Settings (hooks + permissions + extensions)
|
|
2047
|
+
const settingsFile = generateSettings(ast);
|
|
2048
|
+
if (settingsFile)
|
|
2049
|
+
files.push(settingsFile);
|
|
2050
|
+
// MCP servers (with env field)
|
|
2051
|
+
const mcpFile = generateMcpJson(ast);
|
|
2052
|
+
if (mcpFile)
|
|
2053
|
+
files.push(mcpFile);
|
|
2054
|
+
// Metering
|
|
2055
|
+
const meteringFile = generateMetering(ast);
|
|
2056
|
+
if (meteringFile)
|
|
2057
|
+
files.push(meteringFile);
|
|
2058
|
+
// Context file (.claude/CONTEXT.md or custom — never root CLAUDE.md)
|
|
2059
|
+
files.push(generateContextFile(ast));
|
|
2060
|
+
return deduplicateFiles(files);
|
|
2061
|
+
},
|
|
2062
|
+
};
|
|
2063
|
+
//# sourceMappingURL=claude-code.js.map
|