agentopology 0.2.0 → 0.2.1
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/bindings/__tests__/bindings.test.js +11 -11
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +56 -83
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/import/__tests__/import.test.js +2 -2
- package/dist/import/__tests__/import.test.js.map +1 -1
- package/examples/simple-pipeline-topology.html +5700 -0
- package/package.json +2 -1
- package/skill/SKILL.md +7 -7
|
@@ -48,6 +48,8 @@ function toTitle(id) {
|
|
|
48
48
|
* - item1
|
|
49
49
|
* - item2
|
|
50
50
|
*/
|
|
51
|
+
/** Characters that require quoting in YAML scalar values. */
|
|
52
|
+
const YAML_SPECIAL = /[:#\[\]{}&*!|>'"%@`]/;
|
|
51
53
|
function frontmatter(fields) {
|
|
52
54
|
const lines = ["---"];
|
|
53
55
|
for (const [key, value] of Object.entries(fields)) {
|
|
@@ -65,7 +67,10 @@ function frontmatter(fields) {
|
|
|
65
67
|
lines.push(`${key}: ${value}`);
|
|
66
68
|
}
|
|
67
69
|
else {
|
|
68
|
-
|
|
70
|
+
// Auto-quote strings containing YAML-special characters
|
|
71
|
+
const str = String(value);
|
|
72
|
+
const needsQuotes = YAML_SPECIAL.test(str) || str.trim() !== str;
|
|
73
|
+
lines.push(`${key}: ${needsQuotes ? `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : str}`);
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
lines.push("---");
|
|
@@ -93,10 +98,10 @@ function gitkeep(dirPath) {
|
|
|
93
98
|
*/
|
|
94
99
|
function mapPermissionMode(perm) {
|
|
95
100
|
// Claude Code native values — pass through
|
|
96
|
-
const nativeValues = ["plan", "
|
|
101
|
+
const nativeValues = ["plan", "default", "acceptEdits", "dontAsk", "bypassPermissions"];
|
|
97
102
|
if (nativeValues.includes(perm)) {
|
|
98
|
-
// "
|
|
99
|
-
if (perm === "
|
|
103
|
+
// "default" is the default, omit it
|
|
104
|
+
if (perm === "default")
|
|
100
105
|
return undefined;
|
|
101
106
|
return perm;
|
|
102
107
|
}
|
|
@@ -106,8 +111,10 @@ function mapPermissionMode(perm) {
|
|
|
106
111
|
return "plan";
|
|
107
112
|
case "autonomous":
|
|
108
113
|
return undefined; // default, omit
|
|
114
|
+
case "auto":
|
|
115
|
+
return undefined; // default, omit
|
|
109
116
|
case "interactive":
|
|
110
|
-
return "
|
|
117
|
+
return "acceptEdits";
|
|
111
118
|
case "unrestricted":
|
|
112
119
|
return "bypassPermissions";
|
|
113
120
|
default:
|
|
@@ -229,7 +236,7 @@ function generateAgents(ast) {
|
|
|
229
236
|
if (agent.invocation !== "manual") {
|
|
230
237
|
const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
|
|
231
238
|
if (desc) {
|
|
232
|
-
fm.description =
|
|
239
|
+
fm.description = desc;
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
242
|
if (agent.model)
|
|
@@ -237,9 +244,9 @@ function generateAgents(ast) {
|
|
|
237
244
|
if (agent.maxTurns != null)
|
|
238
245
|
fm.maxTurns = String(agent.maxTurns);
|
|
239
246
|
if (agent.tools && agent.tools.length > 0)
|
|
240
|
-
fm.tools = agent.tools.map(convertMcpToolName);
|
|
247
|
+
fm.tools = agent.tools.map(convertMcpToolName).join(", ");
|
|
241
248
|
if (agent.disallowedTools && agent.disallowedTools.length > 0)
|
|
242
|
-
fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName);
|
|
249
|
+
fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName).join(", ");
|
|
243
250
|
if (agent.mcpServers && agent.mcpServers.length > 0)
|
|
244
251
|
fm.mcpServers = agent.mcpServers;
|
|
245
252
|
if (agent.background === true)
|
|
@@ -836,7 +843,7 @@ function generateGroupNodes(ast) {
|
|
|
836
843
|
const skillFm = {};
|
|
837
844
|
skillFm.name = group.id;
|
|
838
845
|
if (group.description)
|
|
839
|
-
skillFm.description =
|
|
846
|
+
skillFm.description = group.description;
|
|
840
847
|
skillFm["user-invocable"] = true;
|
|
841
848
|
const skillSections = [];
|
|
842
849
|
skillSections.push(frontmatter(skillFm));
|
|
@@ -853,7 +860,7 @@ function generateGroupNodes(ast) {
|
|
|
853
860
|
const agentFm = {};
|
|
854
861
|
agentFm.name = group.id;
|
|
855
862
|
if (group.description)
|
|
856
|
-
agentFm.description =
|
|
863
|
+
agentFm.description = group.description;
|
|
857
864
|
const agentSections = [];
|
|
858
865
|
agentSections.push(frontmatter(agentFm));
|
|
859
866
|
agentSections.push("");
|
|
@@ -879,9 +886,9 @@ function generateTopologySkill(ast) {
|
|
|
879
886
|
const fm = {};
|
|
880
887
|
fm.name = name;
|
|
881
888
|
if (ast.topology.description) {
|
|
882
|
-
fm.description =
|
|
889
|
+
fm.description = ast.topology.description;
|
|
883
890
|
}
|
|
884
|
-
fm.version =
|
|
891
|
+
fm.version = ast.topology.version;
|
|
885
892
|
fm.topology = name;
|
|
886
893
|
if (ast.topology.patterns.length > 0) {
|
|
887
894
|
fm.patterns = ast.topology.patterns;
|
|
@@ -905,7 +912,7 @@ function generateTopologySkill(ast) {
|
|
|
905
912
|
if (mainSkill.agent)
|
|
906
913
|
fm.agent = mainSkill.agent;
|
|
907
914
|
if (mainSkill.allowedTools && mainSkill.allowedTools.length > 0) {
|
|
908
|
-
fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName);
|
|
915
|
+
fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName).join(", ");
|
|
909
916
|
}
|
|
910
917
|
}
|
|
911
918
|
const sections = [];
|
|
@@ -1222,7 +1229,7 @@ function generateSkills(ast) {
|
|
|
1222
1229
|
const sfm = {};
|
|
1223
1230
|
sfm.name = skill.id;
|
|
1224
1231
|
if (skill.description)
|
|
1225
|
-
sfm.description =
|
|
1232
|
+
sfm.description = skill.description;
|
|
1226
1233
|
if (skill.disableModelInvocation != null) {
|
|
1227
1234
|
sfm["disable-model-invocation"] = skill.disableModelInvocation;
|
|
1228
1235
|
}
|
|
@@ -1234,7 +1241,7 @@ function generateSkills(ast) {
|
|
|
1234
1241
|
if (skill.agent)
|
|
1235
1242
|
sfm.agent = skill.agent;
|
|
1236
1243
|
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
1237
|
-
sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName);
|
|
1244
|
+
sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName).join(", ");
|
|
1238
1245
|
}
|
|
1239
1246
|
// Only emit frontmatter if there are fields beyond just the name
|
|
1240
1247
|
if (Object.keys(sfm).length > 1) {
|
|
@@ -1434,7 +1441,7 @@ function generateGateScripts(ast) {
|
|
|
1434
1441
|
}
|
|
1435
1442
|
return files;
|
|
1436
1443
|
}
|
|
1437
|
-
/** Generate gate wrapper scripts that
|
|
1444
|
+
/** Generate gate wrapper scripts that run validation when agents complete. */
|
|
1438
1445
|
function generateGateWrapperScripts(ast) {
|
|
1439
1446
|
const files = [];
|
|
1440
1447
|
const name = ast.topology.name;
|
|
@@ -1447,11 +1454,13 @@ function generateGateWrapperScripts(ast) {
|
|
|
1447
1454
|
if (gate.behavior === "advisory")
|
|
1448
1455
|
continue;
|
|
1449
1456
|
const gateScriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
|
|
1450
|
-
const onFailExit = gate.onFail === "halt"
|
|
1457
|
+
const onFailExit = gate.onFail === "halt"
|
|
1458
|
+
? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
|
|
1459
|
+
: `echo "Gate '${gate.id}' failed — bounce back and fix the issues." >&2\n exit 2`;
|
|
1451
1460
|
const contentLines = [
|
|
1452
1461
|
"#!/usr/bin/env bash",
|
|
1453
1462
|
`# Gate wrapper: ${gate.id}`,
|
|
1454
|
-
`# Enforces gate "${gate.id}" on
|
|
1463
|
+
`# Enforces gate "${gate.id}" on SubagentStop (after: ${gate.after || "any"}).`,
|
|
1455
1464
|
`# Auto-generated by agentopology scaffold — edit as needed.`,
|
|
1456
1465
|
"set -euo pipefail",
|
|
1457
1466
|
"",
|
|
@@ -1601,14 +1610,17 @@ function generateSettings(ast) {
|
|
|
1601
1610
|
// Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
|
|
1602
1611
|
// This is the Claude Code native approach: hooks in the agent file are scoped
|
|
1603
1612
|
// to that agent automatically.
|
|
1604
|
-
// Gate enforcement — compile gates with run: to
|
|
1613
|
+
// Gate enforcement — compile gates with run: to SubagentStop hooks
|
|
1614
|
+
// Gates fire AFTER an agent finishes (SubagentStop), not before a tool runs.
|
|
1615
|
+
// The matcher is the gate's `after:` agent name so the hook fires when that
|
|
1616
|
+
// specific agent completes.
|
|
1605
1617
|
const gates = ast.nodes.filter((n) => n.type === "gate");
|
|
1606
1618
|
for (const gate of gates) {
|
|
1607
1619
|
if (!gate.run)
|
|
1608
1620
|
continue;
|
|
1609
1621
|
if (gate.behavior === "advisory")
|
|
1610
1622
|
continue; // advisory gates stay in markdown only
|
|
1611
|
-
const eventName = "
|
|
1623
|
+
const eventName = "SubagentStop";
|
|
1612
1624
|
if (!hooksByEvent[eventName])
|
|
1613
1625
|
hooksByEvent[eventName] = [];
|
|
1614
1626
|
const scriptName = `gate-${gate.id}.sh`;
|
|
@@ -1620,8 +1632,12 @@ function generateSettings(ast) {
|
|
|
1620
1632
|
command,
|
|
1621
1633
|
},
|
|
1622
1634
|
],
|
|
1623
|
-
matcher: "Task", // Gates fire on Task tool (agent spawning)
|
|
1624
1635
|
};
|
|
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
|
+
}
|
|
1625
1641
|
hooksByEvent[eventName].push(hookEntry);
|
|
1626
1642
|
}
|
|
1627
1643
|
// Write hooksByEvent to settings if any hooks or gates were registered
|
|
@@ -1633,66 +1649,21 @@ function generateSettings(ast) {
|
|
|
1633
1649
|
if (envSettings) {
|
|
1634
1650
|
settings.env = envSettings;
|
|
1635
1651
|
}
|
|
1636
|
-
// Permissions section from settings block
|
|
1652
|
+
// Permissions section from settings block — nested under "permissions" key
|
|
1637
1653
|
const perms = ast.settings;
|
|
1638
1654
|
if (perms) {
|
|
1639
1655
|
const allow = perms.allow;
|
|
1640
1656
|
const deny = perms.deny;
|
|
1641
|
-
const
|
|
1657
|
+
const permObj = {};
|
|
1642
1658
|
if (allow && allow.length > 0)
|
|
1643
|
-
|
|
1659
|
+
permObj.allow = allow;
|
|
1644
1660
|
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;
|
|
1661
|
+
permObj.deny = deny;
|
|
1662
|
+
if (Object.keys(permObj).length > 0)
|
|
1663
|
+
settings.permissions = permObj;
|
|
1695
1664
|
}
|
|
1665
|
+
// Defaults and observability are documented in the CLAUDE.md context file,
|
|
1666
|
+
// not in settings.json (Claude Code doesn't recognize these keys).
|
|
1696
1667
|
// Top-level extensions for claude-code
|
|
1697
1668
|
if (ast.extensions?.["claude-code"]) {
|
|
1698
1669
|
for (const [k, v] of Object.entries(ast.extensions["claude-code"])) {
|
|
@@ -1955,21 +1926,23 @@ function generateCommandFiles(ast) {
|
|
|
1955
1926
|
// Frontmatter
|
|
1956
1927
|
const fm = {};
|
|
1957
1928
|
if (ast.topology.description) {
|
|
1958
|
-
fm.description =
|
|
1929
|
+
fm.description = ast.topology.description;
|
|
1930
|
+
}
|
|
1931
|
+
if (trigger.argument) {
|
|
1932
|
+
fm["argument-hint"] = `[${trigger.argument}]`;
|
|
1959
1933
|
}
|
|
1960
1934
|
sections.push(frontmatter(fm));
|
|
1961
1935
|
sections.push("");
|
|
1962
|
-
// Header
|
|
1936
|
+
// Header — use $ARGUMENTS for Claude Code command argument substitution
|
|
1963
1937
|
sections.push(`# /${trigger.name}`);
|
|
1964
1938
|
sections.push("");
|
|
1965
|
-
sections.push(trigger.pattern);
|
|
1966
|
-
sections.push("");
|
|
1967
|
-
// Arguments
|
|
1968
1939
|
if (trigger.argument) {
|
|
1969
|
-
sections.push(
|
|
1970
|
-
sections.push(`- ${trigger.argument}: extracted from the command pattern`);
|
|
1971
|
-
sections.push("");
|
|
1940
|
+
sections.push(`/${trigger.name} $ARGUMENTS`);
|
|
1972
1941
|
}
|
|
1942
|
+
else {
|
|
1943
|
+
sections.push(trigger.pattern);
|
|
1944
|
+
}
|
|
1945
|
+
sections.push("");
|
|
1973
1946
|
// Pipeline (from edges, enhanced)
|
|
1974
1947
|
if (ast.edges.length > 0) {
|
|
1975
1948
|
sections.push("## Pipeline");
|
|
@@ -2076,7 +2049,7 @@ function generateWorkspaceProtocol(ast) {
|
|
|
2076
2049
|
* destroy their content.
|
|
2077
2050
|
*/
|
|
2078
2051
|
function generateContextFile(ast) {
|
|
2079
|
-
const DEFAULT_PATH = ".claude/
|
|
2052
|
+
const DEFAULT_PATH = ".claude/CLAUDE.md";
|
|
2080
2053
|
let fileName = ast.context.file ?? DEFAULT_PATH;
|
|
2081
2054
|
// Safety: never overwrite the user's root-level CLAUDE.md
|
|
2082
2055
|
if (fileName === "CLAUDE.md" || fileName === "./CLAUDE.md") {
|