agentopology 0.2.1 → 0.2.4
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 +423 -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 +188 -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 +19 -5
- package/dist/docs/content.js.map +1 -1
- package/dist/parser/__tests__/parser.test.js +88 -3
- package/dist/parser/__tests__/parser.test.js.map +1 -1
- package/dist/parser/ast.d.ts +15 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +12 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +65 -9
- 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
- package/spec/grammar.md +9 -0
|
@@ -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;AAs8E/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,30 @@ 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);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the topology's orchestration delegation mode.
|
|
95
|
+
*
|
|
96
|
+
* Returns `"inline"` if the orchestrator block declares `delegation: inline`.
|
|
97
|
+
* Otherwise returns `"subagent"` (the default). Inline-orchestrator topologies
|
|
98
|
+
* drive every agent in the main session's context; no Task-tool subagent is
|
|
99
|
+
* ever spawned, so any `SubagentStop` hook would silently never fire.
|
|
100
|
+
*/
|
|
101
|
+
function getDelegationMode(ast) {
|
|
102
|
+
const orch = ast.nodes.find((n) => n.type === "orchestrator");
|
|
103
|
+
return orch?.delegation === "inline" ? "inline" : "subagent";
|
|
90
104
|
}
|
|
91
105
|
/** Generate an empty directory marker (a .gitkeep file). */
|
|
92
106
|
function gitkeep(dirPath) {
|
|
@@ -1457,11 +1471,28 @@ function generateGateWrapperScripts(ast) {
|
|
|
1457
1471
|
const onFailExit = gate.onFail === "halt"
|
|
1458
1472
|
? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
|
|
1459
1473
|
: `echo "Gate '${gate.id}' failed — bounce back and fix the issues." >&2\n exit 2`;
|
|
1474
|
+
const inlineMode = getDelegationMode(ast) === "inline";
|
|
1475
|
+
const wiredAsHook = !inlineMode && isRegisteredSubagentType(ast, gate.after);
|
|
1476
|
+
let enforcementNote;
|
|
1477
|
+
let enforcementNote2;
|
|
1478
|
+
if (inlineMode) {
|
|
1479
|
+
enforcementNote = `# Enforcement: NOT wired as a SubagentStop hook (orchestrator.delegation is "inline").`;
|
|
1480
|
+
enforcementNote2 = `# Invoke this script from your /${ast.topology.name} playbook at the right step.`;
|
|
1481
|
+
}
|
|
1482
|
+
else if (wiredAsHook) {
|
|
1483
|
+
enforcementNote = `# Enforcement: SubagentStop hook on agent "${gate.after}". Exit 2 blocks the subagent`;
|
|
1484
|
+
enforcementNote2 = `# from stopping (Claude Code prevents stop and surfaces stderr to Claude).`;
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
enforcementNote = `# Enforcement: NOT wired as a SubagentStop hook (after: ${gate.after || "<unset>"} is not an agent).`;
|
|
1488
|
+
enforcementNote2 = `# Invoke this script from your orchestrator playbook or a /command at the right step.`;
|
|
1489
|
+
}
|
|
1460
1490
|
const contentLines = [
|
|
1461
1491
|
"#!/usr/bin/env bash",
|
|
1462
1492
|
`# Gate wrapper: ${gate.id}`,
|
|
1463
|
-
`# Enforces gate "${gate.id}" on SubagentStop (after: ${gate.after || "any"}).`,
|
|
1464
1493
|
`# Auto-generated by agentopology scaffold — edit as needed.`,
|
|
1494
|
+
enforcementNote,
|
|
1495
|
+
enforcementNote2,
|
|
1465
1496
|
"set -euo pipefail",
|
|
1466
1497
|
"",
|
|
1467
1498
|
"# Gate runs after: " + (gate.after || "any"),
|
|
@@ -1517,27 +1548,66 @@ function generateGateWrapperScripts(ast) {
|
|
|
1517
1548
|
}
|
|
1518
1549
|
return files;
|
|
1519
1550
|
}
|
|
1520
|
-
/**
|
|
1551
|
+
/**
|
|
1552
|
+
* Generate an observability-aware hook stub (Changes 4+5).
|
|
1553
|
+
*
|
|
1554
|
+
* For SubagentStop/Stop hooks: extract agent_type from the Claude Code stdin
|
|
1555
|
+
* JSON payload and export as AGENT_TYPE, then append a metrics.jsonl entry.
|
|
1556
|
+
* For other events: emit a simple TODO stub.
|
|
1557
|
+
*/
|
|
1558
|
+
function observabilityStub(hookName, eventName, matcherHint) {
|
|
1559
|
+
const isFinishEvent = eventName === "SubagentStop" || eventName === "Stop";
|
|
1560
|
+
if (isFinishEvent) {
|
|
1561
|
+
return [
|
|
1562
|
+
"#!/usr/bin/env bash",
|
|
1563
|
+
`# Auto-generated by agentopology scaffold on ${new Date().toISOString().slice(0, 10)}`,
|
|
1564
|
+
`# Hook: ${hookName} · Event: ${eventName} · Matcher: ${matcherHint || "*"}`,
|
|
1565
|
+
"# Replace this stub with your implementation.",
|
|
1566
|
+
"# Stdin JSON payload from Claude Code is available on FD 0.",
|
|
1567
|
+
"set -euo pipefail",
|
|
1568
|
+
"",
|
|
1569
|
+
"PAYLOAD=$(cat)",
|
|
1570
|
+
'AGENT_TYPE=$(echo "$PAYLOAD" | jq -r \'.agent_type // "unknown"\')',
|
|
1571
|
+
'SESSION_ID=$(echo "$PAYLOAD" | jq -r \'.session_id // ""\')',
|
|
1572
|
+
"",
|
|
1573
|
+
`echo "{\\\"ts\\\":\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\",\\\"event\\\":\\\"${eventName}\\\",\\\"agent\\\":\\\"$AGENT_TYPE\\\",\\\"session\\\":\\\"$SESSION_ID\\\",\\\"hook\\\":\\\"${hookName}\\\"}" >> .claude/memory/metrics.jsonl`,
|
|
1574
|
+
"",
|
|
1575
|
+
].join("\n");
|
|
1576
|
+
}
|
|
1577
|
+
return [
|
|
1578
|
+
"#!/usr/bin/env bash",
|
|
1579
|
+
`# Auto-generated by agentopology scaffold on ${new Date().toISOString().slice(0, 10)}`,
|
|
1580
|
+
`# Hook: ${hookName} · Event: ${eventName} · Matcher: ${matcherHint || "*"}`,
|
|
1581
|
+
"# Replace this stub with your implementation.",
|
|
1582
|
+
"# Stdin JSON payload from Claude Code is available on FD 0.",
|
|
1583
|
+
"set -euo pipefail",
|
|
1584
|
+
"",
|
|
1585
|
+
'PAYLOAD=$(cat)',
|
|
1586
|
+
'AGENT_TYPE=$(echo "$PAYLOAD" | jq -r \'.agent_type // "unknown"\')',
|
|
1587
|
+
"",
|
|
1588
|
+
`echo "TODO: implement ${hookName} hook (event: ${eventName}, agent: $AGENT_TYPE)"`,
|
|
1589
|
+
"",
|
|
1590
|
+
].join("\n");
|
|
1591
|
+
}
|
|
1592
|
+
/** Generate hook script stubs for every hook's `run` field (Changes 1, 4, 5). */
|
|
1521
1593
|
function generateHookScripts(ast) {
|
|
1522
1594
|
const files = [];
|
|
1523
1595
|
const name = ast.topology.name;
|
|
1524
1596
|
for (const hook of ast.hooks) {
|
|
1525
1597
|
if (!hook.run)
|
|
1526
1598
|
continue;
|
|
1527
|
-
|
|
1528
|
-
|
|
1599
|
+
const runPath = hook.run;
|
|
1600
|
+
// If the run path starts with '.' or '/' it is already project-relative —
|
|
1601
|
+
// generate the stub at exactly that path (observability pattern).
|
|
1602
|
+
// Otherwise scope the stub into this topology's skill scripts directory.
|
|
1603
|
+
const filePath = runPath.startsWith(".") || runPath.startsWith("/")
|
|
1604
|
+
? runPath
|
|
1605
|
+
: `.claude/skills/${name}/scripts/${runPath.replace(/^.*\//, "").replace(/\s.*$/, "")}`;
|
|
1529
1606
|
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"),
|
|
1607
|
+
path: filePath,
|
|
1608
|
+
content: observabilityStub(hook.name, hook.on, hook.matcher ?? ""),
|
|
1540
1609
|
category: "script",
|
|
1610
|
+
executable: true,
|
|
1541
1611
|
});
|
|
1542
1612
|
}
|
|
1543
1613
|
return files;
|
|
@@ -1582,16 +1652,34 @@ function generateAgentHookScripts(ast) {
|
|
|
1582
1652
|
function generateSettings(ast) {
|
|
1583
1653
|
const settings = {};
|
|
1584
1654
|
const topologyName = ast.topology.name;
|
|
1655
|
+
const isInline = getDelegationMode(ast) === "inline";
|
|
1585
1656
|
// Hooks section — grouped by event name
|
|
1586
1657
|
const hooksByEvent = {};
|
|
1587
1658
|
for (const hook of ast.hooks) {
|
|
1588
1659
|
const eventName = hook.on;
|
|
1660
|
+
// Inline orchestrator mode never spawns subagents, so SubagentStop /
|
|
1661
|
+
// SubagentStart hooks would never fire. Warn the user and skip emission
|
|
1662
|
+
// — keeping them in settings.json would be a maintenance trap.
|
|
1663
|
+
if (isInline && (eventName === "SubagentStop" || eventName === "SubagentStart")) {
|
|
1664
|
+
console.warn(`[claude-code] Hook "${hook.name}" targets ${eventName} but ` +
|
|
1665
|
+
`orchestrator.delegation is "inline" — no subagent ever runs, so the hook would ` +
|
|
1666
|
+
`never fire. Skipped. Move the work into your orchestrator playbook or change ` +
|
|
1667
|
+
`delegation to "subagent".`);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1589
1670
|
if (!hooksByEvent[eventName]) {
|
|
1590
1671
|
hooksByEvent[eventName] = [];
|
|
1591
1672
|
}
|
|
1592
|
-
// Build the script command
|
|
1593
|
-
|
|
1594
|
-
|
|
1673
|
+
// Build the script command.
|
|
1674
|
+
// If hook.run starts with '.' or '/' it is already a project-relative or
|
|
1675
|
+
// absolute path — use it verbatim so observability hooks like
|
|
1676
|
+
// ".claude/scripts/log-subagent.sh" land at the correct location.
|
|
1677
|
+
// Otherwise treat it as a bare filename scoped to this topology's skill
|
|
1678
|
+
// scripts directory.
|
|
1679
|
+
const runPath = hook.run;
|
|
1680
|
+
const command = runPath.startsWith(".") || runPath.startsWith("/")
|
|
1681
|
+
? `bash ${runPath}`
|
|
1682
|
+
: `bash .claude/skills/${topologyName}/scripts/${runPath.replace(/^.*\//, "").replace(/\s.*$/, "")}`;
|
|
1595
1683
|
const hookEntry = {
|
|
1596
1684
|
hooks: [
|
|
1597
1685
|
{
|
|
@@ -1610,22 +1698,62 @@ function generateSettings(ast) {
|
|
|
1610
1698
|
// Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
|
|
1611
1699
|
// This is the Claude Code native approach: hooks in the agent file are scoped
|
|
1612
1700
|
// to that agent automatically.
|
|
1613
|
-
// Gate enforcement — compile gates with run: to SubagentStop hooks
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1701
|
+
// Gate enforcement — compile gates with run: to SubagentStop hooks.
|
|
1702
|
+
//
|
|
1703
|
+
// SubagentStop fires when a subagent finishes. Its `matcher` field is matched
|
|
1704
|
+
// against `agent_type` (the registered subagent name from .claude/agents/),
|
|
1705
|
+
// NOT against arbitrary node ids. So a SubagentStop hook only fires when its
|
|
1706
|
+
// matcher names a real agent node — human, action, and orchestrator nodes are
|
|
1707
|
+
// NOT registered as subagent_types and their matchers would silently never
|
|
1708
|
+
// fire.
|
|
1709
|
+
//
|
|
1710
|
+
// SubagentStop with exit code 2 (or `decision: "block"`) DOES block: per the
|
|
1711
|
+
// Claude Code hooks reference, exit 2 "prevents subagent from stopping" and
|
|
1712
|
+
// surfaces stderr to Claude. That is the strongest enforcement available for
|
|
1713
|
+
// a gate that runs after an agent completes.
|
|
1714
|
+
//
|
|
1715
|
+
// For gates whose `after` target is not an agent (e.g. a human checkpoint, an
|
|
1716
|
+
// action, or "any"), no SubagentStop hook fires reliably. We skip emitting
|
|
1717
|
+
// dead config and warn the user — the topology's orchestrator (or a slash
|
|
1718
|
+
// command playbook) must invoke the gate script at the right step.
|
|
1719
|
+
//
|
|
1720
|
+
// INLINE-ORCHESTRATOR MODE: when the topology declares
|
|
1721
|
+
// `orchestrator { delegation: inline }`, no subagent is ever spawned even
|
|
1722
|
+
// when the matcher names a real agent — the orchestrator drives every step
|
|
1723
|
+
// in its own session context. SubagentStop hooks would be dead config in
|
|
1724
|
+
// this mode, so we skip emitting them entirely (still generate the wrapper
|
|
1725
|
+
// script). The gate must be invoked from the playbook at the right step.
|
|
1726
|
+
const delegationMode = getDelegationMode(ast);
|
|
1617
1727
|
const gates = ast.nodes.filter((n) => n.type === "gate");
|
|
1618
1728
|
for (const gate of gates) {
|
|
1619
1729
|
if (!gate.run)
|
|
1620
1730
|
continue;
|
|
1621
1731
|
if (gate.behavior === "advisory")
|
|
1622
1732
|
continue; // advisory gates stay in markdown only
|
|
1733
|
+
if (delegationMode === "inline") {
|
|
1734
|
+
console.warn(`[claude-code] Gate "${gate.id}" — orchestrator.delegation is "inline", ` +
|
|
1735
|
+
`so no subagent ever runs. SubagentStop hook not emitted; invoke ` +
|
|
1736
|
+
`.claude/skills/${topologyName}/scripts/gate-${gate.id}.sh from your ` +
|
|
1737
|
+
`orchestrator playbook at the right step.`);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
// SubagentStop matcher must be a registered subagent_type. If the gate
|
|
1741
|
+
// doesn't target an agent node, we cannot wire it as a hook — warn and
|
|
1742
|
+
// skip so we don't ship dead config that never fires.
|
|
1743
|
+
if (!isRegisteredSubagentType(ast, gate.after)) {
|
|
1744
|
+
const targetDesc = gate.after ? `non-agent target "${gate.after}"` : "no after: target";
|
|
1745
|
+
console.warn(`[claude-code] Gate "${gate.id}" has ${targetDesc} — SubagentStop hook not emitted ` +
|
|
1746
|
+
`(matcher must be an agent node id). The gate wrapper script is still generated; ` +
|
|
1747
|
+
`invoke it from your orchestrator playbook or a slash command at the right step.`);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1623
1750
|
const eventName = "SubagentStop";
|
|
1624
1751
|
if (!hooksByEvent[eventName])
|
|
1625
1752
|
hooksByEvent[eventName] = [];
|
|
1626
1753
|
const scriptName = `gate-${gate.id}.sh`;
|
|
1627
1754
|
const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
|
|
1628
1755
|
const hookEntry = {
|
|
1756
|
+
matcher: gate.after,
|
|
1629
1757
|
hooks: [
|
|
1630
1758
|
{
|
|
1631
1759
|
type: "command",
|
|
@@ -1633,11 +1761,6 @@ function generateSettings(ast) {
|
|
|
1633
1761
|
},
|
|
1634
1762
|
],
|
|
1635
1763
|
};
|
|
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
1764
|
hooksByEvent[eventName].push(hookEntry);
|
|
1642
1765
|
}
|
|
1643
1766
|
// Write hooksByEvent to settings if any hooks or gates were registered
|
|
@@ -1986,6 +2109,34 @@ function generateCommandFiles(ast) {
|
|
|
1986
2109
|
}
|
|
1987
2110
|
sections.push("");
|
|
1988
2111
|
}
|
|
2112
|
+
// Inline-orchestrator topologies cannot wire gates via SubagentStop hooks
|
|
2113
|
+
// (no subagent is ever spawned). The orchestrator playbook must invoke
|
|
2114
|
+
// each non-advisory gate at the right step. We emit a copy-pasteable
|
|
2115
|
+
// cheat sheet so the user knows exactly which command to drop in and
|
|
2116
|
+
// where in the flow it belongs.
|
|
2117
|
+
if (getDelegationMode(ast) === "inline") {
|
|
2118
|
+
const blockingGates = ast.nodes.filter((n) => n.type === "gate")
|
|
2119
|
+
.filter((g) => g.run && g.behavior !== "advisory");
|
|
2120
|
+
if (blockingGates.length > 0) {
|
|
2121
|
+
sections.push("## Gates to invoke (inline-orchestrator)");
|
|
2122
|
+
sections.push("");
|
|
2123
|
+
sections.push("Because `orchestrator.delegation: inline` is declared, no `SubagentStop` " +
|
|
2124
|
+
"hooks fire. Invoke each gate from this playbook at the step indicated " +
|
|
2125
|
+
"by its `after:` field. Exit code 0 = pass, non-zero = fail.");
|
|
2126
|
+
sections.push("");
|
|
2127
|
+
for (const gate of blockingGates) {
|
|
2128
|
+
const after = gate.after ?? "(any)";
|
|
2129
|
+
const before = gate.before ?? "(any)";
|
|
2130
|
+
const onFail = gate.onFail ?? "halt";
|
|
2131
|
+
sections.push(`### gate \`${gate.id}\` — after \`${after}\`, before \`${before}\`, on-fail \`${onFail}\``);
|
|
2132
|
+
sections.push("");
|
|
2133
|
+
sections.push("```bash");
|
|
2134
|
+
sections.push(`bash .claude/skills/${ast.topology.name}/scripts/gate-${gate.id}.sh`);
|
|
2135
|
+
sections.push("```");
|
|
2136
|
+
sections.push("");
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
1989
2140
|
files.push({
|
|
1990
2141
|
path: `.claude/commands/${trigger.name}.md`,
|
|
1991
2142
|
content: sections.join("\n") + "\n",
|