agentopology 0.2.1 → 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/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 +277 -0
- 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 +113 -37
- 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/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/package.json +1 -1
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -76,17 +77,18 @@ function frontmatter(fields) {
|
|
|
76
77
|
lines.push("---");
|
|
77
78
|
return lines.join("\n");
|
|
78
79
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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);
|
|
90
92
|
}
|
|
91
93
|
/** Generate an empty directory marker (a .gitkeep file). */
|
|
92
94
|
function gitkeep(dirPath) {
|
|
@@ -1457,11 +1459,19 @@ function generateGateWrapperScripts(ast) {
|
|
|
1457
1459
|
const onFailExit = gate.onFail === "halt"
|
|
1458
1460
|
? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
|
|
1459
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.`;
|
|
1460
1469
|
const contentLines = [
|
|
1461
1470
|
"#!/usr/bin/env bash",
|
|
1462
1471
|
`# Gate wrapper: ${gate.id}`,
|
|
1463
|
-
`# Enforces gate "${gate.id}" on SubagentStop (after: ${gate.after || "any"}).`,
|
|
1464
1472
|
`# Auto-generated by agentopology scaffold — edit as needed.`,
|
|
1473
|
+
enforcementNote,
|
|
1474
|
+
enforcementNote2,
|
|
1465
1475
|
"set -euo pipefail",
|
|
1466
1476
|
"",
|
|
1467
1477
|
"# Gate runs after: " + (gate.after || "any"),
|
|
@@ -1517,27 +1527,66 @@ function generateGateWrapperScripts(ast) {
|
|
|
1517
1527
|
}
|
|
1518
1528
|
return files;
|
|
1519
1529
|
}
|
|
1520
|
-
/**
|
|
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). */
|
|
1521
1572
|
function generateHookScripts(ast) {
|
|
1522
1573
|
const files = [];
|
|
1523
1574
|
const name = ast.topology.name;
|
|
1524
1575
|
for (const hook of ast.hooks) {
|
|
1525
1576
|
if (!hook.run)
|
|
1526
1577
|
continue;
|
|
1527
|
-
|
|
1528
|
-
|
|
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.*$/, "")}`;
|
|
1529
1585
|
files.push({
|
|
1530
|
-
path:
|
|
1531
|
-
content:
|
|
1532
|
-
"#!/usr/bin/env bash",
|
|
1533
|
-
`# Hook: ${hook.name} — fires on ${hook.on}`,
|
|
1534
|
-
"# Auto-generated by agentopology scaffold — edit as needed.",
|
|
1535
|
-
'set -euo pipefail',
|
|
1536
|
-
"",
|
|
1537
|
-
`echo "TODO: implement ${hook.name} hook"`,
|
|
1538
|
-
"",
|
|
1539
|
-
].join("\n"),
|
|
1586
|
+
path: filePath,
|
|
1587
|
+
content: observabilityStub(hook.name, hook.on, hook.matcher ?? ""),
|
|
1540
1588
|
category: "script",
|
|
1589
|
+
executable: true,
|
|
1541
1590
|
});
|
|
1542
1591
|
}
|
|
1543
1592
|
return files;
|
|
@@ -1589,9 +1638,16 @@ function generateSettings(ast) {
|
|
|
1589
1638
|
if (!hooksByEvent[eventName]) {
|
|
1590
1639
|
hooksByEvent[eventName] = [];
|
|
1591
1640
|
}
|
|
1592
|
-
// Build the script command
|
|
1593
|
-
|
|
1594
|
-
|
|
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.*$/, "")}`;
|
|
1595
1651
|
const hookEntry = {
|
|
1596
1652
|
hooks: [
|
|
1597
1653
|
{
|
|
@@ -1610,22 +1666,47 @@ function generateSettings(ast) {
|
|
|
1610
1666
|
// Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
|
|
1611
1667
|
// This is the Claude Code native approach: hooks in the agent file are scoped
|
|
1612
1668
|
// to that agent automatically.
|
|
1613
|
-
// Gate enforcement — compile gates with run: to SubagentStop hooks
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
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.
|
|
1617
1687
|
const gates = ast.nodes.filter((n) => n.type === "gate");
|
|
1618
1688
|
for (const gate of gates) {
|
|
1619
1689
|
if (!gate.run)
|
|
1620
1690
|
continue;
|
|
1621
1691
|
if (gate.behavior === "advisory")
|
|
1622
1692
|
continue; // advisory gates stay in markdown only
|
|
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
|
+
}
|
|
1623
1703
|
const eventName = "SubagentStop";
|
|
1624
1704
|
if (!hooksByEvent[eventName])
|
|
1625
1705
|
hooksByEvent[eventName] = [];
|
|
1626
1706
|
const scriptName = `gate-${gate.id}.sh`;
|
|
1627
1707
|
const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
|
|
1628
1708
|
const hookEntry = {
|
|
1709
|
+
matcher: gate.after,
|
|
1629
1710
|
hooks: [
|
|
1630
1711
|
{
|
|
1631
1712
|
type: "command",
|
|
@@ -1633,11 +1714,6 @@ function generateSettings(ast) {
|
|
|
1633
1714
|
},
|
|
1634
1715
|
],
|
|
1635
1716
|
};
|
|
1636
|
-
// Use the gate's after: agent as matcher so the hook only fires
|
|
1637
|
-
// when that specific agent completes. Without after:, match all agents.
|
|
1638
|
-
if (gate.after) {
|
|
1639
|
-
hookEntry.matcher = gate.after;
|
|
1640
|
-
}
|
|
1641
1717
|
hooksByEvent[eventName].push(hookEntry);
|
|
1642
1718
|
}
|
|
1643
1719
|
// Write hooksByEvent to settings if any hooks or gates were registered
|