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.
- package/README.md +5 -5
- package/dist/bindings/__tests__/reasoning-effort.test.d.ts +11 -0
- package/dist/bindings/__tests__/reasoning-effort.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/reasoning-effort.test.js +93 -0
- package/dist/bindings/__tests__/reasoning-effort.test.js.map +1 -0
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +27 -15
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/claude-workflow.d.ts.map +1 -1
- package/dist/bindings/claude-workflow.js +11 -3
- package/dist/bindings/claude-workflow.js.map +1 -1
- package/dist/bindings/codex.d.ts.map +1 -1
- package/dist/bindings/codex.js +8 -3
- package/dist/bindings/codex.js.map +1 -1
- package/dist/bindings/copilot-cli.d.ts.map +1 -1
- package/dist/bindings/copilot-cli.js +7 -6
- package/dist/bindings/copilot-cli.js.map +1 -1
- package/dist/bindings/cursor.d.ts.map +1 -1
- package/dist/bindings/cursor.js +14 -7
- package/dist/bindings/cursor.js.map +1 -1
- package/dist/bindings/gemini-cli.d.ts.map +1 -1
- package/dist/bindings/gemini-cli.js +3 -2
- package/dist/bindings/gemini-cli.js.map +1 -1
- package/dist/bindings/kiro.d.ts.map +1 -1
- package/dist/bindings/kiro.js +5 -4
- package/dist/bindings/kiro.js.map +1 -1
- package/dist/bindings/openclaw.d.ts.map +1 -1
- package/dist/bindings/openclaw.js +4 -3
- package/dist/bindings/openclaw.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +45 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/__tests__/teach.test.d.ts +10 -0
- package/dist/docs/__tests__/teach.test.d.ts.map +1 -0
- package/dist/docs/__tests__/teach.test.js +109 -0
- package/dist/docs/__tests__/teach.test.js.map +1 -0
- package/dist/docs/content.js +1 -1
- package/dist/docs/index.d.ts +10 -0
- package/dist/docs/index.d.ts.map +1 -1
- package/dist/docs/index.js +91 -0
- package/dist/docs/index.js.map +1 -1
- package/dist/docs/teach.d.ts +45 -0
- package/dist/docs/teach.d.ts.map +1 -0
- package/dist/docs/teach.js +200 -0
- package/dist/docs/teach.js.map +1 -0
- package/dist/exporters/markdown.d.ts.map +1 -1
- package/dist/exporters/markdown.js +3 -2
- package/dist/exporters/markdown.js.map +1 -1
- package/dist/import/serializer.d.ts.map +1 -1
- package/dist/import/serializer.js +11 -2
- package/dist/import/serializer.js.map +1 -1
- package/dist/parser/__tests__/gate-anchors.test.d.ts +18 -0
- package/dist/parser/__tests__/gate-anchors.test.d.ts.map +1 -0
- package/dist/parser/__tests__/gate-anchors.test.js +183 -0
- package/dist/parser/__tests__/gate-anchors.test.js.map +1 -0
- package/dist/parser/__tests__/parser.test.js +92 -0
- package/dist/parser/__tests__/parser.test.js.map +1 -1
- package/dist/parser/ast.d.ts +38 -4
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js +26 -1
- package/dist/parser/ast.js.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +11 -2
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +71 -7
- package/dist/parser/validator.js.map +1 -1
- package/dist/plan/__tests__/evidence.test.d.ts +13 -0
- package/dist/plan/__tests__/evidence.test.d.ts.map +1 -0
- package/dist/plan/__tests__/evidence.test.js +107 -0
- package/dist/plan/__tests__/evidence.test.js.map +1 -0
- package/dist/plan/brief.d.ts +23 -3
- package/dist/plan/brief.d.ts.map +1 -1
- package/dist/plan/brief.js +25 -3
- package/dist/plan/brief.js.map +1 -1
- package/dist/plan/render.d.ts.map +1 -1
- package/dist/plan/render.js +24 -5
- package/dist/plan/render.js.map +1 -1
- package/dist/render/ascii.d.ts.map +1 -1
- package/dist/render/ascii.js +25 -3
- package/dist/render/ascii.js.map +1 -1
- package/dist/resolve/order.d.ts.map +1 -1
- package/dist/resolve/order.js +29 -16
- package/dist/resolve/order.js.map +1 -1
- package/dist/visualizer/index.d.ts.map +1 -1
- package/dist/visualizer/index.js +17 -4
- package/dist/visualizer/index.js.map +1 -1
- package/examples/data-processing.at +1 -1
- package/examples/simple-pipeline.at +1 -1
- package/package.json +1 -1
- package/spec/reserved-keywords.md +1 -1
- 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) │ │
|
|
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
|
|
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 (
|
|
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 |
|
|
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 (
|
|
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;
|
|
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.
|
|
1240
|
+
sections.push(`After: ${gateAnchors(gate, "after").join(", ")}`);
|
|
1240
1241
|
if (gate.before)
|
|
1241
|
-
sections.push(`Before: ${gate.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
1696
|
-
"# Gate runs before: " + (gate.
|
|
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
|
-
|
|
1941
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
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(
|
|
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.
|
|
2341
|
-
const before = gate.
|
|
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("");
|