agentopology 0.2.0 → 0.2.3
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 +7 -5
- package/dist/analyzer/__tests__/observability-gap.test.d.ts +8 -0
- package/dist/analyzer/__tests__/observability-gap.test.d.ts.map +1 -0
- package/dist/analyzer/__tests__/observability-gap.test.js +132 -0
- package/dist/analyzer/__tests__/observability-gap.test.js.map +1 -0
- package/dist/analyzer/index.d.ts +1 -1
- package/dist/analyzer/index.d.ts.map +1 -1
- package/dist/analyzer/index.js +26 -0
- package/dist/analyzer/index.js.map +1 -1
- package/dist/bindings/__tests__/bindings.test.js +288 -11
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/__tests__/observability.test.d.ts +9 -0
- package/dist/bindings/__tests__/observability.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/observability.test.js +136 -0
- package/dist/bindings/__tests__/observability.test.js.map +1 -0
- package/dist/bindings/__tests__/stub-generation.test.d.ts +16 -0
- package/dist/bindings/__tests__/stub-generation.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/stub-generation.test.js +117 -0
- package/dist/bindings/__tests__/stub-generation.test.js.map +1 -0
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +159 -110
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/codex.d.ts.map +1 -1
- package/dist/bindings/codex.js +1 -12
- package/dist/bindings/codex.js.map +1 -1
- package/dist/bindings/cursor.d.ts.map +1 -1
- package/dist/bindings/cursor.js +0 -12
- package/dist/bindings/cursor.js.map +1 -1
- package/dist/bindings/gemini-cli.d.ts.map +1 -1
- package/dist/bindings/gemini-cli.js +1 -12
- package/dist/bindings/gemini-cli.js.map +1 -1
- package/dist/bindings/kiro.d.ts.map +1 -1
- package/dist/bindings/kiro.js +1 -12
- package/dist/bindings/kiro.js.map +1 -1
- package/dist/bindings/lib/__tests__/stub.test.d.ts +8 -0
- package/dist/bindings/lib/__tests__/stub.test.d.ts.map +1 -0
- package/dist/bindings/lib/__tests__/stub.test.js +66 -0
- package/dist/bindings/lib/__tests__/stub.test.js.map +1 -0
- package/dist/bindings/lib/stub.d.ts +56 -0
- package/dist/bindings/lib/stub.d.ts.map +1 -0
- package/dist/bindings/lib/stub.js +69 -0
- package/dist/bindings/lib/stub.js.map +1 -0
- package/dist/bindings/openclaw.d.ts.map +1 -1
- package/dist/bindings/openclaw.js +1 -12
- package/dist/bindings/openclaw.js.map +1 -1
- package/dist/bindings/types.d.ts +5 -0
- package/dist/bindings/types.d.ts.map +1 -1
- package/dist/bindings/types.js.map +1 -1
- package/dist/cli/index.js +89 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/content.d.ts.map +1 -1
- package/dist/docs/content.js +9 -5
- package/dist/docs/content.js.map +1 -1
- package/dist/import/__tests__/import.test.js +2 -2
- package/dist/import/__tests__/import.test.js.map +1 -1
- package/dist/parser/__tests__/parser.test.js +31 -3
- package/dist/parser/__tests__/parser.test.js.map +1 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +28 -8
- package/dist/parser/validator.js.map +1 -1
- package/dist/scaffold/__tests__/scaffold.test.js +20 -0
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
- package/dist/scaffold/__tests__/settings-merge.test.d.ts +15 -0
- package/dist/scaffold/__tests__/settings-merge.test.d.ts.map +1 -0
- package/dist/scaffold/__tests__/settings-merge.test.js +127 -0
- package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -0
- package/dist/scaffold/incremental.d.ts.map +1 -1
- package/dist/scaffold/incremental.js +11 -2
- package/dist/scaffold/incremental.js.map +1 -1
- package/dist/scaffold/merge.d.ts +16 -0
- package/dist/scaffold/merge.d.ts.map +1 -1
- package/dist/scaffold/merge.js +77 -0
- package/dist/scaffold/merge.js.map +1 -1
- package/dist/scaffold/types.d.ts +2 -0
- package/dist/scaffold/types.d.ts.map +1 -1
- package/examples/simple-pipeline-topology.html +5700 -0
- package/package.json +2 -1
- package/skill/SKILL.md +7 -7
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Change 4 + 5: Agent-scoped env var injection + stub script generation.
|
|
3
|
+
*
|
|
4
|
+
* Change 4: When scaffold generates a hook stub, it must include logic to
|
|
5
|
+
* extract ACTIVE_AGENT from the Claude Code stdin JSON payload (agent_type field).
|
|
6
|
+
*
|
|
7
|
+
* Change 5: When a hook's run: path does not exist on disk, scaffold emits a stub
|
|
8
|
+
* at that path with:
|
|
9
|
+
* - #!/usr/bin/env bash shebang
|
|
10
|
+
* - jq-based AGENT_TYPE extraction from stdin
|
|
11
|
+
* - A metrics.jsonl append
|
|
12
|
+
* - chmod +x (executable bit)
|
|
13
|
+
* - Auto-generated marker comment
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import { parse } from "../../parser/index.js";
|
|
20
|
+
import { bindings } from "../index.js";
|
|
21
|
+
let tmpDir;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stub-gen-test-"));
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Topology with a hook pointing at a non-existent script
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const STUB_TOPOLOGY = `
|
|
32
|
+
topology stub-test : [supervisor] {
|
|
33
|
+
meta { version: "1.0.0" description: "Stub generation test" }
|
|
34
|
+
orchestrator { model: opus handles: [start] }
|
|
35
|
+
action start { kind: inline }
|
|
36
|
+
agent tracker { model: sonnet phase: 1 prompt { You are tracker. } }
|
|
37
|
+
flow { start -> tracker }
|
|
38
|
+
hooks {
|
|
39
|
+
hook log-finish {
|
|
40
|
+
on: SubagentStop
|
|
41
|
+
matcher: "tracker"
|
|
42
|
+
run: ".claude/scripts/log-finish.sh"
|
|
43
|
+
type: command
|
|
44
|
+
timeout: 5000
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Change 5 — stub generation for non-existent hook scripts
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
describe("Change 5 — hook stub generation", () => {
|
|
53
|
+
const ast = parse(STUB_TOPOLOGY);
|
|
54
|
+
const binding = bindings["claude-code"];
|
|
55
|
+
it("generates a stub script file for hook with non-existent run path", () => {
|
|
56
|
+
const files = binding.scaffold(ast);
|
|
57
|
+
// The stub should be at the path declared in hook.run
|
|
58
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
59
|
+
expect(stub).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
it("stub file has category 'script'", () => {
|
|
62
|
+
const files = binding.scaffold(ast);
|
|
63
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
64
|
+
expect(stub.category).toBe("script");
|
|
65
|
+
});
|
|
66
|
+
it("stub contains shebang line", () => {
|
|
67
|
+
const files = binding.scaffold(ast);
|
|
68
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
69
|
+
expect(stub.content).toContain("#!/usr/bin/env bash");
|
|
70
|
+
});
|
|
71
|
+
it("stub contains auto-generated marker", () => {
|
|
72
|
+
const files = binding.scaffold(ast);
|
|
73
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
74
|
+
expect(stub.content).toContain("Auto-generated by agentopology scaffold");
|
|
75
|
+
});
|
|
76
|
+
it("stub extracts AGENT_TYPE from stdin JSON (Change 4 — env var injection)", () => {
|
|
77
|
+
const files = binding.scaffold(ast);
|
|
78
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
79
|
+
// Stub must extract agent_type from the Claude Code stdin payload
|
|
80
|
+
expect(stub.content).toContain("agent_type");
|
|
81
|
+
expect(stub.content).toContain("AGENT_TYPE");
|
|
82
|
+
});
|
|
83
|
+
it("stub appends to metrics.jsonl", () => {
|
|
84
|
+
const files = binding.scaffold(ast);
|
|
85
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
86
|
+
expect(stub.content).toContain("metrics.jsonl");
|
|
87
|
+
});
|
|
88
|
+
it("stub includes hook name in header comment", () => {
|
|
89
|
+
const files = binding.scaffold(ast);
|
|
90
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
91
|
+
expect(stub.content).toContain("log-finish");
|
|
92
|
+
});
|
|
93
|
+
it("stub includes event name in header comment", () => {
|
|
94
|
+
const files = binding.scaffold(ast);
|
|
95
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
96
|
+
expect(stub.content).toContain("SubagentStop");
|
|
97
|
+
});
|
|
98
|
+
it("stub is marked executable (via a generated chmod hint or content)", () => {
|
|
99
|
+
// Since scaffold returns file content (not real fs writes), we check the
|
|
100
|
+
// category is 'script' — the executeActions function sets chmod on creation.
|
|
101
|
+
// The actual chmod happens in executeActions. We verify it in an integration
|
|
102
|
+
// test by writing the file and checking the mode.
|
|
103
|
+
const files = binding.scaffold(ast);
|
|
104
|
+
const stub = files.find((f) => f.path === ".claude/scripts/log-finish.sh");
|
|
105
|
+
expect(stub).toBeDefined();
|
|
106
|
+
expect(stub.category).toBe("script");
|
|
107
|
+
expect(stub.executable).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Change 5 — NO stub if the script already exists on disk (not scaffold's job)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Note: scaffold() generates file lists; the incremental engine decides
|
|
114
|
+
// what to write. The stub is always in the generated list — the incremental
|
|
115
|
+
// engine's "script" category + shouldOverwriteScript() prevents overwriting
|
|
116
|
+
// user-edited stubs. This is correct behaviour, already tested in scaffold.test.ts.
|
|
117
|
+
//# sourceMappingURL=stub-generation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stub-generation.test.js","sourceRoot":"","sources":["../../../src/bindings/__tests__/stub-generation.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/bindings/claude-code.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/bindings/claude-code.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAC;AAk3E/D,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,EAAE,aA0F/B,CAAC"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { deduplicateFiles } from "./types.js";
|
|
10
|
+
import { shellStub } from "./lib/stub.js";
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Helpers
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -48,6 +49,8 @@ function toTitle(id) {
|
|
|
48
49
|
* - item1
|
|
49
50
|
* - item2
|
|
50
51
|
*/
|
|
52
|
+
/** Characters that require quoting in YAML scalar values. */
|
|
53
|
+
const YAML_SPECIAL = /[:#\[\]{}&*!|>'"%@`]/;
|
|
51
54
|
function frontmatter(fields) {
|
|
52
55
|
const lines = ["---"];
|
|
53
56
|
for (const [key, value] of Object.entries(fields)) {
|
|
@@ -65,23 +68,27 @@ function frontmatter(fields) {
|
|
|
65
68
|
lines.push(`${key}: ${value}`);
|
|
66
69
|
}
|
|
67
70
|
else {
|
|
68
|
-
|
|
71
|
+
// Auto-quote strings containing YAML-special characters
|
|
72
|
+
const str = String(value);
|
|
73
|
+
const needsQuotes = YAML_SPECIAL.test(str) || str.trim() !== str;
|
|
74
|
+
lines.push(`${key}: ${needsQuotes ? `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : str}`);
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
lines.push("---");
|
|
72
78
|
return lines.join("\n");
|
|
73
79
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Check whether `name` refers to a node registered as a Claude Code subagent_type.
|
|
82
|
+
*
|
|
83
|
+
* Both `agent` and `group` nodes scaffold a `.claude/agents/<name>/AGENT.md`
|
|
84
|
+
* file, which is what Claude Code registers as a subagent_type. Action, gate,
|
|
85
|
+
* human, and orchestrator nodes are NOT registered and therefore CANNOT be
|
|
86
|
+
* used as SubagentStop matchers.
|
|
87
|
+
*/
|
|
88
|
+
function isRegisteredSubagentType(ast, name) {
|
|
89
|
+
if (!name)
|
|
90
|
+
return false;
|
|
91
|
+
return ast.nodes.some((n) => (n.type === "agent" || n.type === "group") && n.id === name);
|
|
85
92
|
}
|
|
86
93
|
/** Generate an empty directory marker (a .gitkeep file). */
|
|
87
94
|
function gitkeep(dirPath) {
|
|
@@ -93,10 +100,10 @@ function gitkeep(dirPath) {
|
|
|
93
100
|
*/
|
|
94
101
|
function mapPermissionMode(perm) {
|
|
95
102
|
// Claude Code native values — pass through
|
|
96
|
-
const nativeValues = ["plan", "
|
|
103
|
+
const nativeValues = ["plan", "default", "acceptEdits", "dontAsk", "bypassPermissions"];
|
|
97
104
|
if (nativeValues.includes(perm)) {
|
|
98
|
-
// "
|
|
99
|
-
if (perm === "
|
|
105
|
+
// "default" is the default, omit it
|
|
106
|
+
if (perm === "default")
|
|
100
107
|
return undefined;
|
|
101
108
|
return perm;
|
|
102
109
|
}
|
|
@@ -106,8 +113,10 @@ function mapPermissionMode(perm) {
|
|
|
106
113
|
return "plan";
|
|
107
114
|
case "autonomous":
|
|
108
115
|
return undefined; // default, omit
|
|
116
|
+
case "auto":
|
|
117
|
+
return undefined; // default, omit
|
|
109
118
|
case "interactive":
|
|
110
|
-
return "
|
|
119
|
+
return "acceptEdits";
|
|
111
120
|
case "unrestricted":
|
|
112
121
|
return "bypassPermissions";
|
|
113
122
|
default:
|
|
@@ -229,7 +238,7 @@ function generateAgents(ast) {
|
|
|
229
238
|
if (agent.invocation !== "manual") {
|
|
230
239
|
const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
|
|
231
240
|
if (desc) {
|
|
232
|
-
fm.description =
|
|
241
|
+
fm.description = desc;
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
if (agent.model)
|
|
@@ -237,9 +246,9 @@ function generateAgents(ast) {
|
|
|
237
246
|
if (agent.maxTurns != null)
|
|
238
247
|
fm.maxTurns = String(agent.maxTurns);
|
|
239
248
|
if (agent.tools && agent.tools.length > 0)
|
|
240
|
-
fm.tools = agent.tools.map(convertMcpToolName);
|
|
249
|
+
fm.tools = agent.tools.map(convertMcpToolName).join(", ");
|
|
241
250
|
if (agent.disallowedTools && agent.disallowedTools.length > 0)
|
|
242
|
-
fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName);
|
|
251
|
+
fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName).join(", ");
|
|
243
252
|
if (agent.mcpServers && agent.mcpServers.length > 0)
|
|
244
253
|
fm.mcpServers = agent.mcpServers;
|
|
245
254
|
if (agent.background === true)
|
|
@@ -836,7 +845,7 @@ function generateGroupNodes(ast) {
|
|
|
836
845
|
const skillFm = {};
|
|
837
846
|
skillFm.name = group.id;
|
|
838
847
|
if (group.description)
|
|
839
|
-
skillFm.description =
|
|
848
|
+
skillFm.description = group.description;
|
|
840
849
|
skillFm["user-invocable"] = true;
|
|
841
850
|
const skillSections = [];
|
|
842
851
|
skillSections.push(frontmatter(skillFm));
|
|
@@ -853,7 +862,7 @@ function generateGroupNodes(ast) {
|
|
|
853
862
|
const agentFm = {};
|
|
854
863
|
agentFm.name = group.id;
|
|
855
864
|
if (group.description)
|
|
856
|
-
agentFm.description =
|
|
865
|
+
agentFm.description = group.description;
|
|
857
866
|
const agentSections = [];
|
|
858
867
|
agentSections.push(frontmatter(agentFm));
|
|
859
868
|
agentSections.push("");
|
|
@@ -879,9 +888,9 @@ function generateTopologySkill(ast) {
|
|
|
879
888
|
const fm = {};
|
|
880
889
|
fm.name = name;
|
|
881
890
|
if (ast.topology.description) {
|
|
882
|
-
fm.description =
|
|
891
|
+
fm.description = ast.topology.description;
|
|
883
892
|
}
|
|
884
|
-
fm.version =
|
|
893
|
+
fm.version = ast.topology.version;
|
|
885
894
|
fm.topology = name;
|
|
886
895
|
if (ast.topology.patterns.length > 0) {
|
|
887
896
|
fm.patterns = ast.topology.patterns;
|
|
@@ -905,7 +914,7 @@ function generateTopologySkill(ast) {
|
|
|
905
914
|
if (mainSkill.agent)
|
|
906
915
|
fm.agent = mainSkill.agent;
|
|
907
916
|
if (mainSkill.allowedTools && mainSkill.allowedTools.length > 0) {
|
|
908
|
-
fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName);
|
|
917
|
+
fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName).join(", ");
|
|
909
918
|
}
|
|
910
919
|
}
|
|
911
920
|
const sections = [];
|
|
@@ -1222,7 +1231,7 @@ function generateSkills(ast) {
|
|
|
1222
1231
|
const sfm = {};
|
|
1223
1232
|
sfm.name = skill.id;
|
|
1224
1233
|
if (skill.description)
|
|
1225
|
-
sfm.description =
|
|
1234
|
+
sfm.description = skill.description;
|
|
1226
1235
|
if (skill.disableModelInvocation != null) {
|
|
1227
1236
|
sfm["disable-model-invocation"] = skill.disableModelInvocation;
|
|
1228
1237
|
}
|
|
@@ -1234,7 +1243,7 @@ function generateSkills(ast) {
|
|
|
1234
1243
|
if (skill.agent)
|
|
1235
1244
|
sfm.agent = skill.agent;
|
|
1236
1245
|
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
1237
|
-
sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName);
|
|
1246
|
+
sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName).join(", ");
|
|
1238
1247
|
}
|
|
1239
1248
|
// Only emit frontmatter if there are fields beyond just the name
|
|
1240
1249
|
if (Object.keys(sfm).length > 1) {
|
|
@@ -1434,7 +1443,7 @@ function generateGateScripts(ast) {
|
|
|
1434
1443
|
}
|
|
1435
1444
|
return files;
|
|
1436
1445
|
}
|
|
1437
|
-
/** Generate gate wrapper scripts that
|
|
1446
|
+
/** Generate gate wrapper scripts that run validation when agents complete. */
|
|
1438
1447
|
function generateGateWrapperScripts(ast) {
|
|
1439
1448
|
const files = [];
|
|
1440
1449
|
const name = ast.topology.name;
|
|
@@ -1447,12 +1456,22 @@ function generateGateWrapperScripts(ast) {
|
|
|
1447
1456
|
if (gate.behavior === "advisory")
|
|
1448
1457
|
continue;
|
|
1449
1458
|
const gateScriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1450
|
-
const onFailExit = gate.onFail === "halt"
|
|
1459
|
+
const onFailExit = gate.onFail === "halt"
|
|
1460
|
+
? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
|
|
1461
|
+
: `echo "Gate '${gate.id}' failed — bounce back and fix the issues." >&2\n exit 2`;
|
|
1462
|
+
const wiredAsHook = isRegisteredSubagentType(ast, gate.after);
|
|
1463
|
+
const enforcementNote = wiredAsHook
|
|
1464
|
+
? `# Enforcement: SubagentStop hook on agent "${gate.after}". Exit 2 blocks the subagent`
|
|
1465
|
+
: `# Enforcement: NOT wired as a SubagentStop hook (after: ${gate.after || "<unset>"} is not an agent).`;
|
|
1466
|
+
const enforcementNote2 = wiredAsHook
|
|
1467
|
+
? `# from stopping (Claude Code prevents stop and surfaces stderr to Claude).`
|
|
1468
|
+
: `# Invoke this script from your orchestrator playbook or a /command at the right step.`;
|
|
1451
1469
|
const contentLines = [
|
|
1452
1470
|
"#!/usr/bin/env bash",
|
|
1453
1471
|
`# Gate wrapper: ${gate.id}`,
|
|
1454
|
-
`# Enforces gate "${gate.id}" on Task tool calls.`,
|
|
1455
1472
|
`# Auto-generated by agentopology scaffold — edit as needed.`,
|
|
1473
|
+
enforcementNote,
|
|
1474
|
+
enforcementNote2,
|
|
1456
1475
|
"set -euo pipefail",
|
|
1457
1476
|
"",
|
|
1458
1477
|
"# Gate runs after: " + (gate.after || "any"),
|
|
@@ -1508,27 +1527,66 @@ function generateGateWrapperScripts(ast) {
|
|
|
1508
1527
|
}
|
|
1509
1528
|
return files;
|
|
1510
1529
|
}
|
|
1511
|
-
/**
|
|
1530
|
+
/**
|
|
1531
|
+
* Generate an observability-aware hook stub (Changes 4+5).
|
|
1532
|
+
*
|
|
1533
|
+
* For SubagentStop/Stop hooks: extract agent_type from the Claude Code stdin
|
|
1534
|
+
* JSON payload and export as AGENT_TYPE, then append a metrics.jsonl entry.
|
|
1535
|
+
* For other events: emit a simple TODO stub.
|
|
1536
|
+
*/
|
|
1537
|
+
function observabilityStub(hookName, eventName, matcherHint) {
|
|
1538
|
+
const isFinishEvent = eventName === "SubagentStop" || eventName === "Stop";
|
|
1539
|
+
if (isFinishEvent) {
|
|
1540
|
+
return [
|
|
1541
|
+
"#!/usr/bin/env bash",
|
|
1542
|
+
`# Auto-generated by agentopology scaffold on ${new Date().toISOString().slice(0, 10)}`,
|
|
1543
|
+
`# Hook: ${hookName} · Event: ${eventName} · Matcher: ${matcherHint || "*"}`,
|
|
1544
|
+
"# Replace this stub with your implementation.",
|
|
1545
|
+
"# Stdin JSON payload from Claude Code is available on FD 0.",
|
|
1546
|
+
"set -euo pipefail",
|
|
1547
|
+
"",
|
|
1548
|
+
"PAYLOAD=$(cat)",
|
|
1549
|
+
'AGENT_TYPE=$(echo "$PAYLOAD" | jq -r \'.agent_type // "unknown"\')',
|
|
1550
|
+
'SESSION_ID=$(echo "$PAYLOAD" | jq -r \'.session_id // ""\')',
|
|
1551
|
+
"",
|
|
1552
|
+
`echo "{\\\"ts\\\":\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\",\\\"event\\\":\\\"${eventName}\\\",\\\"agent\\\":\\\"$AGENT_TYPE\\\",\\\"session\\\":\\\"$SESSION_ID\\\",\\\"hook\\\":\\\"${hookName}\\\"}" >> .claude/memory/metrics.jsonl`,
|
|
1553
|
+
"",
|
|
1554
|
+
].join("\n");
|
|
1555
|
+
}
|
|
1556
|
+
return [
|
|
1557
|
+
"#!/usr/bin/env bash",
|
|
1558
|
+
`# Auto-generated by agentopology scaffold on ${new Date().toISOString().slice(0, 10)}`,
|
|
1559
|
+
`# Hook: ${hookName} · Event: ${eventName} · Matcher: ${matcherHint || "*"}`,
|
|
1560
|
+
"# Replace this stub with your implementation.",
|
|
1561
|
+
"# Stdin JSON payload from Claude Code is available on FD 0.",
|
|
1562
|
+
"set -euo pipefail",
|
|
1563
|
+
"",
|
|
1564
|
+
'PAYLOAD=$(cat)',
|
|
1565
|
+
'AGENT_TYPE=$(echo "$PAYLOAD" | jq -r \'.agent_type // "unknown"\')',
|
|
1566
|
+
"",
|
|
1567
|
+
`echo "TODO: implement ${hookName} hook (event: ${eventName}, agent: $AGENT_TYPE)"`,
|
|
1568
|
+
"",
|
|
1569
|
+
].join("\n");
|
|
1570
|
+
}
|
|
1571
|
+
/** Generate hook script stubs for every hook's `run` field (Changes 1, 4, 5). */
|
|
1512
1572
|
function generateHookScripts(ast) {
|
|
1513
1573
|
const files = [];
|
|
1514
1574
|
const name = ast.topology.name;
|
|
1515
1575
|
for (const hook of ast.hooks) {
|
|
1516
1576
|
if (!hook.run)
|
|
1517
1577
|
continue;
|
|
1518
|
-
|
|
1519
|
-
|
|
1578
|
+
const runPath = hook.run;
|
|
1579
|
+
// If the run path starts with '.' or '/' it is already project-relative —
|
|
1580
|
+
// generate the stub at exactly that path (observability pattern).
|
|
1581
|
+
// Otherwise scope the stub into this topology's skill scripts directory.
|
|
1582
|
+
const filePath = runPath.startsWith(".") || runPath.startsWith("/")
|
|
1583
|
+
? runPath
|
|
1584
|
+
: `.claude/skills/${name}/scripts/${runPath.replace(/^.*\//, "").replace(/\s.*$/, "")}`;
|
|
1520
1585
|
files.push({
|
|
1521
|
-
path:
|
|
1522
|
-
content:
|
|
1523
|
-
"#!/usr/bin/env bash",
|
|
1524
|
-
`# Hook: ${hook.name} — fires on ${hook.on}`,
|
|
1525
|
-
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1526
|
-
'set -euo pipefail',
|
|
1527
|
-
"",
|
|
1528
|
-
`echo "TODO: implement ${hook.name} hook"`,
|
|
1529
|
-
"",
|
|
1530
|
-
].join("\n"),
|
|
1586
|
+
path: filePath,
|
|
1587
|
+
content: observabilityStub(hook.name, hook.on, hook.matcher ?? ""),
|
|
1531
1588
|
category: "script",
|
|
1589
|
+
executable: true,
|
|
1532
1590
|
});
|
|
1533
1591
|
}
|
|
1534
1592
|
return files;
|
|
@@ -1580,9 +1638,16 @@ function generateSettings(ast) {
|
|
|
1580
1638
|
if (!hooksByEvent[eventName]) {
|
|
1581
1639
|
hooksByEvent[eventName] = [];
|
|
1582
1640
|
}
|
|
1583
|
-
// Build the script command
|
|
1584
|
-
|
|
1585
|
-
|
|
1641
|
+
// Build the script command.
|
|
1642
|
+
// If hook.run starts with '.' or '/' it is already a project-relative or
|
|
1643
|
+
// absolute path — use it verbatim so observability hooks like
|
|
1644
|
+
// ".claude/scripts/log-subagent.sh" land at the correct location.
|
|
1645
|
+
// Otherwise treat it as a bare filename scoped to this topology's skill
|
|
1646
|
+
// scripts directory.
|
|
1647
|
+
const runPath = hook.run;
|
|
1648
|
+
const command = runPath.startsWith(".") || runPath.startsWith("/")
|
|
1649
|
+
? `bash ${runPath}`
|
|
1650
|
+
: `bash .claude/skills/${topologyName}/scripts/${runPath.replace(/^.*\//, "").replace(/\s.*$/, "")}`;
|
|
1586
1651
|
const hookEntry = {
|
|
1587
1652
|
hooks: [
|
|
1588
1653
|
{
|
|
@@ -1601,26 +1666,53 @@ function generateSettings(ast) {
|
|
|
1601
1666
|
// Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
|
|
1602
1667
|
// This is the Claude Code native approach: hooks in the agent file are scoped
|
|
1603
1668
|
// to that agent automatically.
|
|
1604
|
-
// Gate enforcement — compile gates with run: to
|
|
1669
|
+
// Gate enforcement — compile gates with run: to SubagentStop hooks.
|
|
1670
|
+
//
|
|
1671
|
+
// SubagentStop fires when a subagent finishes. Its `matcher` field is matched
|
|
1672
|
+
// against `agent_type` (the registered subagent name from .claude/agents/),
|
|
1673
|
+
// NOT against arbitrary node ids. So a SubagentStop hook only fires when its
|
|
1674
|
+
// matcher names a real agent node — human, action, and orchestrator nodes are
|
|
1675
|
+
// NOT registered as subagent_types and their matchers would silently never
|
|
1676
|
+
// fire.
|
|
1677
|
+
//
|
|
1678
|
+
// SubagentStop with exit code 2 (or `decision: "block"`) DOES block: per the
|
|
1679
|
+
// Claude Code hooks reference, exit 2 "prevents subagent from stopping" and
|
|
1680
|
+
// surfaces stderr to Claude. That is the strongest enforcement available for
|
|
1681
|
+
// a gate that runs after an agent completes.
|
|
1682
|
+
//
|
|
1683
|
+
// For gates whose `after` target is not an agent (e.g. a human checkpoint, an
|
|
1684
|
+
// action, or "any"), no SubagentStop hook fires reliably. We skip emitting
|
|
1685
|
+
// dead config and warn the user — the topology's orchestrator (or a slash
|
|
1686
|
+
// command playbook) must invoke the gate script at the right step.
|
|
1605
1687
|
const gates = ast.nodes.filter((n) => n.type === "gate");
|
|
1606
1688
|
for (const gate of gates) {
|
|
1607
1689
|
if (!gate.run)
|
|
1608
1690
|
continue;
|
|
1609
1691
|
if (gate.behavior === "advisory")
|
|
1610
1692
|
continue; // advisory gates stay in markdown only
|
|
1611
|
-
|
|
1693
|
+
// SubagentStop matcher must be a registered subagent_type. If the gate
|
|
1694
|
+
// doesn't target an agent node, we cannot wire it as a hook — warn and
|
|
1695
|
+
// skip so we don't ship dead config that never fires.
|
|
1696
|
+
if (!isRegisteredSubagentType(ast, gate.after)) {
|
|
1697
|
+
const targetDesc = gate.after ? `non-agent target "${gate.after}"` : "no after: target";
|
|
1698
|
+
console.warn(`[claude-code] Gate "${gate.id}" has ${targetDesc} — SubagentStop hook not emitted ` +
|
|
1699
|
+
`(matcher must be an agent node id). The gate wrapper script is still generated; ` +
|
|
1700
|
+
`invoke it from your orchestrator playbook or a slash command at the right step.`);
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
const eventName = "SubagentStop";
|
|
1612
1704
|
if (!hooksByEvent[eventName])
|
|
1613
1705
|
hooksByEvent[eventName] = [];
|
|
1614
1706
|
const scriptName = `gate-${gate.id}.sh`;
|
|
1615
1707
|
const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
|
|
1616
1708
|
const hookEntry = {
|
|
1709
|
+
matcher: gate.after,
|
|
1617
1710
|
hooks: [
|
|
1618
1711
|
{
|
|
1619
1712
|
type: "command",
|
|
1620
1713
|
command,
|
|
1621
1714
|
},
|
|
1622
1715
|
],
|
|
1623
|
-
matcher: "Task", // Gates fire on Task tool (agent spawning)
|
|
1624
1716
|
};
|
|
1625
1717
|
hooksByEvent[eventName].push(hookEntry);
|
|
1626
1718
|
}
|
|
@@ -1633,66 +1725,21 @@ function generateSettings(ast) {
|
|
|
1633
1725
|
if (envSettings) {
|
|
1634
1726
|
settings.env = envSettings;
|
|
1635
1727
|
}
|
|
1636
|
-
// Permissions section from settings block
|
|
1728
|
+
// Permissions section from settings block — nested under "permissions" key
|
|
1637
1729
|
const perms = ast.settings;
|
|
1638
1730
|
if (perms) {
|
|
1639
1731
|
const allow = perms.allow;
|
|
1640
1732
|
const deny = perms.deny;
|
|
1641
|
-
const
|
|
1733
|
+
const permObj = {};
|
|
1642
1734
|
if (allow && allow.length > 0)
|
|
1643
|
-
|
|
1735
|
+
permObj.allow = allow;
|
|
1644
1736
|
if (deny && deny.length > 0)
|
|
1645
|
-
|
|
1646
|
-
if (
|
|
1647
|
-
settings.
|
|
1648
|
-
}
|
|
1649
|
-
// Defaults documentation
|
|
1650
|
-
if (ast.defaults) {
|
|
1651
|
-
const d = ast.defaults;
|
|
1652
|
-
const defaultsDoc = {};
|
|
1653
|
-
if (d.temperature != null)
|
|
1654
|
-
defaultsDoc.temperature = d.temperature;
|
|
1655
|
-
if (d.maxTokens != null)
|
|
1656
|
-
defaultsDoc.maxTokens = d.maxTokens;
|
|
1657
|
-
if (d.topP != null)
|
|
1658
|
-
defaultsDoc.topP = d.topP;
|
|
1659
|
-
if (d.topK != null)
|
|
1660
|
-
defaultsDoc.topK = d.topK;
|
|
1661
|
-
if (d.stop)
|
|
1662
|
-
defaultsDoc.stop = d.stop;
|
|
1663
|
-
if (d.seed != null)
|
|
1664
|
-
defaultsDoc.seed = d.seed;
|
|
1665
|
-
if (d.thinking)
|
|
1666
|
-
defaultsDoc.thinking = d.thinking;
|
|
1667
|
-
if (d.thinkingBudget != null)
|
|
1668
|
-
defaultsDoc.thinkingBudget = d.thinkingBudget;
|
|
1669
|
-
if (d.outputFormat)
|
|
1670
|
-
defaultsDoc.outputFormat = d.outputFormat;
|
|
1671
|
-
if (d.timeout)
|
|
1672
|
-
defaultsDoc.timeout = d.timeout;
|
|
1673
|
-
if (d.logLevel)
|
|
1674
|
-
defaultsDoc.logLevel = d.logLevel;
|
|
1675
|
-
if (Object.keys(defaultsDoc).length > 0) {
|
|
1676
|
-
settings._defaults = defaultsDoc;
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
// Observability documentation
|
|
1680
|
-
if (ast.observability) {
|
|
1681
|
-
const o = ast.observability;
|
|
1682
|
-
const obsDoc = {
|
|
1683
|
-
enabled: o.enabled,
|
|
1684
|
-
level: o.level,
|
|
1685
|
-
exporter: o.exporter,
|
|
1686
|
-
sampleRate: o.sampleRate,
|
|
1687
|
-
};
|
|
1688
|
-
if (o.endpoint)
|
|
1689
|
-
obsDoc.endpoint = o.endpoint;
|
|
1690
|
-
if (o.service)
|
|
1691
|
-
obsDoc.service = o.service;
|
|
1692
|
-
obsDoc.capture = o.capture;
|
|
1693
|
-
obsDoc.spans = o.spans;
|
|
1694
|
-
settings._observability = obsDoc;
|
|
1737
|
+
permObj.deny = deny;
|
|
1738
|
+
if (Object.keys(permObj).length > 0)
|
|
1739
|
+
settings.permissions = permObj;
|
|
1695
1740
|
}
|
|
1741
|
+
// Defaults and observability are documented in the CLAUDE.md context file,
|
|
1742
|
+
// not in settings.json (Claude Code doesn't recognize these keys).
|
|
1696
1743
|
// Top-level extensions for claude-code
|
|
1697
1744
|
if (ast.extensions?.["claude-code"]) {
|
|
1698
1745
|
for (const [k, v] of Object.entries(ast.extensions["claude-code"])) {
|
|
@@ -1955,21 +2002,23 @@ function generateCommandFiles(ast) {
|
|
|
1955
2002
|
// Frontmatter
|
|
1956
2003
|
const fm = {};
|
|
1957
2004
|
if (ast.topology.description) {
|
|
1958
|
-
fm.description =
|
|
2005
|
+
fm.description = ast.topology.description;
|
|
2006
|
+
}
|
|
2007
|
+
if (trigger.argument) {
|
|
2008
|
+
fm["argument-hint"] = `[${trigger.argument}]`;
|
|
1959
2009
|
}
|
|
1960
2010
|
sections.push(frontmatter(fm));
|
|
1961
2011
|
sections.push("");
|
|
1962
|
-
// Header
|
|
2012
|
+
// Header — use $ARGUMENTS for Claude Code command argument substitution
|
|
1963
2013
|
sections.push(`# /${trigger.name}`);
|
|
1964
2014
|
sections.push("");
|
|
1965
|
-
sections.push(trigger.pattern);
|
|
1966
|
-
sections.push("");
|
|
1967
|
-
// Arguments
|
|
1968
2015
|
if (trigger.argument) {
|
|
1969
|
-
sections.push(
|
|
1970
|
-
sections.push(`- ${trigger.argument}: extracted from the command pattern`);
|
|
1971
|
-
sections.push("");
|
|
2016
|
+
sections.push(`/${trigger.name} $ARGUMENTS`);
|
|
1972
2017
|
}
|
|
2018
|
+
else {
|
|
2019
|
+
sections.push(trigger.pattern);
|
|
2020
|
+
}
|
|
2021
|
+
sections.push("");
|
|
1973
2022
|
// Pipeline (from edges, enhanced)
|
|
1974
2023
|
if (ast.edges.length > 0) {
|
|
1975
2024
|
sections.push("## Pipeline");
|
|
@@ -2076,7 +2125,7 @@ function generateWorkspaceProtocol(ast) {
|
|
|
2076
2125
|
* destroy their content.
|
|
2077
2126
|
*/
|
|
2078
2127
|
function generateContextFile(ast) {
|
|
2079
|
-
const DEFAULT_PATH = ".claude/
|
|
2128
|
+
const DEFAULT_PATH = ".claude/CLAUDE.md";
|
|
2080
2129
|
let fileName = ast.context.file ?? DEFAULT_PATH;
|
|
2081
2130
|
// Safety: never overwrite the user's root-level CLAUDE.md
|
|
2082
2131
|
if (fileName === "CLAUDE.md" || fileName === "./CLAUDE.md") {
|