agentopology 0.6.0 → 0.8.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/README.md +5 -5
- package/dist/__tests__/e2e/compile.test.d.ts +12 -0
- package/dist/__tests__/e2e/compile.test.d.ts.map +1 -0
- package/dist/__tests__/e2e/compile.test.js +287 -0
- package/dist/__tests__/e2e/compile.test.js.map +1 -0
- package/dist/__tests__/e2e/roundtrip.test.js +2 -4
- package/dist/__tests__/e2e/roundtrip.test.js.map +1 -1
- package/dist/__tests__/e2e/scaffold-owns.test.js +12 -3
- package/dist/__tests__/e2e/scaffold-owns.test.js.map +1 -1
- package/dist/__tests__/e2e/sync-cycle.test.d.ts +10 -0
- package/dist/__tests__/e2e/sync-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/e2e/sync-cycle.test.js +49 -0
- package/dist/__tests__/e2e/sync-cycle.test.js.map +1 -0
- package/dist/__tests__/live/claude-code.live.test.js +73 -4
- package/dist/__tests__/live/claude-code.live.test.js.map +1 -1
- package/dist/__tests__/open-findings.test.js +4 -25
- package/dist/__tests__/open-findings.test.js.map +1 -1
- package/dist/bindings/__tests__/bindings.test.js +19 -19
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/__tests__/claude-code-hybrid.test.js +5 -5
- package/dist/bindings/__tests__/claude-code-hybrid.test.js.map +1 -1
- package/dist/bindings/__tests__/hybrid-e2e.test.js +1 -1
- package/dist/bindings/__tests__/hybrid-e2e.test.js.map +1 -1
- package/dist/bindings/claude-code.d.ts +6 -8
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +492 -196
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/lib/prompt-fence.d.ts +22 -0
- package/dist/bindings/lib/prompt-fence.d.ts.map +1 -0
- package/dist/bindings/lib/prompt-fence.js +29 -0
- package/dist/bindings/lib/prompt-fence.js.map +1 -0
- package/dist/cli/index.js +9 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/teach.d.ts.map +1 -1
- package/dist/docs/teach.js +10 -0
- package/dist/docs/teach.js.map +1 -1
- package/dist/import/claude-code.d.ts.map +1 -1
- package/dist/import/claude-code.js +21 -5
- package/dist/import/claude-code.js.map +1 -1
- package/dist/import/serializer.d.ts.map +1 -1
- package/dist/import/serializer.js +44 -3
- package/dist/import/serializer.js.map +1 -1
- package/dist/parser/__tests__/mod-hooks.test.js +1 -1
- package/dist/parser/__tests__/mod-hooks.test.js.map +1 -1
- package/dist/parser/ast.d.ts +46 -3
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +60 -3
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/run.d.ts +23 -2
- package/dist/parser/run.d.ts.map +1 -1
- package/dist/parser/run.js +39 -3
- package/dist/parser/run.js.map +1 -1
- package/dist/parser/validator.d.ts +1 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +131 -3
- package/dist/parser/validator.js.map +1 -1
- package/dist/scaffold/diff.d.ts +17 -0
- package/dist/scaffold/diff.d.ts.map +1 -0
- package/dist/scaffold/diff.js +66 -0
- package/dist/scaffold/diff.js.map +1 -0
- package/dist/scaffold/merge.d.ts.map +1 -1
- package/dist/scaffold/merge.js +14 -0
- package/dist/scaffold/merge.js.map +1 -1
- package/dist/sync/index.d.ts +1 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +40 -2
- package/dist/sync/index.js.map +1 -1
- package/package.json +1 -1
- package/spec/grammar.md +28 -2
- package/spec/testing.md +35 -25
- package/spec/validation.md +5 -1
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { deduplicateFiles } from "./types.js";
|
|
10
10
|
import { shellStub, STUB_MARKER } from "./lib/stub.js";
|
|
11
|
+
import { PROMPT_FENCE_CLOSE, PROMPT_FENCE_OPEN } from "./lib/prompt-fence.js";
|
|
11
12
|
import { isWorkflowSeamAgent, seamFiles } from "./lib/seam.js";
|
|
12
13
|
import { phaseOf as sharedPhaseOf } from "../resolve/phase.js";
|
|
13
14
|
import { gateAnchors } from "../parser/ast.js";
|
|
14
|
-
import { proseInRun, scriptNameFromRun } from "../parser/run.js";
|
|
15
|
+
import { isProjectPath, proseInRun, runParts, scriptNameFromRun } from "../parser/run.js";
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Helpers
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -22,13 +23,56 @@ import { proseInRun, scriptNameFromRun } from "../parser/run.js";
|
|
|
22
23
|
function convertMcpToolName(tool) {
|
|
23
24
|
if (!tool.startsWith("mcp."))
|
|
24
25
|
return tool;
|
|
25
|
-
// mcp.server
|
|
26
|
-
// Only
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// mcp.server → mcp__server (every tool on it); mcp.server.* → mcp__server__*;
|
|
27
|
+
// mcp.server.method_name → mcp__server__method_name. Only the first dot after
|
|
28
|
+
// the server separates; a dotted method name keeps its own dots.
|
|
29
|
+
const rest = tool.slice(4);
|
|
30
|
+
const dot = rest.indexOf(".");
|
|
31
|
+
if (dot === -1)
|
|
32
|
+
return `mcp__${rest}`;
|
|
33
|
+
return `mcp__${rest.slice(0, dot)}__${rest.slice(dot + 1)}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether Claude Code accepts `model` as written: an alias (optionally with a
|
|
37
|
+
* context suffix such as [1m]), inherit, or a full claude-* model id. Any other
|
|
38
|
+
* value (gpt-4o, gemini-2.5-pro) was written to the agent file unchecked.
|
|
39
|
+
*/
|
|
40
|
+
function isClaudeCodeModel(model) {
|
|
41
|
+
return /^(?:sonnet|opus|haiku|fable)(?:\[[0-9a-z]+\])?$|^inherit$|^claude-[a-z0-9.-]+(?:\[[0-9a-z]+\])?$/.test(model);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Claude Code hook events for the language's universal names. A hook on a
|
|
45
|
+
* name Claude Code does not know never fires, so AgentStop used to be dead.
|
|
46
|
+
*/
|
|
47
|
+
const UNIVERSAL_EVENTS = {
|
|
48
|
+
AgentStart: "SubagentStart",
|
|
49
|
+
AgentStop: "SubagentStop",
|
|
50
|
+
ToolUse: "PostToolUse",
|
|
51
|
+
Error: "PostToolUseFailure",
|
|
52
|
+
};
|
|
53
|
+
function claudeEvent(on) {
|
|
54
|
+
return UNIVERSAL_EVENTS[on] ?? on;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a hook runs a shell command (and so has a script). */
|
|
57
|
+
function isCommandHook(hook) {
|
|
58
|
+
return (hook.type ?? "command") === "command";
|
|
59
|
+
}
|
|
60
|
+
/** A settings.json hook handler, in the shape Claude Code reads for its type. */
|
|
61
|
+
function hookHandler(hook, command) {
|
|
62
|
+
const timeout = hook.timeout ? { timeout: hook.timeout } : {};
|
|
63
|
+
switch (hook.type ?? "command") {
|
|
64
|
+
// A prompt or agent hook asks a model (or a subagent) to judge the event;
|
|
65
|
+
// its run: is the prompt.
|
|
66
|
+
case "prompt":
|
|
67
|
+
case "agent":
|
|
68
|
+
return { type: hook.type, prompt: hook.run, ...(hook.model ? { model: hook.model } : {}), ...timeout };
|
|
69
|
+
case "http":
|
|
70
|
+
return { type: "http", url: hook.url, ...timeout };
|
|
71
|
+
case "mcp_tool":
|
|
72
|
+
return { type: "mcp_tool", server: hook.server, tool: hook.tool, ...timeout };
|
|
73
|
+
default:
|
|
74
|
+
return { type: "command", command: command(), ...(hook.async ? { async: true } : {}), ...timeout };
|
|
30
75
|
}
|
|
31
|
-
return tool;
|
|
32
76
|
}
|
|
33
77
|
/**
|
|
34
78
|
* Downshift all markdown headings by one level so they nest properly
|
|
@@ -55,6 +99,8 @@ function toTitle(id) {
|
|
|
55
99
|
*/
|
|
56
100
|
/** Characters that require quoting in YAML scalar values. */
|
|
57
101
|
const YAML_SPECIAL = /[:#\[\]{}&*!|>'"%@`]/;
|
|
102
|
+
/** Plain scalars YAML reads as something other than a string (yes → true, 1.0 → a number). */
|
|
103
|
+
const YAML_NOT_STRING = /^(?:y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF|null|Null|NULL|~|[-+]?(?:\d[\d_]*)?\.?\d+(?:[eE][-+]?\d+)?|0x[0-9a-fA-F]+|0o[0-7]+|\.inf|\.nan)$/;
|
|
58
104
|
function frontmatter(fields) {
|
|
59
105
|
const lines = ["---"];
|
|
60
106
|
for (const [key, value] of Object.entries(fields)) {
|
|
@@ -68,13 +114,13 @@ function frontmatter(fields) {
|
|
|
68
114
|
lines.push(` - ${item}`);
|
|
69
115
|
}
|
|
70
116
|
}
|
|
71
|
-
else if (typeof value === "boolean") {
|
|
117
|
+
else if (typeof value === "boolean" || typeof value === "number") {
|
|
72
118
|
lines.push(`${key}: ${value}`);
|
|
73
119
|
}
|
|
74
120
|
else {
|
|
75
121
|
// Auto-quote strings containing YAML-special characters
|
|
76
122
|
const str = String(value);
|
|
77
|
-
const needsQuotes = YAML_SPECIAL.test(str) || str.trim() !== str;
|
|
123
|
+
const needsQuotes = YAML_SPECIAL.test(str) || str.trim() !== str || str === "" || YAML_NOT_STRING.test(str);
|
|
78
124
|
lines.push(`${key}: ${needsQuotes ? `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : str}`);
|
|
79
125
|
}
|
|
80
126
|
}
|
|
@@ -89,10 +135,16 @@ function frontmatter(fields) {
|
|
|
89
135
|
* human, and orchestrator nodes are NOT registered and therefore CANNOT be
|
|
90
136
|
* used as SubagentStop matchers.
|
|
91
137
|
*/
|
|
138
|
+
/** An agent with runs: inline or fork compiles to a skill, not a subagent. */
|
|
139
|
+
function runsAsSkill(agent) {
|
|
140
|
+
return agent.runs === "inline" || agent.runs === "fork";
|
|
141
|
+
}
|
|
142
|
+
/** Permission modes a subagent may ask for but cannot get beyond its session. */
|
|
143
|
+
const LOOSER_MODES = new Set(["acceptEdits", "auto", "dontAsk", "bypassPermissions"]);
|
|
92
144
|
function isRegisteredSubagentType(ast, name) {
|
|
93
145
|
if (!name)
|
|
94
146
|
return false;
|
|
95
|
-
return ast.nodes.some((n) => (n.type === "
|
|
147
|
+
return ast.nodes.some((n) => (n.type === "group" || (n.type === "agent" && !runsAsSkill(n))) && n.id === name);
|
|
96
148
|
}
|
|
97
149
|
/**
|
|
98
150
|
* Resolve the topology's orchestration delegation mode.
|
|
@@ -106,7 +158,96 @@ function getDelegationMode(ast) {
|
|
|
106
158
|
const orch = ast.nodes.find((n) => n.type === "orchestrator");
|
|
107
159
|
return orch?.delegation === "inline" ? "inline" : "subagent";
|
|
108
160
|
}
|
|
109
|
-
|
|
161
|
+
function gateWiring(ast, gate) {
|
|
162
|
+
if (getDelegationMode(ast) === "inline")
|
|
163
|
+
return { kind: "none" };
|
|
164
|
+
const afters = gateAnchors(gate, "after");
|
|
165
|
+
if (afters.length > 0 && afters.every((a) => isRegisteredSubagentType(ast, a))) {
|
|
166
|
+
return { kind: "subagent-stop", anchors: afters };
|
|
167
|
+
}
|
|
168
|
+
const agents = gateAnchors(gate, "before").filter((b) => isRegisteredSubagentType(ast, b));
|
|
169
|
+
return agents.length > 0 ? { kind: "before-agent", agents } : { kind: "none" };
|
|
170
|
+
}
|
|
171
|
+
/** Agents a human approval step stands before: its direct flow successors. */
|
|
172
|
+
function approvalTargets(ast, human) {
|
|
173
|
+
if (getDelegationMode(ast) === "inline")
|
|
174
|
+
return [];
|
|
175
|
+
const out = ast.edges.filter((e) => e.from === human.id && !e.isError).map((e) => e.to);
|
|
176
|
+
return [...new Set(out)].filter((id) => isRegisteredSubagentType(ast, id));
|
|
177
|
+
}
|
|
178
|
+
/** The settings.json command for the topology's before-agent hook. */
|
|
179
|
+
function beforeAgentCommand(ast) {
|
|
180
|
+
return `bash "$CLAUDE_PROJECT_DIR/.claude/skills/${ast.topology.name}/scripts/before-agent.sh"`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* One PreToolUse hook on the Agent tool for the whole topology: before Claude
|
|
184
|
+
* Code launches a subagent it runs that agent's gates (from actions or
|
|
185
|
+
* humans), then asks the user to approve when a human step stands before it.
|
|
186
|
+
* Null when nothing needs it.
|
|
187
|
+
*/
|
|
188
|
+
function generateBeforeAgentScript(ast) {
|
|
189
|
+
const name = ast.topology.name;
|
|
190
|
+
const steps = new Map();
|
|
191
|
+
const add = (agent, lines) => steps.set(agent, [...(steps.get(agent) ?? []), ...lines]);
|
|
192
|
+
for (const node of ast.nodes) {
|
|
193
|
+
if (node.type !== "gate")
|
|
194
|
+
continue;
|
|
195
|
+
const gate = node;
|
|
196
|
+
if (!gate.run || gate.behavior === "advisory" || !scriptNameFromRun(gate.run))
|
|
197
|
+
continue;
|
|
198
|
+
const wiring = gateWiring(ast, gate);
|
|
199
|
+
if (wiring.kind !== "before-agent")
|
|
200
|
+
continue;
|
|
201
|
+
for (const agent of wiring.agents) {
|
|
202
|
+
add(agent, [
|
|
203
|
+
` # gate ${gate.id} (after: ${gateAnchors(gate, "after").join(", ") || "-"})`,
|
|
204
|
+
` bash "$DIR/gate-${gate.id}.sh" >&2 || exit 2`,
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const node of ast.nodes) {
|
|
209
|
+
if (node.type !== "human")
|
|
210
|
+
continue;
|
|
211
|
+
const human = node;
|
|
212
|
+
const reason = `Approval step "${human.id}"${human.description ? `: ${human.description}` : ""}`;
|
|
213
|
+
for (const agent of approvalTargets(ast, human)) {
|
|
214
|
+
add(agent, [` # approval step ${human.id}`, ` ask ${shellQuote(reason)}`]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (steps.size === 0)
|
|
218
|
+
return null;
|
|
219
|
+
const lines = [
|
|
220
|
+
"#!/usr/bin/env bash",
|
|
221
|
+
`# Before Claude Code launches a subagent of topology "${name}".`,
|
|
222
|
+
"# Auto-generated by agentopology scaffold. Wired in .claude/settings.json as a",
|
|
223
|
+
"# PreToolUse hook on the Agent tool. Exit 2 blocks the launch and shows the",
|
|
224
|
+
"# reason to Claude; an \"ask\" decision makes Claude Code ask the user first.",
|
|
225
|
+
"set -euo pipefail",
|
|
226
|
+
'cd "$CLAUDE_PROJECT_DIR"',
|
|
227
|
+
`DIR=".claude/skills/${name}/scripts"`,
|
|
228
|
+
'AGENT=$(jq -r \'.tool_input.subagent_type // ""\')',
|
|
229
|
+
"",
|
|
230
|
+
"ask() {",
|
|
231
|
+
" jq -n --arg r \"$1\" '{hookSpecificOutput: {hookEventName: \"PreToolUse\", permissionDecision: \"ask\", permissionDecisionReason: $r}}'",
|
|
232
|
+
"}",
|
|
233
|
+
"",
|
|
234
|
+
'case "$AGENT" in',
|
|
235
|
+
];
|
|
236
|
+
for (const [agent, body] of steps) {
|
|
237
|
+
lines.push(` ${agent})`, ...body, " ;;");
|
|
238
|
+
}
|
|
239
|
+
lines.push("esac", "exit 0", "");
|
|
240
|
+
return {
|
|
241
|
+
path: `.claude/skills/${name}/scripts/before-agent.sh`,
|
|
242
|
+
content: lines.join("\n"),
|
|
243
|
+
category: "script",
|
|
244
|
+
executable: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Single-quote a string for bash. */
|
|
248
|
+
function shellQuote(text) {
|
|
249
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
250
|
+
}
|
|
110
251
|
function gitkeep(dirPath) {
|
|
111
252
|
return { path: `${dirPath}/.gitkeep`, content: "", category: "machine" };
|
|
112
253
|
}
|
|
@@ -252,7 +393,7 @@ function workflowRunLabel(ast, run) {
|
|
|
252
393
|
*/
|
|
253
394
|
function mapPermissionMode(perm) {
|
|
254
395
|
// Claude Code native values — pass through
|
|
255
|
-
const nativeValues = ["plan", "default", "acceptEdits", "dontAsk", "bypassPermissions"];
|
|
396
|
+
const nativeValues = ["plan", "default", "acceptEdits", "auto", "dontAsk", "bypassPermissions"];
|
|
256
397
|
if (nativeValues.includes(perm)) {
|
|
257
398
|
// "default" is the default, omit it
|
|
258
399
|
if (perm === "default")
|
|
@@ -265,14 +406,16 @@ function mapPermissionMode(perm) {
|
|
|
265
406
|
return "plan";
|
|
266
407
|
case "autonomous":
|
|
267
408
|
return undefined; // default, omit
|
|
268
|
-
case "
|
|
269
|
-
return undefined; // default
|
|
409
|
+
case "confirm":
|
|
410
|
+
return undefined; // asks before each action: Claude Code's default
|
|
270
411
|
case "interactive":
|
|
271
412
|
return "acceptEdits";
|
|
272
413
|
case "unrestricted":
|
|
414
|
+
case "bypass":
|
|
273
415
|
return "bypassPermissions";
|
|
274
416
|
default:
|
|
275
|
-
|
|
417
|
+
// Claude Code rejects an unknown permissionMode; V27 already warned.
|
|
418
|
+
return undefined;
|
|
276
419
|
}
|
|
277
420
|
}
|
|
278
421
|
/** Format a SchemaType to a human-readable string. Optionally resolves refs. */
|
|
@@ -326,45 +469,24 @@ function generateActionNodes(ast) {
|
|
|
326
469
|
if (action.join) {
|
|
327
470
|
lines.push(`# Join semantics: ${action.join}`);
|
|
328
471
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
case "git":
|
|
348
|
-
lines.push("");
|
|
349
|
-
lines.push("# Git operation");
|
|
350
|
-
if (action.commands && action.commands.length > 0) {
|
|
351
|
-
for (const cmd of action.commands) {
|
|
352
|
-
lines.push(cmd);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
lines.push('echo "TODO: implement git action"');
|
|
357
|
-
}
|
|
358
|
-
break;
|
|
359
|
-
case "report":
|
|
360
|
-
lines.push("");
|
|
361
|
-
lines.push("# Report generation");
|
|
362
|
-
lines.push('echo "TODO: implement report action"');
|
|
363
|
-
break;
|
|
364
|
-
default:
|
|
365
|
-
lines.push("");
|
|
366
|
-
lines.push(`echo "TODO: implement ${action.kind ?? "unknown"} action"`);
|
|
367
|
-
break;
|
|
472
|
+
if (action.kind === "external")
|
|
473
|
+
lines.push(`# External source: ${action.source ?? "unknown"}`);
|
|
474
|
+
lines.push("");
|
|
475
|
+
// The action's own commands, whatever its kind: an external or report
|
|
476
|
+
// action used to drop them for a TODO line.
|
|
477
|
+
if (action.commands && action.commands.length > 0) {
|
|
478
|
+
lines.push(...action.commands);
|
|
479
|
+
}
|
|
480
|
+
else if (!action.kind || action.kind === "inline") {
|
|
481
|
+
// Done by the orchestrator in conversation (the playbook says how).
|
|
482
|
+
lines.push(`# Performed by the orchestrator in conversation; nothing to run.`);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// A placeholder that reports itself as one (agentopology stubs), never a
|
|
486
|
+
// silent success.
|
|
487
|
+
lines.splice(3, 0, STUB_MARKER);
|
|
488
|
+
lines.push(`echo "TODO: implement ${action.kind ?? "this"} action ${action.id}" >&2`);
|
|
489
|
+
lines.push("exit 1");
|
|
368
490
|
}
|
|
369
491
|
lines.push("");
|
|
370
492
|
files.push({
|
|
@@ -389,21 +511,31 @@ function generateAgents(ast) {
|
|
|
389
511
|
// .claude/agents/<name>/AGENT.md for it. See generateWorkflowLaunchArtifacts.
|
|
390
512
|
if (isWorkflowSeamAgent(agent))
|
|
391
513
|
continue;
|
|
514
|
+
// runs: inline or fork compiles to a skill (generateAgentSkills).
|
|
515
|
+
if (runsAsSkill(agent))
|
|
516
|
+
continue;
|
|
392
517
|
// Build frontmatter fields
|
|
393
518
|
const fm = {};
|
|
394
519
|
fm.name = agent.id;
|
|
395
520
|
// Description: prefer agent.description, then role description from roles block.
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
521
|
+
// Claude Code skips an agent without one, so a manual agent keeps it and
|
|
522
|
+
// says it runs only when asked (there is no frontmatter field for that).
|
|
523
|
+
const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role ?? toTitle(agent.id);
|
|
524
|
+
fm.description = agent.invocation === "manual"
|
|
525
|
+
? `${desc.replace(/[.\s]*$/, "")}. Use only when the user asks for this agent by name; never delegate to it on your own.`
|
|
526
|
+
: desc;
|
|
527
|
+
if (agent.model) {
|
|
528
|
+
if (isClaudeCodeModel(agent.model)) {
|
|
529
|
+
fm.model = agent.model;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.warn(`[claude-code] Agent "${agent.id}" model "${agent.model}" is not a Claude Code model ` +
|
|
533
|
+
`(sonnet, opus, haiku, fable, inherit, or a claude-* id); omitted, so the agent ` +
|
|
534
|
+
`runs on the session's model.`);
|
|
401
535
|
}
|
|
402
536
|
}
|
|
403
|
-
if (agent.model)
|
|
404
|
-
fm.model = agent.model;
|
|
405
537
|
if (agent.maxTurns != null)
|
|
406
|
-
fm.maxTurns =
|
|
538
|
+
fm.maxTurns = agent.maxTurns;
|
|
407
539
|
if (agent.tools && agent.tools.length > 0)
|
|
408
540
|
fm.tools = agent.tools.map(convertMcpToolName).join(", ");
|
|
409
541
|
if (agent.disallowedTools && agent.disallowedTools.length > 0)
|
|
@@ -416,7 +548,23 @@ function generateAgents(ast) {
|
|
|
416
548
|
const mapped = mapPermissionMode(agent.permissions);
|
|
417
549
|
if (mapped)
|
|
418
550
|
fm.permissionMode = mapped;
|
|
551
|
+
// B5: allow rules are session-wide; a subagent can narrow what its
|
|
552
|
+
// session may do, never widen it.
|
|
553
|
+
if (mapped && LOOSER_MODES.has(mapped)) {
|
|
554
|
+
console.warn(`[claude-code] Agent "${agent.id}" permissions: ${agent.permissions} compiles to permissionMode ${mapped}. ` +
|
|
555
|
+
`Claude Code ignores it when the session already runs in acceptEdits, auto or bypassPermissions, ` +
|
|
556
|
+
`and a subagent cannot get permissions its session lacks: grant them in settings { allow: [...] }.`);
|
|
557
|
+
}
|
|
419
558
|
}
|
|
559
|
+
// thinking is the language's reasoning-effort field; Claude Code calls it effort.
|
|
560
|
+
if (agent.thinking && agent.thinking !== "off")
|
|
561
|
+
fm.effort = agent.thinking;
|
|
562
|
+
if (agent.color)
|
|
563
|
+
fm.color = agent.color;
|
|
564
|
+
if (agent.initialPrompt)
|
|
565
|
+
fm.initialPrompt = agent.initialPrompt;
|
|
566
|
+
if (agent.omitClaudeMd != null)
|
|
567
|
+
fm.omitClaudeMd = agent.omitClaudeMd;
|
|
420
568
|
if (agent.isolation)
|
|
421
569
|
fm.isolation = agent.isolation;
|
|
422
570
|
// Note: sandbox and fallback-chain are not recognized by Claude Code frontmatter
|
|
@@ -427,8 +575,6 @@ function generateAgents(ast) {
|
|
|
427
575
|
if (agent.fallbackChain && agent.fallbackChain.length > 0) {
|
|
428
576
|
fm["fallback-chain"] = agent.fallbackChain.join(", ");
|
|
429
577
|
}
|
|
430
|
-
// invocation: manual — Claude Code doesn't parse this field. Instead, omit
|
|
431
|
-
// description to prevent auto-delegation for manual agents.
|
|
432
578
|
if (agent.skills && agent.skills.length > 0)
|
|
433
579
|
fm.skills = agent.skills;
|
|
434
580
|
// scale is NOT a Claude Code frontmatter field — rendered only in body as prompt text
|
|
@@ -452,30 +598,28 @@ function generateAgents(ast) {
|
|
|
452
598
|
for (const hook of agent.hooks) {
|
|
453
599
|
if (hook.type === "mod")
|
|
454
600
|
continue; // engine-wide only (V94)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
hooksByEvent[
|
|
601
|
+
// In an agent's frontmatter Claude Code turns Stop into SubagentStop itself.
|
|
602
|
+
const event = hook.on === "AgentStop" ? "Stop" : claudeEvent(hook.on);
|
|
603
|
+
if (!hooksByEvent[event])
|
|
604
|
+
hooksByEvent[event] = [];
|
|
605
|
+
hooksByEvent[event].push(hook);
|
|
458
606
|
}
|
|
459
607
|
const hookLines = ["hooks:"];
|
|
460
608
|
for (const [eventName, hooks] of Object.entries(hooksByEvent)) {
|
|
461
609
|
hookLines.push(` ${eventName}:`);
|
|
462
610
|
for (const hook of hooks) {
|
|
463
611
|
const scriptName = scriptNameFromRun(hook.run);
|
|
464
|
-
if (!scriptName)
|
|
612
|
+
if (isCommandHook(hook) && !scriptName)
|
|
465
613
|
continue; // V96: prose in run names no script
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
hookLines.push(` -`);
|
|
472
|
-
}
|
|
614
|
+
const handler = hookHandler(hook, () => `bash .claude/skills/${topologyName}/scripts/${agent.id}-${scriptName}`);
|
|
615
|
+
// JSON strings are valid YAML double-quoted scalars, so a regex
|
|
616
|
+
// matcher or a prompt with quotes or colons can never break the file.
|
|
617
|
+
hookLines.push(hook.matcher ? ` - matcher: ${JSON.stringify(hook.matcher)}` : ` -`);
|
|
473
618
|
hookLines.push(` hooks:`);
|
|
474
|
-
|
|
475
|
-
hookLines.push(`
|
|
476
|
-
|
|
477
|
-
hookLines.push(`
|
|
478
|
-
}
|
|
619
|
+
const [first, ...rest] = Object.entries(handler);
|
|
620
|
+
hookLines.push(` - ${first[0]}: ${JSON.stringify(first[1])}`);
|
|
621
|
+
for (const [k, v] of rest)
|
|
622
|
+
hookLines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
479
623
|
}
|
|
480
624
|
}
|
|
481
625
|
// Insert hooks before the closing ---
|
|
@@ -493,6 +637,9 @@ function generateAgents(ast) {
|
|
|
493
637
|
}
|
|
494
638
|
sections.push("## Instructions");
|
|
495
639
|
sections.push("");
|
|
640
|
+
// Only the text between the markers is the prompt: sync and re-scaffold
|
|
641
|
+
// read it there, so generated sections below never leak into the .at.
|
|
642
|
+
sections.push(PROMPT_FENCE_OPEN);
|
|
496
643
|
if (agent.prompt) {
|
|
497
644
|
sections.push(downshiftHeadings(agent.prompt));
|
|
498
645
|
}
|
|
@@ -507,7 +654,14 @@ function generateAgents(ast) {
|
|
|
507
654
|
else {
|
|
508
655
|
sections.push("Complete the assigned task.");
|
|
509
656
|
}
|
|
657
|
+
sections.push(PROMPT_FENCE_CLOSE);
|
|
510
658
|
sections.push("");
|
|
659
|
+
if (agent.independent) {
|
|
660
|
+
sections.push("## Independence");
|
|
661
|
+
sections.push("");
|
|
662
|
+
sections.push(INDEPENDENT_NOTE);
|
|
663
|
+
sections.push("");
|
|
664
|
+
}
|
|
511
665
|
// Reads section
|
|
512
666
|
if (agent.reads && agent.reads.length > 0) {
|
|
513
667
|
sections.push("## Reads");
|
|
@@ -814,35 +968,6 @@ function generateAgents(ast) {
|
|
|
814
968
|
}
|
|
815
969
|
return files;
|
|
816
970
|
}
|
|
817
|
-
/** Generate AGENT.md files for each human node. */
|
|
818
|
-
function generateHumanNodes(ast) {
|
|
819
|
-
const files = [];
|
|
820
|
-
for (const node of ast.nodes) {
|
|
821
|
-
if (node.type !== "human")
|
|
822
|
-
continue;
|
|
823
|
-
const human = node;
|
|
824
|
-
const sections = [];
|
|
825
|
-
sections.push(`# ${toTitle(human.id)} (Human-in-the-Loop)`);
|
|
826
|
-
sections.push("");
|
|
827
|
-
if (human.description) {
|
|
828
|
-
sections.push(human.description);
|
|
829
|
-
sections.push("");
|
|
830
|
-
}
|
|
831
|
-
if (human.timeout) {
|
|
832
|
-
sections.push(`Timeout: ${human.timeout}`);
|
|
833
|
-
}
|
|
834
|
-
if (human.onTimeout) {
|
|
835
|
-
sections.push(`On timeout: ${human.onTimeout}`);
|
|
836
|
-
}
|
|
837
|
-
sections.push("");
|
|
838
|
-
files.push({
|
|
839
|
-
path: `.claude/agents/${human.id}/AGENT.md`,
|
|
840
|
-
content: sections.join("\n") + "\n",
|
|
841
|
-
category: "machine",
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
return files;
|
|
845
|
-
}
|
|
846
971
|
/**
|
|
847
972
|
* Generate group chat artifacts:
|
|
848
973
|
* - Orchestrator AGENT.md — spawns members sequentially
|
|
@@ -1259,6 +1384,48 @@ function generateTopologySkill(ast) {
|
|
|
1259
1384
|
sections.push("");
|
|
1260
1385
|
}
|
|
1261
1386
|
}
|
|
1387
|
+
// Approval steps: a human node is a point where the user decides. It used to
|
|
1388
|
+
// compile to an AGENT.md without frontmatter, which Claude Code ignores.
|
|
1389
|
+
const humans = ast.nodes.filter((n) => n.type === "human");
|
|
1390
|
+
if (humans.length > 0) {
|
|
1391
|
+
sections.push("## Approval steps");
|
|
1392
|
+
sections.push("");
|
|
1393
|
+
sections.push("At each step below, stop and ask the user (AskUserQuestion), then continue only on their approval.");
|
|
1394
|
+
sections.push("");
|
|
1395
|
+
for (const human of humans) {
|
|
1396
|
+
sections.push(`### ${toTitle(human.id)}`);
|
|
1397
|
+
if (human.description)
|
|
1398
|
+
sections.push(human.description);
|
|
1399
|
+
const before = approvalTargets(ast, human);
|
|
1400
|
+
if (before.length > 0) {
|
|
1401
|
+
sections.push(`Before: ${before.join(", ")} (Claude Code also asks the user before launching ${before.length > 1 ? "them" : "it"})`);
|
|
1402
|
+
}
|
|
1403
|
+
if (human.timeout)
|
|
1404
|
+
sections.push(`Timeout: ${human.timeout}`);
|
|
1405
|
+
if (human.onTimeout)
|
|
1406
|
+
sections.push(`On timeout: ${human.onTimeout}`);
|
|
1407
|
+
sections.push("");
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
// How each node runs, when it is not a plain subagent: the choice is on the
|
|
1411
|
+
// node (runs:, independent), and the playbook says how to call it.
|
|
1412
|
+
const special = ast.nodes.filter((n) => n.type === "agent" && (runsAsSkill(n) || !!n.independent));
|
|
1413
|
+
if (special.length > 0) {
|
|
1414
|
+
sections.push("## How these nodes run");
|
|
1415
|
+
sections.push("");
|
|
1416
|
+
for (const agent of special) {
|
|
1417
|
+
const how = agent.runs === "inline"
|
|
1418
|
+
? `invoke the \`${agent.id}\` skill yourself; it runs in your own context`
|
|
1419
|
+
: agent.runs === "fork"
|
|
1420
|
+
? `invoke the \`${agent.id}\` skill; it runs in a fresh forked context`
|
|
1421
|
+
: `launch the \`${agent.id}\` subagent`;
|
|
1422
|
+
const indep = agent.independent
|
|
1423
|
+
? "; it is independent: hand it only the artifacts it reads, never your reasoning or the author's"
|
|
1424
|
+
: "";
|
|
1425
|
+
sections.push(`- **${agent.id}**: ${how}${indep}.`);
|
|
1426
|
+
}
|
|
1427
|
+
sections.push("");
|
|
1428
|
+
}
|
|
1262
1429
|
// Schedules section
|
|
1263
1430
|
if (ast.schedules.length > 0) {
|
|
1264
1431
|
sections.push("## Schedules");
|
|
@@ -1399,6 +1566,54 @@ function generateTopologySkill(ast) {
|
|
|
1399
1566
|
return files;
|
|
1400
1567
|
}
|
|
1401
1568
|
/** Generate skill-level files from the skills block. */
|
|
1569
|
+
/** What an independent node is told, wherever it runs. */
|
|
1570
|
+
const INDEPENDENT_NOTE = "You are independent of the author: judge only the artifacts you are given to read. " +
|
|
1571
|
+
"If you are handed the author's reasoning, drafts notes or conversation, ignore them.";
|
|
1572
|
+
/**
|
|
1573
|
+
* An agent with runs: inline or fork compiles to a skill: inline runs in the
|
|
1574
|
+
* caller's own context, fork in a fresh forked one (context: fork). The node
|
|
1575
|
+
* keeps its id, description, model, tools and prompt.
|
|
1576
|
+
*/
|
|
1577
|
+
function generateAgentSkills(ast) {
|
|
1578
|
+
const files = [];
|
|
1579
|
+
for (const node of ast.nodes) {
|
|
1580
|
+
if (node.type !== "agent")
|
|
1581
|
+
continue;
|
|
1582
|
+
const agent = node;
|
|
1583
|
+
if (!runsAsSkill(agent))
|
|
1584
|
+
continue;
|
|
1585
|
+
const fm = { name: agent.id };
|
|
1586
|
+
const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
|
|
1587
|
+
if (desc)
|
|
1588
|
+
fm.description = desc;
|
|
1589
|
+
if (agent.runs === "fork")
|
|
1590
|
+
fm.context = "fork";
|
|
1591
|
+
if (agent.model && isClaudeCodeModel(agent.model))
|
|
1592
|
+
fm.model = agent.model;
|
|
1593
|
+
if (agent.thinking && agent.thinking !== "off")
|
|
1594
|
+
fm.effort = agent.thinking;
|
|
1595
|
+
if (agent.tools && agent.tools.length > 0)
|
|
1596
|
+
fm["allowed-tools"] = agent.tools.map(convertMcpToolName).join(", ");
|
|
1597
|
+
if (agent.disallowedTools && agent.disallowedTools.length > 0) {
|
|
1598
|
+
fm["disallowed-tools"] = agent.disallowedTools.map(convertMcpToolName).join(", ");
|
|
1599
|
+
}
|
|
1600
|
+
if (agent.invocation === "manual")
|
|
1601
|
+
fm["disable-model-invocation"] = true;
|
|
1602
|
+
const sections = [frontmatter(fm), "", `# ${agent.label || toTitle(agent.id)}`, ""];
|
|
1603
|
+
sections.push(agent.runs === "fork"
|
|
1604
|
+
? "Runs as a skill in a fresh forked context: it sees none of the conversation that invoked it."
|
|
1605
|
+
: "Runs as a skill in the caller's own context: the same agent, no handoff.");
|
|
1606
|
+
sections.push("", "## Instructions", "", downshiftHeadings(agent.prompt ?? desc ?? "Complete the assigned task."), "");
|
|
1607
|
+
if (agent.reads && agent.reads.length > 0)
|
|
1608
|
+
sections.push("## Reads", ...agent.reads.map((r) => `- ${r}`), "");
|
|
1609
|
+
if (agent.writes && agent.writes.length > 0)
|
|
1610
|
+
sections.push("## Writes", ...agent.writes.map((w) => `- ${w}`), "");
|
|
1611
|
+
if (agent.independent)
|
|
1612
|
+
sections.push("## Independence", "", INDEPENDENT_NOTE, "");
|
|
1613
|
+
files.push({ path: `.claude/skills/${agent.id}/SKILL.md`, content: sections.join("\n"), category: "agent" });
|
|
1614
|
+
}
|
|
1615
|
+
return files;
|
|
1616
|
+
}
|
|
1402
1617
|
function generateSkills(ast) {
|
|
1403
1618
|
const files = [];
|
|
1404
1619
|
for (const skill of ast.skills) {
|
|
@@ -1421,6 +1636,23 @@ function generateSkills(ast) {
|
|
|
1421
1636
|
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
1422
1637
|
sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName).join(", ");
|
|
1423
1638
|
}
|
|
1639
|
+
if (skill.disallowedTools && skill.disallowedTools.length > 0) {
|
|
1640
|
+
sfm["disallowed-tools"] = skill.disallowedTools.map(convertMcpToolName).join(", ");
|
|
1641
|
+
}
|
|
1642
|
+
if (skill.whenToUse)
|
|
1643
|
+
sfm.when_to_use = skill.whenToUse;
|
|
1644
|
+
if (skill.argumentHint)
|
|
1645
|
+
sfm["argument-hint"] = skill.argumentHint;
|
|
1646
|
+
if (skill.arguments && skill.arguments.length > 0)
|
|
1647
|
+
sfm.arguments = skill.arguments;
|
|
1648
|
+
if (skill.paths && skill.paths.length > 0)
|
|
1649
|
+
sfm.paths = skill.paths;
|
|
1650
|
+
if (skill.shell)
|
|
1651
|
+
sfm.shell = skill.shell;
|
|
1652
|
+
if (skill.model)
|
|
1653
|
+
sfm.model = skill.model;
|
|
1654
|
+
if (skill.thinking && skill.thinking !== "off")
|
|
1655
|
+
sfm.effort = skill.thinking;
|
|
1424
1656
|
// Only emit frontmatter if there are fields beyond just the name
|
|
1425
1657
|
if (Object.keys(sfm).length > 1) {
|
|
1426
1658
|
sections.push(frontmatter(sfm));
|
|
@@ -1453,9 +1685,19 @@ function generateSkills(ast) {
|
|
|
1453
1685
|
}
|
|
1454
1686
|
sections.push("");
|
|
1455
1687
|
}
|
|
1688
|
+
// The instructions themselves, or a file that holds them. "See: path" was
|
|
1689
|
+
// written before: Claude Code loads a skill's other files only when the
|
|
1690
|
+
// skill tells it to, so the instructions never reached the model.
|
|
1456
1691
|
if (skill.prompt) {
|
|
1457
|
-
sections.push("##
|
|
1458
|
-
sections.push(
|
|
1692
|
+
sections.push("## Instructions");
|
|
1693
|
+
sections.push("");
|
|
1694
|
+
const path = skill.prompt.trim();
|
|
1695
|
+
if (!/\s/.test(path) && /\.(md|txt)$/.test(path)) {
|
|
1696
|
+
sections.push(`Read \`${path}\` before anything else and follow it: it holds this skill's instructions.`);
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
sections.push(downshiftHeadings(skill.prompt));
|
|
1700
|
+
}
|
|
1459
1701
|
sections.push("");
|
|
1460
1702
|
}
|
|
1461
1703
|
files.push({
|
|
@@ -1647,13 +1889,21 @@ function generateGateScripts(ast) {
|
|
|
1647
1889
|
continue;
|
|
1648
1890
|
const gate = node;
|
|
1649
1891
|
const scriptName = scriptNameFromRun(gate.run);
|
|
1650
|
-
if (scriptName)
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1892
|
+
if (!scriptName)
|
|
1893
|
+
continue;
|
|
1894
|
+
const { script } = runParts(gate.run);
|
|
1895
|
+
// The user's own script, outside the project: never stubbed.
|
|
1896
|
+
if (script.startsWith("/"))
|
|
1897
|
+
continue;
|
|
1898
|
+
// A project path names the user's script: a stub only fills the gap until
|
|
1899
|
+
// it exists, and is never written over it.
|
|
1900
|
+
const project = isProjectPath(script);
|
|
1901
|
+
files.push({
|
|
1902
|
+
path: project ? script.replace(/^\.\//, "") : `.claude/skills/${name}/scripts/${scriptName}`,
|
|
1903
|
+
content: shellStub(`Gate: ${gate.id} — ${gate.run}`),
|
|
1904
|
+
category: "script",
|
|
1905
|
+
...(project ? { createOnly: true } : {}),
|
|
1906
|
+
});
|
|
1657
1907
|
}
|
|
1658
1908
|
return files;
|
|
1659
1909
|
}
|
|
@@ -1669,20 +1919,22 @@ function generateGateWrapperScripts(ast) {
|
|
|
1669
1919
|
continue;
|
|
1670
1920
|
if (gate.behavior === "advisory")
|
|
1671
1921
|
continue;
|
|
1672
|
-
|
|
1673
|
-
if (!gateScriptName)
|
|
1922
|
+
if (!scriptNameFromRun(gate.run))
|
|
1674
1923
|
continue; // V96: prose in run names no script
|
|
1924
|
+
const gateCommand = hookCommand(gate.run, name);
|
|
1675
1925
|
const onFailExit = gate.onFail === "halt"
|
|
1676
1926
|
? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
|
|
1677
1927
|
: `echo "Gate '${gate.id}' failed — bounce back and fix the issues." >&2\n exit 2`;
|
|
1678
1928
|
const inlineMode = getDelegationMode(ast) === "inline";
|
|
1679
|
-
const
|
|
1680
|
-
const wiredAsHook =
|
|
1681
|
-
gateAfters.length > 0 &&
|
|
1682
|
-
gateAfters.every((a) => isRegisteredSubagentType(ast, a));
|
|
1929
|
+
const wiring = gateWiring(ast, gate);
|
|
1930
|
+
const wiredAsHook = wiring.kind === "subagent-stop";
|
|
1683
1931
|
let enforcementNote;
|
|
1684
1932
|
let enforcementNote2;
|
|
1685
|
-
if (
|
|
1933
|
+
if (wiring.kind === "before-agent") {
|
|
1934
|
+
enforcementNote = `# Enforcement: PreToolUse hook (before-agent.sh) before Claude Code launches "${wiring.agents.join('", "')}".`;
|
|
1935
|
+
enforcementNote2 = `# Exit 2 blocks the launch and surfaces stderr to Claude.`;
|
|
1936
|
+
}
|
|
1937
|
+
else if (inlineMode) {
|
|
1686
1938
|
enforcementNote = `# Enforcement: NOT wired as a SubagentStop hook (orchestrator.delegation is "inline").`;
|
|
1687
1939
|
enforcementNote2 = `# Invoke this script from your /${ast.topology.name} playbook at the right step.`;
|
|
1688
1940
|
}
|
|
@@ -1718,10 +1970,10 @@ function generateGateWrapperScripts(ast) {
|
|
|
1718
1970
|
contentLines.push("while [ $ATTEMPT -le $MAX_RETRIES ] && [ $GATE_RESULT -ne 0 ]; do");
|
|
1719
1971
|
contentLines.push(" ATTEMPT=$((ATTEMPT + 1))");
|
|
1720
1972
|
if (gate.timeout) {
|
|
1721
|
-
contentLines.push(` timeout ${gate.timeout}
|
|
1973
|
+
contentLines.push(` timeout ${gate.timeout} ${gateCommand} && GATE_RESULT=0 || GATE_RESULT=$?`);
|
|
1722
1974
|
}
|
|
1723
1975
|
else {
|
|
1724
|
-
contentLines.push(`
|
|
1976
|
+
contentLines.push(` ${gateCommand} && GATE_RESULT=0 || GATE_RESULT=$?`);
|
|
1725
1977
|
}
|
|
1726
1978
|
contentLines.push(' if [ $GATE_RESULT -ne 0 ] && [ $ATTEMPT -le $MAX_RETRIES ]; then');
|
|
1727
1979
|
contentLines.push(` echo "Gate '${gate.id}' attempt $ATTEMPT failed, retrying..."`);
|
|
@@ -1734,10 +1986,10 @@ function generateGateWrapperScripts(ast) {
|
|
|
1734
1986
|
contentLines.push("# Run the gate check script");
|
|
1735
1987
|
contentLines.push(`GATE_RESULT=0`);
|
|
1736
1988
|
if (gate.timeout) {
|
|
1737
|
-
contentLines.push(`timeout ${gate.timeout}
|
|
1989
|
+
contentLines.push(`timeout ${gate.timeout} ${gateCommand} || GATE_RESULT=$?`);
|
|
1738
1990
|
}
|
|
1739
1991
|
else {
|
|
1740
|
-
contentLines.push(
|
|
1992
|
+
contentLines.push(`${gateCommand} || GATE_RESULT=$?`);
|
|
1741
1993
|
}
|
|
1742
1994
|
}
|
|
1743
1995
|
contentLines.push("");
|
|
@@ -1767,7 +2019,7 @@ function observabilityStub(hookName, eventName, matcherHint) {
|
|
|
1767
2019
|
if (isFinishEvent) {
|
|
1768
2020
|
return [
|
|
1769
2021
|
"#!/usr/bin/env bash",
|
|
1770
|
-
`# Auto-generated by agentopology scaffold
|
|
2022
|
+
`# Auto-generated by agentopology scaffold`,
|
|
1771
2023
|
`# Hook: ${hookName} · Event: ${eventName} · Matcher: ${matcherHint || "*"}`,
|
|
1772
2024
|
"# Replace this stub with your implementation.",
|
|
1773
2025
|
"# Stdin JSON payload from Claude Code is available on FD 0.",
|
|
@@ -1783,7 +2035,7 @@ function observabilityStub(hookName, eventName, matcherHint) {
|
|
|
1783
2035
|
}
|
|
1784
2036
|
return [
|
|
1785
2037
|
"#!/usr/bin/env bash",
|
|
1786
|
-
`# Auto-generated by agentopology scaffold
|
|
2038
|
+
`# Auto-generated by agentopology scaffold`,
|
|
1787
2039
|
`# Hook: ${hookName} · Event: ${eventName} · Matcher: ${matcherHint || "*"}`,
|
|
1788
2040
|
"# Replace this stub with your implementation.",
|
|
1789
2041
|
"# Stdin JSON payload from Claude Code is available on FD 0.",
|
|
@@ -1922,20 +2174,20 @@ function generateHookScripts(ast) {
|
|
|
1922
2174
|
continue;
|
|
1923
2175
|
// A Mod's run: is the user's TypeScript module — never overwrite it with a
|
|
1924
2176
|
// bash stub. generateModPlugin imports it instead.
|
|
1925
|
-
if (hook
|
|
1926
|
-
continue;
|
|
2177
|
+
if (!isCommandHook(hook))
|
|
2178
|
+
continue; // a mod's module, or no script at all
|
|
1927
2179
|
const runPath = hook.run;
|
|
1928
2180
|
// If the run path starts with '.' or '/' it is already project-relative —
|
|
1929
2181
|
// generate the stub at exactly that path (observability pattern).
|
|
1930
2182
|
// Otherwise scope the stub into this topology's skill scripts directory.
|
|
1931
2183
|
if (proseInRun(runPath))
|
|
1932
2184
|
continue; // V96: prose in run names no script
|
|
1933
|
-
const { script } =
|
|
2185
|
+
const { script } = runParts(runPath);
|
|
1934
2186
|
// An absolute script is the user's own, outside the project: never stubbed.
|
|
1935
2187
|
if (script.startsWith("/"))
|
|
1936
2188
|
continue;
|
|
1937
2189
|
const scoped = scriptNameFromRun(script);
|
|
1938
|
-
const filePath =
|
|
2190
|
+
const filePath = isProjectPath(script)
|
|
1939
2191
|
? script
|
|
1940
2192
|
: scoped
|
|
1941
2193
|
? `.claude/skills/${name}/scripts/${scoped}`
|
|
@@ -1949,7 +2201,7 @@ function generateHookScripts(ast) {
|
|
|
1949
2201
|
executable: true,
|
|
1950
2202
|
// A run that names its own path names the user's script: a stub only
|
|
1951
2203
|
// fills the gap until it exists. --force used to overwrite it.
|
|
1952
|
-
...(
|
|
2204
|
+
...(isProjectPath(script) ? { createOnly: true } : {}),
|
|
1953
2205
|
});
|
|
1954
2206
|
}
|
|
1955
2207
|
return files;
|
|
@@ -1965,7 +2217,7 @@ function generateAgentHookScripts(ast) {
|
|
|
1965
2217
|
if (!agent.hooks || agent.hooks.length === 0)
|
|
1966
2218
|
continue;
|
|
1967
2219
|
for (const hook of agent.hooks) {
|
|
1968
|
-
if (!hook.run || hook
|
|
2220
|
+
if (!hook.run || !isCommandHook(hook))
|
|
1969
2221
|
continue;
|
|
1970
2222
|
const scriptName = scriptNameFromRun(hook.run);
|
|
1971
2223
|
if (!scriptName)
|
|
@@ -1987,31 +2239,22 @@ function generateAgentHookScripts(ast) {
|
|
|
1987
2239
|
}
|
|
1988
2240
|
return files;
|
|
1989
2241
|
}
|
|
1990
|
-
const isHookPath = (w = "") => w.startsWith(".") || w.startsWith("/");
|
|
1991
|
-
/** A hook's `run:` split: the interpreter it names (if any), its script, its arguments. */
|
|
1992
|
-
function hookRun(run) {
|
|
1993
|
-
const words = run.trim().split(/\s+/);
|
|
1994
|
-
const named = words.length > 1 && !/\.(py|sh|ts|mjs|js)$/.test(words[0]) && isHookPath(words[1]);
|
|
1995
|
-
const [script = "", ...args] = named ? words.slice(1) : words;
|
|
1996
|
-
return { ...(named ? { interpreter: words[0] } : {}), script, args };
|
|
1997
|
-
}
|
|
1998
2242
|
/**
|
|
1999
|
-
* The shell command for a command hook's `run:`.
|
|
2243
|
+
* The shell command for a command hook's or gate's `run:`.
|
|
2000
2244
|
*
|
|
2001
|
-
* A `run:` whose script (
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
* names, else the one its extension names (`.py` → python3, anything else →
|
|
2007
|
-
* bash), with its arguments kept.
|
|
2245
|
+
* A `run:` whose script (see runParts) is a project path names the user's own
|
|
2246
|
+
* script. A project-relative one runs through `$CLAUDE_PROJECT_DIR` (a hook's
|
|
2247
|
+
* cwd is the session's, which moves); an absolute one is kept as written. It
|
|
2248
|
+
* runs by the interpreter the `run:` names, else the one its extension names
|
|
2249
|
+
* (`.py` → python3, anything else → bash). Arguments are kept.
|
|
2008
2250
|
*
|
|
2009
2251
|
* Any other `run:` is a script this topology's skill scaffolds, by basename.
|
|
2010
2252
|
*/
|
|
2011
2253
|
export function hookCommand(run, topologyName) {
|
|
2012
|
-
const { interpreter: named, script: first, args: rest } =
|
|
2013
|
-
if (!
|
|
2014
|
-
|
|
2254
|
+
const { interpreter: named, script: first, args: rest } = runParts(run);
|
|
2255
|
+
if (!isProjectPath(first)) {
|
|
2256
|
+
const base = first.replace(/^.*\//, "");
|
|
2257
|
+
return [named ?? "bash", `.claude/skills/${topologyName}/scripts/${base}`, ...rest].join(" ");
|
|
2015
2258
|
}
|
|
2016
2259
|
const interpreter = named ?? (first.endsWith(".py") ? "python3" : "bash");
|
|
2017
2260
|
const where = first.startsWith("/")
|
|
@@ -2035,7 +2278,7 @@ export function settingsHooksOf(ast) {
|
|
|
2035
2278
|
// emitted as a plugin (generateModPlugin).
|
|
2036
2279
|
if (hook.type === "mod")
|
|
2037
2280
|
continue;
|
|
2038
|
-
const eventName = hook.on;
|
|
2281
|
+
const eventName = claudeEvent(hook.on);
|
|
2039
2282
|
// Inline orchestrator mode never spawns subagents, so SubagentStop /
|
|
2040
2283
|
// SubagentStart hooks would never fire. Warn the user and skip emission
|
|
2041
2284
|
// — keeping them in settings.json would be a maintenance trap.
|
|
@@ -2046,19 +2289,13 @@ export function settingsHooksOf(ast) {
|
|
|
2046
2289
|
`delegation to "subagent".`);
|
|
2047
2290
|
continue;
|
|
2048
2291
|
}
|
|
2049
|
-
if (proseInRun(hook.run))
|
|
2292
|
+
if (isCommandHook(hook) && proseInRun(hook.run))
|
|
2050
2293
|
continue; // V96: prose in run names no script
|
|
2051
2294
|
if (!hooksByEvent[eventName]) {
|
|
2052
2295
|
hooksByEvent[eventName] = [];
|
|
2053
2296
|
}
|
|
2054
2297
|
const hookEntry = {
|
|
2055
|
-
hooks: [
|
|
2056
|
-
{
|
|
2057
|
-
type: hook.type ?? "command",
|
|
2058
|
-
command: hookCommand(hook.run, topologyName),
|
|
2059
|
-
...(hook.timeout ? { timeout: hook.timeout } : {}),
|
|
2060
|
-
},
|
|
2061
|
-
],
|
|
2298
|
+
hooks: [hookHandler(hook, () => hookCommand(hook.run, topologyName))],
|
|
2062
2299
|
};
|
|
2063
2300
|
// Only add matcher when it has a value
|
|
2064
2301
|
if (hook.matcher) {
|
|
@@ -2121,11 +2358,15 @@ function generateSettings(ast) {
|
|
|
2121
2358
|
`orchestrator playbook at the right step.`);
|
|
2122
2359
|
continue;
|
|
2123
2360
|
}
|
|
2124
|
-
// SubagentStop matcher must be a registered subagent_type.
|
|
2125
|
-
//
|
|
2126
|
-
// skip so we don't
|
|
2361
|
+
// SubagentStop matcher must be a registered subagent_type. A gate after an
|
|
2362
|
+
// action or a human runs before its next agent instead (before-agent.sh,
|
|
2363
|
+
// wired below); with no agent on either side, warn and skip so we don't
|
|
2364
|
+
// ship dead config that never fires.
|
|
2127
2365
|
const anchors = gateAnchors(gate, "after");
|
|
2128
|
-
|
|
2366
|
+
const wiring = gateWiring(ast, gate);
|
|
2367
|
+
if (wiring.kind === "before-agent")
|
|
2368
|
+
continue;
|
|
2369
|
+
if (wiring.kind !== "subagent-stop") {
|
|
2129
2370
|
const bad = anchors.filter((a) => !isRegisteredSubagentType(ast, a));
|
|
2130
2371
|
const targetDesc = anchors.length
|
|
2131
2372
|
? `non-agent target ${bad.map((b) => `"${b}"`).join(", ")}`
|
|
@@ -2164,6 +2405,15 @@ function generateSettings(ast) {
|
|
|
2164
2405
|
// Blackboard implies a host-side observer that watches the workflow rung's
|
|
2165
2406
|
// Write tool-calls LIVE. This is the host's piece of the hybrid's concurrent
|
|
2166
2407
|
// observability (the deterministic workflow rung cannot express it).
|
|
2408
|
+
if (generateBeforeAgentScript(ast)) {
|
|
2409
|
+
if (!hooksByEvent.PreToolUse)
|
|
2410
|
+
hooksByEvent.PreToolUse = [];
|
|
2411
|
+
hooksByEvent.PreToolUse.push({
|
|
2412
|
+
// Agent is the subagent tool; Task is its earlier name.
|
|
2413
|
+
matcher: "Agent|Task",
|
|
2414
|
+
hooks: [{ type: "command", command: beforeAgentCommand(ast) }],
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2167
2417
|
const observeHook = buildHybridObserveHook(ast, topologyName);
|
|
2168
2418
|
if (observeHook) {
|
|
2169
2419
|
if (!hooksByEvent[observeHook.event])
|
|
@@ -2182,13 +2432,12 @@ function generateSettings(ast) {
|
|
|
2182
2432
|
// Permissions section from settings block — nested under "permissions" key
|
|
2183
2433
|
const perms = ast.settings;
|
|
2184
2434
|
if (perms) {
|
|
2185
|
-
const allow = perms.allow;
|
|
2186
|
-
const deny = perms.deny;
|
|
2187
2435
|
const permObj = {};
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2436
|
+
for (const key of ["allow", "ask", "deny"]) {
|
|
2437
|
+
const rules = perms[key];
|
|
2438
|
+
if (rules && rules.length > 0)
|
|
2439
|
+
permObj[key] = rules.map(convertMcpToolName);
|
|
2440
|
+
}
|
|
2192
2441
|
if (Object.keys(permObj).length > 0)
|
|
2193
2442
|
settings.permissions = permObj;
|
|
2194
2443
|
}
|
|
@@ -2242,9 +2491,12 @@ function generateMcpJson(ast) {
|
|
|
2242
2491
|
for (const [name, config] of Object.entries(ast.mcpServers)) {
|
|
2243
2492
|
const entry = {};
|
|
2244
2493
|
for (const [key, value] of Object.entries(config)) {
|
|
2245
|
-
if (key === "command" || key === "url") {
|
|
2494
|
+
if (key === "command" || key === "url" || key === "type") {
|
|
2246
2495
|
entry[key] = value;
|
|
2247
2496
|
}
|
|
2497
|
+
else if (key === "headers" && typeof value === "object" && value !== null) {
|
|
2498
|
+
entry.headers = value;
|
|
2499
|
+
}
|
|
2248
2500
|
else if (key === "args" && Array.isArray(value)) {
|
|
2249
2501
|
entry.args = value;
|
|
2250
2502
|
}
|
|
@@ -2256,8 +2508,11 @@ function generateMcpJson(ast) {
|
|
|
2256
2508
|
// Only include servers that have a command or url (filter parser artifacts)
|
|
2257
2509
|
if (!("command" in entry) && !("url" in entry))
|
|
2258
2510
|
continue;
|
|
2259
|
-
//
|
|
2260
|
-
if (!("
|
|
2511
|
+
// Claude Code reads a url entry without type as stdio and skips it.
|
|
2512
|
+
if ("url" in entry && !("type" in entry))
|
|
2513
|
+
entry.type = "http";
|
|
2514
|
+
// Always include env field for a stdio server in .mcp.json
|
|
2515
|
+
if (!("url" in entry) && !("env" in entry)) {
|
|
2261
2516
|
entry.env = {};
|
|
2262
2517
|
}
|
|
2263
2518
|
servers[name] = entry;
|
|
@@ -2338,6 +2593,7 @@ function generateToolScripts(ast) {
|
|
|
2338
2593
|
shebang,
|
|
2339
2594
|
`# Tool: ${tool.id} — ${tool.description}`,
|
|
2340
2595
|
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
2596
|
+
tool.lang === "node" ? `// ${STUB_MARKER}` : STUB_MARKER,
|
|
2341
2597
|
"",
|
|
2342
2598
|
];
|
|
2343
2599
|
if (tool.lang === "bash" || !tool.lang) {
|
|
@@ -2358,14 +2614,50 @@ function generateToolScripts(ast) {
|
|
|
2358
2614
|
lines.push(`echo "TODO: implement ${tool.id}"`);
|
|
2359
2615
|
}
|
|
2360
2616
|
lines.push("");
|
|
2617
|
+
// The stub goes where the tool's script: says, not to a copy beside it: a
|
|
2618
|
+
// project path is the user's script (create-only); an absolute one is never
|
|
2619
|
+
// touched; a bare name lives in the skill's scripts folder.
|
|
2620
|
+
const script = tool.script ? runParts(tool.script).script : "";
|
|
2621
|
+
if (script.startsWith("/"))
|
|
2622
|
+
continue;
|
|
2623
|
+
const project = isProjectPath(script);
|
|
2624
|
+
const base = script.replace(/^.*\//, "");
|
|
2361
2625
|
files.push({
|
|
2362
|
-
path:
|
|
2626
|
+
path: project
|
|
2627
|
+
? script.replace(/^\.\//, "")
|
|
2628
|
+
: `.claude/skills/${name}/scripts/${base || `${tool.id}.${ext}`}`,
|
|
2363
2629
|
content: lines.join("\n"),
|
|
2364
2630
|
category: "script",
|
|
2631
|
+
...(project ? { createOnly: true } : {}),
|
|
2365
2632
|
});
|
|
2366
2633
|
}
|
|
2367
2634
|
return files;
|
|
2368
2635
|
}
|
|
2636
|
+
/**
|
|
2637
|
+
* A cron expression for an `every:` duration, or null when cron cannot say
|
|
2638
|
+
* it. Only "1h", "daily" and a few words converted before; anything else
|
|
2639
|
+
* became a commented-out crontab line that never ran.
|
|
2640
|
+
*/
|
|
2641
|
+
function everyToCron(every) {
|
|
2642
|
+
const text = every.trim().toLowerCase();
|
|
2643
|
+
const words = { hourly: "0 * * * *", daily: "0 0 * * *", weekly: "0 0 * * 0" };
|
|
2644
|
+
if (words[text])
|
|
2645
|
+
return words[text];
|
|
2646
|
+
const m = text.match(/^(\d+)\s*(m|min|mins|minutes?|h|hours?|d|days?|w|weeks?)$/);
|
|
2647
|
+
if (!m)
|
|
2648
|
+
return null;
|
|
2649
|
+
const n = Number(m[1]);
|
|
2650
|
+
const unit = m[2][0];
|
|
2651
|
+
if (n <= 0)
|
|
2652
|
+
return null;
|
|
2653
|
+
if (unit === "m")
|
|
2654
|
+
return n < 60 && 60 % n === 0 ? (n === 1 ? "* * * * *" : `*/${n} * * * *`) : null;
|
|
2655
|
+
if (unit === "h")
|
|
2656
|
+
return n < 24 && 24 % n === 0 ? (n === 1 ? "0 * * * *" : `0 */${n} * * *`) : null;
|
|
2657
|
+
if (unit === "d")
|
|
2658
|
+
return n === 1 ? "0 0 * * *" : null;
|
|
2659
|
+
return n === 1 ? "0 0 * * 0" : null;
|
|
2660
|
+
}
|
|
2369
2661
|
/** Generate schema documentation files. */
|
|
2370
2662
|
function generateSchemas(ast) {
|
|
2371
2663
|
const files = [];
|
|
@@ -2410,15 +2702,18 @@ function generateScheduleScripts(ast) {
|
|
|
2410
2702
|
`# Schedule: ${job.id} — invokes ${target}`,
|
|
2411
2703
|
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
2412
2704
|
'set -euo pipefail',
|
|
2705
|
+
"# cron starts in $HOME: run from the project root (this script is in scripts/).",
|
|
2706
|
+
'cd "$(dirname "$0")/.."',
|
|
2413
2707
|
"",
|
|
2414
2708
|
];
|
|
2415
2709
|
if (job.agent) {
|
|
2710
|
+
// Headless: cron has no terminal, so an interactive session never ran.
|
|
2416
2711
|
lines.push(`# Invoke agent: ${job.agent}`);
|
|
2417
|
-
lines.push(`claude --agent ${job.agent}`);
|
|
2712
|
+
lines.push(`claude -p "Run your scheduled task (${job.id})." --agent ${job.agent}`);
|
|
2418
2713
|
}
|
|
2419
2714
|
else if (job.action) {
|
|
2420
2715
|
lines.push(`# Invoke action: ${job.action}`);
|
|
2421
|
-
lines.push(`bash scripts/action-${job.action}.sh`);
|
|
2716
|
+
lines.push(`bash .claude/skills/${ast.topology.name}/scripts/action-${job.action}.sh`);
|
|
2422
2717
|
}
|
|
2423
2718
|
if (job.enabled === false) {
|
|
2424
2719
|
lines.unshift("# DISABLED — this schedule is not enabled");
|
|
@@ -2428,18 +2723,12 @@ function generateScheduleScripts(ast) {
|
|
|
2428
2723
|
// Add crontab entry
|
|
2429
2724
|
let cronExpr = job.cron ?? "# every: " + (job.every ?? "unknown");
|
|
2430
2725
|
if (!job.cron && job.every) {
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
"
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
"1 day": "0 0 * * *",
|
|
2438
|
-
"daily": "0 0 * * *",
|
|
2439
|
-
"1w": "0 0 * * 0",
|
|
2440
|
-
"weekly": "0 0 * * 0",
|
|
2441
|
-
};
|
|
2442
|
-
cronExpr = everyMap[job.every.toLowerCase()] ?? `# every: ${job.every}`;
|
|
2726
|
+
const converted = everyToCron(job.every);
|
|
2727
|
+
if (!converted) {
|
|
2728
|
+
console.warn(`[claude-code] Schedule "${job.id}" every: ${job.every} has no cron form; ` +
|
|
2729
|
+
`its crontab line is commented out. Use cron: instead.`);
|
|
2730
|
+
}
|
|
2731
|
+
cronExpr = converted ?? `# every: ${job.every}`;
|
|
2443
2732
|
}
|
|
2444
2733
|
const enabledPrefix = job.enabled === false ? "# DISABLED: " : "";
|
|
2445
2734
|
cronLines.push(`${enabledPrefix}${cronExpr} bash ${scriptPath} # ${job.id}`);
|
|
@@ -2607,7 +2896,11 @@ function generateWorkspaceProtocol(ast) {
|
|
|
2607
2896
|
* destroy their content.
|
|
2608
2897
|
*/
|
|
2609
2898
|
function generateContextFile(ast) {
|
|
2610
|
-
|
|
2899
|
+
// A rules file, one per topology: Claude Code loads .claude/rules/*.md like
|
|
2900
|
+
// CLAUDE.md, so the context still reaches every session, but it never
|
|
2901
|
+
// competes with the project's own CLAUDE.md or .claude/CLAUDE.md (it used to
|
|
2902
|
+
// be written to .claude/CLAUDE.md, a conflict in any project that had one).
|
|
2903
|
+
const DEFAULT_PATH = `.claude/rules/${ast.topology.name}.md`;
|
|
2611
2904
|
let fileName = ast.context.file ?? DEFAULT_PATH;
|
|
2612
2905
|
// Safety: never overwrite the user's root-level CLAUDE.md
|
|
2613
2906
|
if (fileName === "CLAUDE.md" || fileName === "./CLAUDE.md") {
|
|
@@ -2804,7 +3097,7 @@ function generateWorkflowLaunchArtifacts(ast) {
|
|
|
2804
3097
|
}
|
|
2805
3098
|
/**
|
|
2806
3099
|
* HYBRID: build the "Hybrid run order" markdown block appended to the generated
|
|
2807
|
-
* context file (.claude/
|
|
3100
|
+
* context file (.claude/rules/<topology>.md) and the topology SKILL.md. It states the
|
|
2808
3101
|
* host's interleaving with the embedded workflow rung, in the SAME run-order
|
|
2809
3102
|
* vocabulary the claude-workflow binding's SEAM.md uses, so the two bindings
|
|
2810
3103
|
* stay in sync. Returns null when the topology is not a hybrid.
|
|
@@ -3061,7 +3354,6 @@ export const claudeCodeBinding = {
|
|
|
3061
3354
|
// Agent files — may include group conversation protocol sections
|
|
3062
3355
|
files.push(...generateAgents(ast));
|
|
3063
3356
|
// Human nodes
|
|
3064
|
-
files.push(...generateHumanNodes(ast));
|
|
3065
3357
|
// Main topology skill (with frontmatter)
|
|
3066
3358
|
const skillFiles = generateTopologySkill(ast);
|
|
3067
3359
|
// Append trigger info to the main skill
|
|
@@ -3071,6 +3363,7 @@ export const claudeCodeBinding = {
|
|
|
3071
3363
|
files.push(...skillFiles);
|
|
3072
3364
|
// Named skills
|
|
3073
3365
|
files.push(...generateSkills(ast));
|
|
3366
|
+
files.push(...generateAgentSkills(ast));
|
|
3074
3367
|
// Schemas
|
|
3075
3368
|
files.push(...generateSchemas(ast));
|
|
3076
3369
|
// Memory directories
|
|
@@ -3080,6 +3373,9 @@ export const claudeCodeBinding = {
|
|
|
3080
3373
|
// Gate scripts
|
|
3081
3374
|
files.push(...generateGateScripts(ast));
|
|
3082
3375
|
files.push(...generateGateWrapperScripts(ast));
|
|
3376
|
+
const beforeAgent = generateBeforeAgentScript(ast);
|
|
3377
|
+
if (beforeAgent)
|
|
3378
|
+
files.push(beforeAgent);
|
|
3083
3379
|
// Hook scripts (global)
|
|
3084
3380
|
files.push(...generateHookScripts(ast));
|
|
3085
3381
|
// Hook scripts (per-agent)
|