agentopology 0.4.3 → 0.5.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.
Files changed (93) hide show
  1. package/README.md +5 -5
  2. package/dist/bindings/__tests__/reasoning-effort.test.d.ts +11 -0
  3. package/dist/bindings/__tests__/reasoning-effort.test.d.ts.map +1 -0
  4. package/dist/bindings/__tests__/reasoning-effort.test.js +93 -0
  5. package/dist/bindings/__tests__/reasoning-effort.test.js.map +1 -0
  6. package/dist/bindings/claude-code.d.ts.map +1 -1
  7. package/dist/bindings/claude-code.js +27 -15
  8. package/dist/bindings/claude-code.js.map +1 -1
  9. package/dist/bindings/claude-workflow.d.ts.map +1 -1
  10. package/dist/bindings/claude-workflow.js +11 -3
  11. package/dist/bindings/claude-workflow.js.map +1 -1
  12. package/dist/bindings/codex.d.ts.map +1 -1
  13. package/dist/bindings/codex.js +8 -3
  14. package/dist/bindings/codex.js.map +1 -1
  15. package/dist/bindings/copilot-cli.d.ts.map +1 -1
  16. package/dist/bindings/copilot-cli.js +7 -6
  17. package/dist/bindings/copilot-cli.js.map +1 -1
  18. package/dist/bindings/cursor.d.ts.map +1 -1
  19. package/dist/bindings/cursor.js +14 -7
  20. package/dist/bindings/cursor.js.map +1 -1
  21. package/dist/bindings/gemini-cli.d.ts.map +1 -1
  22. package/dist/bindings/gemini-cli.js +3 -2
  23. package/dist/bindings/gemini-cli.js.map +1 -1
  24. package/dist/bindings/kiro.d.ts.map +1 -1
  25. package/dist/bindings/kiro.js +5 -4
  26. package/dist/bindings/kiro.js.map +1 -1
  27. package/dist/bindings/openclaw.d.ts.map +1 -1
  28. package/dist/bindings/openclaw.js +4 -3
  29. package/dist/bindings/openclaw.js.map +1 -1
  30. package/dist/cli/index.d.ts +1 -0
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +45 -2
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/docs/__tests__/teach.test.d.ts +10 -0
  35. package/dist/docs/__tests__/teach.test.d.ts.map +1 -0
  36. package/dist/docs/__tests__/teach.test.js +109 -0
  37. package/dist/docs/__tests__/teach.test.js.map +1 -0
  38. package/dist/docs/content.js +1 -1
  39. package/dist/docs/index.d.ts +10 -0
  40. package/dist/docs/index.d.ts.map +1 -1
  41. package/dist/docs/index.js +91 -0
  42. package/dist/docs/index.js.map +1 -1
  43. package/dist/docs/teach.d.ts +45 -0
  44. package/dist/docs/teach.d.ts.map +1 -0
  45. package/dist/docs/teach.js +200 -0
  46. package/dist/docs/teach.js.map +1 -0
  47. package/dist/exporters/markdown.d.ts.map +1 -1
  48. package/dist/exporters/markdown.js +3 -2
  49. package/dist/exporters/markdown.js.map +1 -1
  50. package/dist/import/serializer.d.ts.map +1 -1
  51. package/dist/import/serializer.js +11 -2
  52. package/dist/import/serializer.js.map +1 -1
  53. package/dist/parser/__tests__/gate-anchors.test.d.ts +18 -0
  54. package/dist/parser/__tests__/gate-anchors.test.d.ts.map +1 -0
  55. package/dist/parser/__tests__/gate-anchors.test.js +183 -0
  56. package/dist/parser/__tests__/gate-anchors.test.js.map +1 -0
  57. package/dist/parser/__tests__/parser.test.js +92 -0
  58. package/dist/parser/__tests__/parser.test.js.map +1 -1
  59. package/dist/parser/ast.d.ts +38 -4
  60. package/dist/parser/ast.d.ts.map +1 -1
  61. package/dist/parser/ast.js +26 -1
  62. package/dist/parser/ast.js.map +1 -1
  63. package/dist/parser/index.d.ts.map +1 -1
  64. package/dist/parser/index.js +11 -2
  65. package/dist/parser/index.js.map +1 -1
  66. package/dist/parser/validator.d.ts.map +1 -1
  67. package/dist/parser/validator.js +71 -7
  68. package/dist/parser/validator.js.map +1 -1
  69. package/dist/plan/__tests__/evidence.test.d.ts +13 -0
  70. package/dist/plan/__tests__/evidence.test.d.ts.map +1 -0
  71. package/dist/plan/__tests__/evidence.test.js +107 -0
  72. package/dist/plan/__tests__/evidence.test.js.map +1 -0
  73. package/dist/plan/brief.d.ts +23 -3
  74. package/dist/plan/brief.d.ts.map +1 -1
  75. package/dist/plan/brief.js +25 -3
  76. package/dist/plan/brief.js.map +1 -1
  77. package/dist/plan/render.d.ts.map +1 -1
  78. package/dist/plan/render.js +24 -5
  79. package/dist/plan/render.js.map +1 -1
  80. package/dist/render/ascii.d.ts.map +1 -1
  81. package/dist/render/ascii.js +25 -3
  82. package/dist/render/ascii.js.map +1 -1
  83. package/dist/resolve/order.d.ts.map +1 -1
  84. package/dist/resolve/order.js +29 -16
  85. package/dist/resolve/order.js.map +1 -1
  86. package/dist/visualizer/index.d.ts.map +1 -1
  87. package/dist/visualizer/index.js +17 -4
  88. package/dist/visualizer/index.js.map +1 -1
  89. package/examples/data-processing.at +1 -1
  90. package/examples/simple-pipeline.at +1 -1
  91. package/package.json +1 -1
  92. package/spec/reserved-keywords.md +1 -1
  93. package/spec/validation.md +2 -1
package/README.md CHANGED
@@ -96,7 +96,7 @@ who works, when they work, and what they know when they work. One file, two ways
96
96
  ┌────────────┐
97
97
  ┌──────────────┐ │ Parser & │ scaffold ──▶ Platform configs
98
98
  │ .at file │─▶│ Validator │──┤ ├── .claude/agents/
99
- │ (you write) │ │ 92 rules │ │ ├── <topology>.workflow.js
99
+ │ (you write) │ │ 93 rules │ │ ├── <topology>.workflow.js
100
100
  └──────────────┘ └────────────┘ │ ├── .codex/ .cursor/rules/
101
101
  │ ├── .openclaw/ .github/agents/
102
102
  │ └── .kiro/ …
@@ -234,7 +234,7 @@ In Claude Code, type `/agentopology` — or just ask naturally:
234
234
  Say **"I want a code review team with an analyzer, security scanner, and reviewer"** — the skill:
235
235
 
236
236
  1. Generates the `.at` file with the right syntax
237
- 2. Validates it against 92 rules
237
+ 2. Validates it against 93 rules
238
238
  3. Scaffolds it to Claude Code, OpenClaw, Cursor, or any target
239
239
 
240
240
  **Full agent team in under 2 minutes.** No docs to read. No syntax to memorize. You describe what you want, the skill handles the rest.
@@ -566,7 +566,7 @@ import { parse, validate, bindings } from "agentopology";
566
566
  // Parse
567
567
  const ast = parse(atSource);
568
568
 
569
- // Validate (92 built-in rules, V1-V92)
569
+ // Validate (93 built-in rules, V1-V93)
570
570
  const issues = validate(ast);
571
571
 
572
572
  // Scaffold — generate platform files
@@ -624,7 +624,7 @@ You can `agentopology visualize` it into an interactive graph. You can hand it t
624
624
  | **Switch platforms** | Rewrite everything | Change `--target` |
625
625
  | **Add an agent** | Update 5-12 files across 3 tools | Add 4 lines to `.at` file |
626
626
  | **See the architecture** | Read YAML, JSON, TOML, Markdown across 6 dirs | One `.at` file. Or `visualize` it. |
627
- | **Validate** | Hope for the best | 92 built-in rules catch errors before deploy |
627
+ | **Validate** | Hope for the best | 93 built-in rules catch errors before deploy |
628
628
  | **Onboard someone** | "Read these 15 files and figure it out" | "Read this `.at` file" |
629
629
  | **Version control** | Diff 47 generated files | Diff one `.at` file |
630
630
  | **Move to a new tool** | Start over | `--target new-tool` |
@@ -646,7 +646,7 @@ You can `agentopology visualize` it into an interactive graph. You can hand it t
646
646
  ## CLI Reference
647
647
 
648
648
  ```
649
- agentopology validate <file> Validate an .at file (92 rules)
649
+ agentopology validate <file> Validate an .at file (93 rules)
650
650
  agentopology plan <file> Resolve for interpreted execution — no files written
651
651
  agentopology scaffold <file> --target <t> Generate platform configs
652
652
  agentopology sync <file> --target <t> --dir <d> Sync platform files back to .at
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `thinking` IS the reasoning-effort field.
3
+ *
4
+ * First-contact feedback asked for an `effort` field beside `model`. An audit
5
+ * found `AgentNode.thinking` already existed AND that codex already compiled it
6
+ * to `model_reasoning_effort`. Adding a second field would have given the
7
+ * language two words for one concept — the exact failure mode the anti-KPIs
8
+ * name. So: zero new vocabulary, three real fixes.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=reasoning-effort.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-effort.test.d.ts","sourceRoot":"","sources":["../../../src/bindings/__tests__/reasoning-effort.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `thinking` IS the reasoning-effort field.
3
+ *
4
+ * First-contact feedback asked for an `effort` field beside `model`. An audit
5
+ * found `AgentNode.thinking` already existed AND that codex already compiled it
6
+ * to `model_reasoning_effort`. Adding a second field would have given the
7
+ * language two words for one concept — the exact failure mode the anti-KPIs
8
+ * name. So: zero new vocabulary, three real fixes.
9
+ */
10
+ import { describe, it, expect } from "vitest";
11
+ import { parse } from "../../parser/index.js";
12
+ import { validate } from "../../parser/validator.js";
13
+ import { bindings } from "../index.js";
14
+ const at = (thinking, extra = "") => `topology t : [pipeline] {
15
+ meta {
16
+ version: "1.0.0"
17
+ description: "x"
18
+ }
19
+ agent a {
20
+ model: opus
21
+ description: "a"
22
+ thinking: ${thinking}${extra}
23
+ }
24
+ action i {
25
+ kind: inline
26
+ description: "in"
27
+ }
28
+ flow { i -> a }
29
+ }`;
30
+ const WORKFLOW_EXT = `
31
+ extensions {
32
+ claude-workflow {
33
+ execution: workflow
34
+ }
35
+ }`;
36
+ describe("thinking accepts the full effort ladder", () => {
37
+ it("accepts xhigh, which modern routing exposes and the enum lacked", () => {
38
+ expect(validate(parse(at("xhigh"))).filter((r) => r.rule === "V35")).toEqual([]);
39
+ });
40
+ it("still accepts every previously valid level", () => {
41
+ for (const t of ["off", "low", "medium", "high", "max"]) {
42
+ expect(validate(parse(at(t))).filter((r) => r.rule === "V35"), t).toEqual([]);
43
+ }
44
+ });
45
+ it("still rejects a level that does not exist", () => {
46
+ expect(validate(parse(at("ludicrous"))).filter((r) => r.rule === "V35")).toHaveLength(1);
47
+ });
48
+ });
49
+ describe("codex maps every level to a DISTINCT value", () => {
50
+ const effortOf = (thinking) => {
51
+ const f = bindings["codex"]
52
+ .scaffold(parse(at(thinking)))
53
+ .find((x) => x.path.endsWith("agents/a.toml"));
54
+ return /model_reasoning_effort = "(\w+)"/.exec(f.content)?.[1];
55
+ };
56
+ it("no longer collapses max onto high", () => {
57
+ // `max` returned "high", so max and xhigh had no distinct Codex value.
58
+ expect(effortOf("max")).toBe("max");
59
+ expect(effortOf("xhigh")).toBe("xhigh");
60
+ expect(effortOf("high")).toBe("high");
61
+ expect(new Set(["max", "xhigh", "high"]).size).toBe(3);
62
+ });
63
+ it("omits the key entirely for off", () => {
64
+ expect(effortOf("off")).toBeUndefined();
65
+ });
66
+ });
67
+ describe("claude-workflow emits effort — the option its runtime accepts", () => {
68
+ const script = (thinking) => bindings["claude-workflow"]
69
+ .scaffold(parse(at(thinking, WORKFLOW_EXT)))
70
+ .find((f) => f.path.endsWith(".js"))?.content ?? "";
71
+ it("passes thinking through as the agent() effort option", () => {
72
+ // This binding emitted nothing for it and never read agent.thinking at all,
73
+ // so a declared reasoning level was dropped on the one target that can
74
+ // honour it natively.
75
+ expect(script("xhigh")).toContain("effort: 'xhigh'");
76
+ });
77
+ it("omits effort when thinking is off", () => {
78
+ const s = script("off");
79
+ expect(s.length).toBeGreaterThan(0);
80
+ expect(s).not.toContain("effort:");
81
+ });
82
+ });
83
+ describe("no `effort` field was added to the language", () => {
84
+ it("rejects `effort` as an agent field rather than silently accepting it", () => {
85
+ // Two words for one concept is the failure mode. If someone writes
86
+ // `effort:` expecting it to work, they should find out.
87
+ const src = at("high").replace("thinking: high", "effort: high");
88
+ const ast = parse(src);
89
+ const agent = ast.nodes.find((n) => n.id === "a");
90
+ expect(agent.effort, "no effort field should exist on the AST").toBeUndefined();
91
+ });
92
+ });
93
+ //# sourceMappingURL=reasoning-effort.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-effort.test.js","sourceRoot":"","sources":["../../../src/bindings/__tests__/reasoning-effort.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;AAw4F/D,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,EAAE,aAkG/B,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;AAo5F/D,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,EAAE,aAkG/B,CAAC"}
@@ -10,6 +10,7 @@ import { deduplicateFiles } from "./types.js";
10
10
  import { shellStub, STUB_MARKER } from "./lib/stub.js";
11
11
  import { isWorkflowSeamAgent, seamFiles } from "./lib/seam.js";
12
12
  import { phaseOf as sharedPhaseOf } from "../resolve/phase.js";
13
+ import { gateAnchors } from "../parser/ast.js";
13
14
  // ---------------------------------------------------------------------------
14
15
  // Helpers
15
16
  // ---------------------------------------------------------------------------
@@ -1236,9 +1237,9 @@ function generateTopologySkill(ast) {
1236
1237
  for (const gate of gates) {
1237
1238
  sections.push(`### ${toTitle(gate.id)}`);
1238
1239
  if (gate.after)
1239
- sections.push(`After: ${gate.after}`);
1240
+ sections.push(`After: ${gateAnchors(gate, "after").join(", ")}`);
1240
1241
  if (gate.before)
1241
- sections.push(`Before: ${gate.before}`);
1242
+ sections.push(`Before: ${gateAnchors(gate, "before").join(", ")}`);
1242
1243
  if (gate.run)
1243
1244
  sections.push(`Run: ${gate.run}`);
1244
1245
  if (gate.checks && gate.checks.length > 0) {
@@ -1669,7 +1670,10 @@ function generateGateWrapperScripts(ast) {
1669
1670
  ? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
1670
1671
  : `echo "Gate '${gate.id}' failed — bounce back and fix the issues." >&2\n exit 2`;
1671
1672
  const inlineMode = getDelegationMode(ast) === "inline";
1672
- const wiredAsHook = !inlineMode && isRegisteredSubagentType(ast, gate.after);
1673
+ const gateAfters = gateAnchors(gate, "after");
1674
+ const wiredAsHook = !inlineMode &&
1675
+ gateAfters.length > 0 &&
1676
+ gateAfters.every((a) => isRegisteredSubagentType(ast, a));
1673
1677
  let enforcementNote;
1674
1678
  let enforcementNote2;
1675
1679
  if (inlineMode) {
@@ -1677,11 +1681,11 @@ function generateGateWrapperScripts(ast) {
1677
1681
  enforcementNote2 = `# Invoke this script from your /${ast.topology.name} playbook at the right step.`;
1678
1682
  }
1679
1683
  else if (wiredAsHook) {
1680
- enforcementNote = `# Enforcement: SubagentStop hook on agent "${gate.after}". Exit 2 blocks the subagent`;
1684
+ enforcementNote = `# Enforcement: SubagentStop hook on agent "${gateAnchors(gate, "after").join('", "')}". Exit 2 blocks the subagent`;
1681
1685
  enforcementNote2 = `# from stopping (Claude Code prevents stop and surfaces stderr to Claude).`;
1682
1686
  }
1683
1687
  else {
1684
- enforcementNote = `# Enforcement: NOT wired as a SubagentStop hook (after: ${gate.after || "<unset>"} is not an agent).`;
1688
+ enforcementNote = `# Enforcement: NOT wired as a SubagentStop hook (after: ${gateAnchors(gate, "after").join(", ") || "<unset>"} is not an agent).`;
1685
1689
  enforcementNote2 = `# Invoke this script from your orchestrator playbook or a /command at the right step.`;
1686
1690
  }
1687
1691
  const contentLines = [
@@ -1692,8 +1696,8 @@ function generateGateWrapperScripts(ast) {
1692
1696
  enforcementNote2,
1693
1697
  "set -euo pipefail",
1694
1698
  "",
1695
- "# Gate runs after: " + (gate.after || "any"),
1696
- "# Gate runs before: " + (gate.before || "any"),
1699
+ "# Gate runs after: " + (gateAnchors(gate, "after").join(", ") || "any"),
1700
+ "# Gate runs before: " + (gateAnchors(gate, "before").join(", ") || "any"),
1697
1701
  `# On failure: ${gate.onFail || "halt"}`,
1698
1702
  ];
1699
1703
  if (gate.timeout) {
@@ -1937,8 +1941,12 @@ function generateSettings(ast) {
1937
1941
  // SubagentStop matcher must be a registered subagent_type. If the gate
1938
1942
  // doesn't target an agent node, we cannot wire it as a hook — warn and
1939
1943
  // skip so we don't ship dead config that never fires.
1940
- if (!isRegisteredSubagentType(ast, gate.after)) {
1941
- const targetDesc = gate.after ? `non-agent target "${gate.after}"` : "no after: target";
1944
+ const anchors = gateAnchors(gate, "after");
1945
+ if (anchors.length === 0 || !anchors.every((a) => isRegisteredSubagentType(ast, a))) {
1946
+ const bad = anchors.filter((a) => !isRegisteredSubagentType(ast, a));
1947
+ const targetDesc = anchors.length
1948
+ ? `non-agent target ${bad.map((b) => `"${b}"`).join(", ")}`
1949
+ : "no after: target";
1942
1950
  console.warn(`[claude-code] Gate "${gate.id}" has ${targetDesc} — SubagentStop hook not emitted ` +
1943
1951
  `(matcher must be an agent node id). The gate wrapper script is still generated; ` +
1944
1952
  `invoke it from your orchestrator playbook or a slash command at the right step.`);
@@ -1949,16 +1957,20 @@ function generateSettings(ast) {
1949
1957
  hooksByEvent[eventName] = [];
1950
1958
  const scriptName = `gate-${gate.id}.sh`;
1951
1959
  const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
1952
- const hookEntry = {
1953
- matcher: gate.after,
1960
+ // ONE ENTRY PER ANCHOR. A SubagentStop matcher is a STRING — an array would
1961
+ // land in settings.json, type-check clean through Record<string, unknown>,
1962
+ // and the hook would simply never fire. Silent, and the gate would look
1963
+ // wired while doing nothing.
1964
+ const hookEntries = anchors.map((anchor) => ({
1965
+ matcher: anchor,
1954
1966
  hooks: [
1955
1967
  {
1956
1968
  type: "command",
1957
1969
  command,
1958
1970
  },
1959
1971
  ],
1960
- };
1961
- hooksByEvent[eventName].push(hookEntry);
1972
+ }));
1973
+ hooksByEvent[eventName].push(...hookEntries);
1962
1974
  }
1963
1975
  // HYBRID: host-layer concurrent Blackboard observer.
1964
1976
  //
@@ -2337,8 +2349,8 @@ function generateCommandFiles(ast) {
2337
2349
  "by its `after:` field. Exit code 0 = pass, non-zero = fail.");
2338
2350
  sections.push("");
2339
2351
  for (const gate of blockingGates) {
2340
- const after = gate.after ?? "(any)";
2341
- const before = gate.before ?? "(any)";
2352
+ const after = gateAnchors(gate, "after").join(", ") || "(any)";
2353
+ const before = gateAnchors(gate, "before").join(", ") || "(any)";
2342
2354
  const onFail = gate.onFail ?? "halt";
2343
2355
  sections.push(`### gate \`${gate.id}\` — after \`${after}\`, before \`${before}\`, on-fail \`${onFail}\``);
2344
2356
  sections.push("");