agentopology 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/__tests__/e2e/compile.test.d.ts +12 -0
- package/dist/__tests__/e2e/compile.test.d.ts.map +1 -0
- package/dist/__tests__/e2e/compile.test.js +287 -0
- package/dist/__tests__/e2e/compile.test.js.map +1 -0
- package/dist/__tests__/e2e/roundtrip.test.js +2 -4
- package/dist/__tests__/e2e/roundtrip.test.js.map +1 -1
- package/dist/__tests__/e2e/scaffold-owns.test.js +12 -3
- package/dist/__tests__/e2e/scaffold-owns.test.js.map +1 -1
- package/dist/__tests__/e2e/sync-cycle.test.d.ts +10 -0
- package/dist/__tests__/e2e/sync-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/e2e/sync-cycle.test.js +49 -0
- package/dist/__tests__/e2e/sync-cycle.test.js.map +1 -0
- package/dist/__tests__/live/claude-code.live.test.js +73 -4
- package/dist/__tests__/live/claude-code.live.test.js.map +1 -1
- package/dist/__tests__/open-findings.test.js +4 -25
- package/dist/__tests__/open-findings.test.js.map +1 -1
- package/dist/bindings/__tests__/bindings.test.js +19 -19
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/__tests__/claude-code-hybrid.test.js +5 -5
- package/dist/bindings/__tests__/claude-code-hybrid.test.js.map +1 -1
- package/dist/bindings/__tests__/hybrid-e2e.test.js +1 -1
- package/dist/bindings/__tests__/hybrid-e2e.test.js.map +1 -1
- package/dist/bindings/claude-code.d.ts +6 -8
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +492 -196
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/lib/prompt-fence.d.ts +22 -0
- package/dist/bindings/lib/prompt-fence.d.ts.map +1 -0
- package/dist/bindings/lib/prompt-fence.js +29 -0
- package/dist/bindings/lib/prompt-fence.js.map +1 -0
- package/dist/cli/index.js +9 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/teach.d.ts.map +1 -1
- package/dist/docs/teach.js +10 -0
- package/dist/docs/teach.js.map +1 -1
- package/dist/import/claude-code.d.ts.map +1 -1
- package/dist/import/claude-code.js +21 -5
- package/dist/import/claude-code.js.map +1 -1
- package/dist/import/serializer.d.ts.map +1 -1
- package/dist/import/serializer.js +44 -3
- package/dist/import/serializer.js.map +1 -1
- package/dist/parser/__tests__/mod-hooks.test.js +1 -1
- package/dist/parser/__tests__/mod-hooks.test.js.map +1 -1
- package/dist/parser/ast.d.ts +46 -3
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +60 -3
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/run.d.ts +23 -2
- package/dist/parser/run.d.ts.map +1 -1
- package/dist/parser/run.js +39 -3
- package/dist/parser/run.js.map +1 -1
- package/dist/parser/validator.d.ts +1 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +131 -3
- package/dist/parser/validator.js.map +1 -1
- package/dist/scaffold/diff.d.ts +17 -0
- package/dist/scaffold/diff.d.ts.map +1 -0
- package/dist/scaffold/diff.js +66 -0
- package/dist/scaffold/diff.js.map +1 -0
- package/dist/scaffold/merge.d.ts.map +1 -1
- package/dist/scaffold/merge.js +14 -0
- package/dist/scaffold/merge.js.map +1 -1
- package/dist/sync/index.d.ts +1 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +40 -2
- package/dist/sync/index.js.map +1 -1
- package/package.json +1 -1
- package/spec/grammar.md +28 -2
- package/spec/testing.md +35 -25
- package/spec/validation.md +5 -1
package/dist/parser/validator.js
CHANGED
|
@@ -1019,16 +1019,19 @@ function v96RunIsCommand(ast) {
|
|
|
1019
1019
|
...(nodeId ? { node: nodeId, line: lookupLine(ast, nodeId) } : {}),
|
|
1020
1020
|
});
|
|
1021
1021
|
};
|
|
1022
|
+
// Only a command hook's run: is a command: a prompt or agent hook's run: is
|
|
1023
|
+
// the prompt itself, and http, mcp_tool and mod hooks are checked by V100/V95.
|
|
1024
|
+
const isCommand = (hook) => (hook.type ?? "command") === "command";
|
|
1022
1025
|
for (const node of ast.nodes) {
|
|
1023
1026
|
if (isGate(node))
|
|
1024
1027
|
check(`Gate "${node.id}"`, node.run, node.id);
|
|
1025
1028
|
if (node.type === "agent") {
|
|
1026
|
-
for (const hook of node.hooks ?? []) {
|
|
1029
|
+
for (const hook of (node.hooks ?? []).filter(isCommand)) {
|
|
1027
1030
|
check(`Hook "${hook.name}" on agent "${node.id}"`, hook.run, node.id);
|
|
1028
1031
|
}
|
|
1029
1032
|
}
|
|
1030
1033
|
}
|
|
1031
|
-
for (const hook of ast.hooks ?? [])
|
|
1034
|
+
for (const hook of (ast.hooks ?? []).filter(isCommand))
|
|
1032
1035
|
check(`Hook "${hook.name}"`, hook.run);
|
|
1033
1036
|
return results;
|
|
1034
1037
|
}
|
|
@@ -1062,6 +1065,127 @@ function v97FileIsProjectPath(ast) {
|
|
|
1062
1065
|
check(`Command "${trigger.name}"`, trigger.file);
|
|
1063
1066
|
return results;
|
|
1064
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Claude Code events that take only command, http and mcp_tool hooks (no
|
|
1070
|
+
* prompt or agent judge), and the two that take only command and mcp_tool.
|
|
1071
|
+
* From the hooks reference, 2026-10-30.
|
|
1072
|
+
*/
|
|
1073
|
+
const NO_JUDGE_EVENTS = new Set([
|
|
1074
|
+
"ConfigChange", "CwdChanged", "DirectoryAdded", "Elicitation", "ElicitationResult", "FileChanged",
|
|
1075
|
+
"InstructionsLoaded", "MessageDisplay", "Notification", "PostCompact", "PostModelSwitch", "PreCompact",
|
|
1076
|
+
"PreModelSwitch", "SessionEnd", "StopFailure", "SubagentStart", "WorktreeCreate", "WorktreeRemove",
|
|
1077
|
+
"AgentStart",
|
|
1078
|
+
]);
|
|
1079
|
+
const COMMAND_OR_MCP_EVENTS = new Set(["SessionStart", "Setup"]);
|
|
1080
|
+
/**
|
|
1081
|
+
* V100: a hook has what its type needs, on an event that takes that type.
|
|
1082
|
+
*
|
|
1083
|
+
* A command needs run:, a prompt or agent hook needs run: (the prompt), an
|
|
1084
|
+
* http hook url:, an mcp_tool hook server: and tool:. A prompt or agent judge
|
|
1085
|
+
* on an event Claude Code runs only command hooks for never fires.
|
|
1086
|
+
*/
|
|
1087
|
+
function v100HookTypeFields(ast) {
|
|
1088
|
+
const results = [];
|
|
1089
|
+
for (const { hook, where } of allHooks(ast)) {
|
|
1090
|
+
const type = hook.type ?? "command";
|
|
1091
|
+
if (type === "mod")
|
|
1092
|
+
continue; // V95
|
|
1093
|
+
const err = (message) => results.push({ rule: "V100", level: "error", message: `Hook "${where}" ${message}`, node: where });
|
|
1094
|
+
if ((type === "command" || type === "prompt" || type === "agent") && !hook.run) {
|
|
1095
|
+
err(`is type ${type} but has no run: — ${type === "command" ? "name the command" : "write the prompt in run:"}.`);
|
|
1096
|
+
}
|
|
1097
|
+
if (type === "http" && !hook.url)
|
|
1098
|
+
err(`is type http but has no url: — name the endpoint the event is sent to.`);
|
|
1099
|
+
if (type === "mcp_tool" && (!hook.server || !hook.tool))
|
|
1100
|
+
err(`is type mcp_tool but needs server: and tool:.`);
|
|
1101
|
+
if (type !== "command" && hook.async !== undefined)
|
|
1102
|
+
err(`sets async: but is type ${type} — async: belongs to a command hook.`);
|
|
1103
|
+
const on = hook.on.trim();
|
|
1104
|
+
if ((type === "prompt" || type === "agent") && NO_JUDGE_EVENTS.has(on)) {
|
|
1105
|
+
err(`is a ${type} hook on ${on}, which Claude Code runs only command, http and mcp_tool hooks for — it would never fire.`);
|
|
1106
|
+
}
|
|
1107
|
+
else if ((type === "prompt" || type === "agent" || type === "http") && COMMAND_OR_MCP_EVENTS.has(on)) {
|
|
1108
|
+
err(`is a ${type} hook on ${on}, which takes only command and mcp_tool hooks — it would never fire.`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return results;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* V101: a node's run-mode is one Claude Code has, and an independent node
|
|
1115
|
+
* does not share its caller's context.
|
|
1116
|
+
*
|
|
1117
|
+
* runs: subagent (own agent), inline (a skill in the caller's context) or
|
|
1118
|
+
* fork (a skill in a fresh forked context). An independent critic running
|
|
1119
|
+
* inline would read the author's reasoning, which is what independent forbids.
|
|
1120
|
+
*/
|
|
1121
|
+
const RUN_MODES = ["subagent", "inline", "fork"];
|
|
1122
|
+
function v101RunMode(ast) {
|
|
1123
|
+
const results = [];
|
|
1124
|
+
for (const node of ast.nodes) {
|
|
1125
|
+
if (node.type !== "agent")
|
|
1126
|
+
continue;
|
|
1127
|
+
const agent = node;
|
|
1128
|
+
if (agent.runs !== undefined && !RUN_MODES.includes(agent.runs)) {
|
|
1129
|
+
results.push({
|
|
1130
|
+
rule: "V101", level: "error", node: agent.id, line: lookupLine(ast, agent.id),
|
|
1131
|
+
message: `Agent "${agent.id}" has runs: ${agent.runs} — a node runs as ${RUN_MODES.join(", ")}.`,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
if (agent.independent && agent.runs === "inline") {
|
|
1135
|
+
results.push({
|
|
1136
|
+
rule: "V101", level: "error", node: agent.id, line: lookupLine(ast, agent.id),
|
|
1137
|
+
message: `Agent "${agent.id}" is independent but runs inline, in its caller's context, where it reads the author's reasoning. Use runs: fork or runs: subagent.`,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return results;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* V98: a block that belongs inside a container is not written beside it.
|
|
1145
|
+
*
|
|
1146
|
+
* `gate x { }` outside `gates { }` (likewise hook, tool, job, command, level)
|
|
1147
|
+
* was dropped by the parser with no message. Collected at parse time.
|
|
1148
|
+
*/
|
|
1149
|
+
function v98BlockInsideItsContainer(ast) {
|
|
1150
|
+
return ast._misplacedBlockErrors ?? [];
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* V99: an agent's `skills:` and `mcp-servers:`, and a skill's `agent:`, name
|
|
1154
|
+
* something this topology declares.
|
|
1155
|
+
*
|
|
1156
|
+
* A misspelled name compiled to a reference Claude Code could not resolve,
|
|
1157
|
+
* with no message. A warning, not an error: the name may be real outside the
|
|
1158
|
+
* topology (a skill on disk, a user-level MCP server, a built-in agent).
|
|
1159
|
+
*/
|
|
1160
|
+
const BUILT_IN_AGENTS = new Set(["general-purpose", "Explore", "Plan"]);
|
|
1161
|
+
function v99ReferencesDeclared(ast) {
|
|
1162
|
+
const results = [];
|
|
1163
|
+
const skills = new Set([ast.topology.name, ...(ast.skills ?? []).map((s) => s.id)]);
|
|
1164
|
+
const servers = new Set([...Object.keys(ast.mcpServers ?? {}), ...(ast.stores ?? []).map((s) => s.id)]);
|
|
1165
|
+
const agents = new Set(ast.nodes.filter((n) => n.type === "agent" || n.type === "group").map((n) => n.id));
|
|
1166
|
+
const warn = (message, nodeId) => results.push({ rule: "V99", level: "warning", message, ...(nodeId ? { node: nodeId, line: lookupLine(ast, nodeId) } : {}) });
|
|
1167
|
+
for (const node of ast.nodes) {
|
|
1168
|
+
if (node.type !== "agent")
|
|
1169
|
+
continue;
|
|
1170
|
+
const agent = node;
|
|
1171
|
+
for (const skill of agent.skills ?? []) {
|
|
1172
|
+
if (!skills.has(skill)) {
|
|
1173
|
+
warn(`Agent "${agent.id}" uses skill "${skill}", which this topology does not declare — scaffold writes no SKILL.md for it. If it is hand-written, declare skill ${skill} { file: ".claude/skills/${skill}/SKILL.md" }; otherwise check the name`, agent.id);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
for (const server of agent.mcpServers ?? []) {
|
|
1177
|
+
if (!servers.has(server)) {
|
|
1178
|
+
warn(`Agent "${agent.id}" uses MCP server "${server}", which mcp-servers { } does not declare — it resolves only if configured outside this project`, agent.id);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
for (const skill of ast.skills ?? []) {
|
|
1183
|
+
if (skill.agent && !agents.has(skill.agent) && !BUILT_IN_AGENTS.has(skill.agent)) {
|
|
1184
|
+
warn(`Skill "${skill.id}" runs in agent "${skill.agent}", which is not an agent in this topology or a built-in one (${[...BUILT_IN_AGENTS].join(", ")})`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return results;
|
|
1188
|
+
}
|
|
1065
1189
|
function v93EdgeHandoffDeclared(ast) {
|
|
1066
1190
|
const results = [];
|
|
1067
1191
|
const byId = new Map(ast.nodes.map((n) => [n.id, n]));
|
|
@@ -3030,7 +3154,7 @@ export const MOD_EVENTS_SEEN = [
|
|
|
3030
3154
|
"prompt.context", "session.start", "session.end", "model.complete", "message.received",
|
|
3031
3155
|
];
|
|
3032
3156
|
/** Hook types the grammar defines (`spec/grammar.md` §4.16). */
|
|
3033
|
-
export const HOOK_TYPES = ["command", "prompt", "mod"];
|
|
3157
|
+
export const HOOK_TYPES = ["command", "prompt", "agent", "http", "mcp_tool", "mod"];
|
|
3034
3158
|
/** Every hook in the topology — global first, then per-agent — with its owner. */
|
|
3035
3159
|
function allHooks(ast) {
|
|
3036
3160
|
const out = ast.hooks.map((hook) => ({ hook, where: hook.name }));
|
|
@@ -3186,6 +3310,10 @@ export function validate(ast) {
|
|
|
3186
3310
|
...v95HookEventKnown(ast),
|
|
3187
3311
|
...v96RunIsCommand(ast),
|
|
3188
3312
|
...v97FileIsProjectPath(ast),
|
|
3313
|
+
...v98BlockInsideItsContainer(ast),
|
|
3314
|
+
...v99ReferencesDeclared(ast),
|
|
3315
|
+
...v100HookTypeFields(ast),
|
|
3316
|
+
...v101RunMode(ast),
|
|
3189
3317
|
...v25BounceBackAdvisory(ast),
|
|
3190
3318
|
...v26ActionKindEnum(ast),
|
|
3191
3319
|
...v27AgentPermissionsEnum(ast),
|