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.
Files changed (73) hide show
  1. package/README.md +5 -5
  2. package/dist/__tests__/e2e/compile.test.d.ts +12 -0
  3. package/dist/__tests__/e2e/compile.test.d.ts.map +1 -0
  4. package/dist/__tests__/e2e/compile.test.js +287 -0
  5. package/dist/__tests__/e2e/compile.test.js.map +1 -0
  6. package/dist/__tests__/e2e/roundtrip.test.js +2 -4
  7. package/dist/__tests__/e2e/roundtrip.test.js.map +1 -1
  8. package/dist/__tests__/e2e/scaffold-owns.test.js +12 -3
  9. package/dist/__tests__/e2e/scaffold-owns.test.js.map +1 -1
  10. package/dist/__tests__/e2e/sync-cycle.test.d.ts +10 -0
  11. package/dist/__tests__/e2e/sync-cycle.test.d.ts.map +1 -0
  12. package/dist/__tests__/e2e/sync-cycle.test.js +49 -0
  13. package/dist/__tests__/e2e/sync-cycle.test.js.map +1 -0
  14. package/dist/__tests__/live/claude-code.live.test.js +73 -4
  15. package/dist/__tests__/live/claude-code.live.test.js.map +1 -1
  16. package/dist/__tests__/open-findings.test.js +4 -25
  17. package/dist/__tests__/open-findings.test.js.map +1 -1
  18. package/dist/bindings/__tests__/bindings.test.js +19 -19
  19. package/dist/bindings/__tests__/bindings.test.js.map +1 -1
  20. package/dist/bindings/__tests__/claude-code-hybrid.test.js +5 -5
  21. package/dist/bindings/__tests__/claude-code-hybrid.test.js.map +1 -1
  22. package/dist/bindings/__tests__/hybrid-e2e.test.js +1 -1
  23. package/dist/bindings/__tests__/hybrid-e2e.test.js.map +1 -1
  24. package/dist/bindings/claude-code.d.ts +6 -8
  25. package/dist/bindings/claude-code.d.ts.map +1 -1
  26. package/dist/bindings/claude-code.js +492 -196
  27. package/dist/bindings/claude-code.js.map +1 -1
  28. package/dist/bindings/lib/prompt-fence.d.ts +22 -0
  29. package/dist/bindings/lib/prompt-fence.d.ts.map +1 -0
  30. package/dist/bindings/lib/prompt-fence.js +29 -0
  31. package/dist/bindings/lib/prompt-fence.js.map +1 -0
  32. package/dist/cli/index.js +9 -1
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/docs/teach.d.ts.map +1 -1
  35. package/dist/docs/teach.js +10 -0
  36. package/dist/docs/teach.js.map +1 -1
  37. package/dist/import/claude-code.d.ts.map +1 -1
  38. package/dist/import/claude-code.js +21 -5
  39. package/dist/import/claude-code.js.map +1 -1
  40. package/dist/import/serializer.d.ts.map +1 -1
  41. package/dist/import/serializer.js +44 -3
  42. package/dist/import/serializer.js.map +1 -1
  43. package/dist/parser/__tests__/mod-hooks.test.js +1 -1
  44. package/dist/parser/__tests__/mod-hooks.test.js.map +1 -1
  45. package/dist/parser/ast.d.ts +46 -3
  46. package/dist/parser/ast.d.ts.map +1 -1
  47. package/dist/parser/ast.js.map +1 -1
  48. package/dist/parser/index.d.ts.map +1 -1
  49. package/dist/parser/index.js +60 -3
  50. package/dist/parser/index.js.map +1 -1
  51. package/dist/parser/run.d.ts +23 -2
  52. package/dist/parser/run.d.ts.map +1 -1
  53. package/dist/parser/run.js +39 -3
  54. package/dist/parser/run.js.map +1 -1
  55. package/dist/parser/validator.d.ts +1 -1
  56. package/dist/parser/validator.d.ts.map +1 -1
  57. package/dist/parser/validator.js +131 -3
  58. package/dist/parser/validator.js.map +1 -1
  59. package/dist/scaffold/diff.d.ts +17 -0
  60. package/dist/scaffold/diff.d.ts.map +1 -0
  61. package/dist/scaffold/diff.js +66 -0
  62. package/dist/scaffold/diff.js.map +1 -0
  63. package/dist/scaffold/merge.d.ts.map +1 -1
  64. package/dist/scaffold/merge.js +14 -0
  65. package/dist/scaffold/merge.js.map +1 -1
  66. package/dist/sync/index.d.ts +1 -0
  67. package/dist/sync/index.d.ts.map +1 -1
  68. package/dist/sync/index.js +40 -2
  69. package/dist/sync/index.js.map +1 -1
  70. package/package.json +1 -1
  71. package/spec/grammar.md +28 -2
  72. package/spec/testing.md +35 -25
  73. package/spec/validation.md +5 -1
@@ -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),