agentopology 0.5.2 → 0.7.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 (141) 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 +219 -0
  5. package/dist/__tests__/e2e/compile.test.js.map +1 -0
  6. package/dist/__tests__/e2e/roundtrip.test.d.ts +16 -0
  7. package/dist/__tests__/e2e/roundtrip.test.d.ts.map +1 -0
  8. package/dist/__tests__/e2e/roundtrip.test.js +178 -0
  9. package/dist/__tests__/e2e/roundtrip.test.js.map +1 -0
  10. package/dist/__tests__/e2e/scaffold-owns.test.d.ts +10 -0
  11. package/dist/__tests__/e2e/scaffold-owns.test.d.ts.map +1 -0
  12. package/dist/__tests__/e2e/scaffold-owns.test.js +163 -0
  13. package/dist/__tests__/e2e/scaffold-owns.test.js.map +1 -0
  14. package/dist/__tests__/e2e/sync-cycle.test.d.ts +10 -0
  15. package/dist/__tests__/e2e/sync-cycle.test.d.ts.map +1 -0
  16. package/dist/__tests__/e2e/sync-cycle.test.js +49 -0
  17. package/dist/__tests__/e2e/sync-cycle.test.js.map +1 -0
  18. package/dist/__tests__/global-setup.d.ts +7 -0
  19. package/dist/__tests__/global-setup.d.ts.map +1 -0
  20. package/dist/__tests__/global-setup.js +16 -0
  21. package/dist/__tests__/global-setup.js.map +1 -0
  22. package/dist/__tests__/live/claude-code.live.test.d.ts +20 -0
  23. package/dist/__tests__/live/claude-code.live.test.d.ts.map +1 -0
  24. package/dist/__tests__/live/claude-code.live.test.js +198 -0
  25. package/dist/__tests__/live/claude-code.live.test.js.map +1 -0
  26. package/dist/__tests__/live/zz-debug.test.d.ts +2 -0
  27. package/dist/__tests__/live/zz-debug.test.d.ts.map +1 -0
  28. package/dist/__tests__/live/zz-debug.test.js +4 -0
  29. package/dist/__tests__/live/zz-debug.test.js.map +1 -0
  30. package/dist/__tests__/open-findings.test.d.ts +10 -0
  31. package/dist/__tests__/open-findings.test.d.ts.map +1 -0
  32. package/dist/__tests__/open-findings.test.js +15 -0
  33. package/dist/__tests__/open-findings.test.js.map +1 -0
  34. package/dist/__tests__/spec-testing-sync.test.d.ts +10 -0
  35. package/dist/__tests__/spec-testing-sync.test.d.ts.map +1 -0
  36. package/dist/__tests__/spec-testing-sync.test.js +57 -0
  37. package/dist/__tests__/spec-testing-sync.test.js.map +1 -0
  38. package/dist/bindings/__tests__/bindings.test.js +19 -19
  39. package/dist/bindings/__tests__/bindings.test.js.map +1 -1
  40. package/dist/bindings/__tests__/claude-code-hybrid.test.js +5 -5
  41. package/dist/bindings/__tests__/claude-code-hybrid.test.js.map +1 -1
  42. package/dist/bindings/__tests__/hook-command.test.d.ts +9 -0
  43. package/dist/bindings/__tests__/hook-command.test.d.ts.map +1 -0
  44. package/dist/bindings/__tests__/hook-command.test.js +88 -0
  45. package/dist/bindings/__tests__/hook-command.test.js.map +1 -0
  46. package/dist/bindings/__tests__/hybrid-e2e.test.js +1 -1
  47. package/dist/bindings/__tests__/hybrid-e2e.test.js.map +1 -1
  48. package/dist/bindings/__tests__/mod-plugin.test.d.ts +9 -0
  49. package/dist/bindings/__tests__/mod-plugin.test.d.ts.map +1 -0
  50. package/dist/bindings/__tests__/mod-plugin.test.js +164 -0
  51. package/dist/bindings/__tests__/mod-plugin.test.js.map +1 -0
  52. package/dist/bindings/claude-code.d.ts +50 -1
  53. package/dist/bindings/claude-code.d.ts.map +1 -1
  54. package/dist/bindings/claude-code.js +636 -231
  55. package/dist/bindings/claude-code.js.map +1 -1
  56. package/dist/bindings/index.d.ts +7 -0
  57. package/dist/bindings/index.d.ts.map +1 -1
  58. package/dist/bindings/index.js +26 -7
  59. package/dist/bindings/index.js.map +1 -1
  60. package/dist/bindings/lib/prompt-fence.d.ts +22 -0
  61. package/dist/bindings/lib/prompt-fence.d.ts.map +1 -0
  62. package/dist/bindings/lib/prompt-fence.js +29 -0
  63. package/dist/bindings/lib/prompt-fence.js.map +1 -0
  64. package/dist/bindings/types.d.ts +6 -0
  65. package/dist/bindings/types.d.ts.map +1 -1
  66. package/dist/bindings/types.js.map +1 -1
  67. package/dist/cli/__tests__/cli.test.d.ts +1 -1
  68. package/dist/cli/__tests__/cli.test.js +1 -1
  69. package/dist/cli/index.js +124 -76
  70. package/dist/cli/index.js.map +1 -1
  71. package/dist/docs/content.d.ts.map +1 -1
  72. package/dist/docs/content.js +39 -3
  73. package/dist/docs/content.js.map +1 -1
  74. package/dist/docs/teach.d.ts.map +1 -1
  75. package/dist/docs/teach.js +46 -0
  76. package/dist/docs/teach.js.map +1 -1
  77. package/dist/import/__tests__/import.test.js +21 -9
  78. package/dist/import/__tests__/import.test.js.map +1 -1
  79. package/dist/import/claude-code.d.ts.map +1 -1
  80. package/dist/import/claude-code.js +128 -44
  81. package/dist/import/claude-code.js.map +1 -1
  82. package/dist/import/serializer.d.ts.map +1 -1
  83. package/dist/import/serializer.js +10 -0
  84. package/dist/import/serializer.js.map +1 -1
  85. package/dist/parser/__tests__/mod-hooks.test.d.ts +10 -0
  86. package/dist/parser/__tests__/mod-hooks.test.d.ts.map +1 -0
  87. package/dist/parser/__tests__/mod-hooks.test.js +136 -0
  88. package/dist/parser/__tests__/mod-hooks.test.js.map +1 -0
  89. package/dist/parser/ast.d.ts +29 -2
  90. package/dist/parser/ast.d.ts.map +1 -1
  91. package/dist/parser/ast.js.map +1 -1
  92. package/dist/parser/index.d.ts +6 -1
  93. package/dist/parser/index.d.ts.map +1 -1
  94. package/dist/parser/index.js +102 -25
  95. package/dist/parser/index.js.map +1 -1
  96. package/dist/parser/paths.d.ts +15 -0
  97. package/dist/parser/paths.d.ts.map +1 -0
  98. package/dist/parser/paths.js +34 -0
  99. package/dist/parser/paths.js.map +1 -0
  100. package/dist/parser/run.d.ts +45 -0
  101. package/dist/parser/run.d.ts.map +1 -0
  102. package/dist/parser/run.js +91 -0
  103. package/dist/parser/run.js.map +1 -0
  104. package/dist/parser/validator.d.ts +12 -0
  105. package/dist/parser/validator.d.ts.map +1 -1
  106. package/dist/parser/validator.js +268 -2
  107. package/dist/parser/validator.js.map +1 -1
  108. package/dist/scaffold/__tests__/safety.test.d.ts +11 -0
  109. package/dist/scaffold/__tests__/safety.test.d.ts.map +1 -0
  110. package/dist/scaffold/__tests__/safety.test.js +184 -0
  111. package/dist/scaffold/__tests__/safety.test.js.map +1 -0
  112. package/dist/scaffold/__tests__/scaffold.test.js +2 -1
  113. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
  114. package/dist/scaffold/__tests__/settings-merge.test.js +40 -9
  115. package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -1
  116. package/dist/scaffold/diff.d.ts +17 -0
  117. package/dist/scaffold/diff.d.ts.map +1 -0
  118. package/dist/scaffold/diff.js +66 -0
  119. package/dist/scaffold/diff.js.map +1 -0
  120. package/dist/scaffold/incremental.d.ts +49 -13
  121. package/dist/scaffold/incremental.d.ts.map +1 -1
  122. package/dist/scaffold/incremental.js +235 -95
  123. package/dist/scaffold/incremental.js.map +1 -1
  124. package/dist/scaffold/manifest.d.ts +8 -1
  125. package/dist/scaffold/manifest.d.ts.map +1 -1
  126. package/dist/scaffold/manifest.js +32 -8
  127. package/dist/scaffold/manifest.js.map +1 -1
  128. package/dist/scaffold/merge.d.ts +15 -4
  129. package/dist/scaffold/merge.d.ts.map +1 -1
  130. package/dist/scaffold/merge.js +123 -18
  131. package/dist/scaffold/merge.js.map +1 -1
  132. package/dist/scaffold/types.d.ts +17 -0
  133. package/dist/scaffold/types.d.ts.map +1 -1
  134. package/dist/sync/index.d.ts +1 -0
  135. package/dist/sync/index.d.ts.map +1 -1
  136. package/dist/sync/index.js +40 -2
  137. package/dist/sync/index.js.map +1 -1
  138. package/package.json +1 -1
  139. package/spec/grammar.md +24 -5
  140. package/spec/testing.md +85 -0
  141. package/spec/validation.md +8 -2
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Which project-relative paths are safe to write or to name as a node's file.
3
+ *
4
+ * Shared by scaffold (every generated path) and the validator (every `file:`),
5
+ * so a path the language accepts is a path scaffold can honour.
6
+ *
7
+ * @module
8
+ */
9
+ /** One path segment a binding may produce. Junk like `E;`, `fail:`, `x)` fails. */
10
+ const SAFE_SEGMENT = /^[A-Za-z0-9_.@+-]+$/;
11
+ /**
12
+ * Why a generated relative path is unsafe to write, or null when it is fine.
13
+ *
14
+ * @param relPath - Path relative to the project root, as a binding emits it
15
+ */
16
+ export function unsafePathReason(relPath) {
17
+ if (relPath.trim() === "")
18
+ return "empty path";
19
+ if (relPath.endsWith("/"))
20
+ return "ends in '/' (no file name)";
21
+ if (relPath.startsWith("/"))
22
+ return "absolute path";
23
+ const segments = relPath.split("/");
24
+ for (const seg of segments) {
25
+ if (seg === "")
26
+ return "empty path segment";
27
+ if (seg === "..")
28
+ return "climbs out of the project ('..')";
29
+ if (!SAFE_SEGMENT.test(seg))
30
+ return `unsafe name "${seg}"`;
31
+ }
32
+ return null;
33
+ }
34
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/parser/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,mFAAmF;AACnF,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,YAAY,CAAC;IAC/C,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,4BAA4B,CAAC;IAC/D,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,eAAe,CAAC;IACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,GAAG,KAAK,EAAE;YAAE,OAAO,oBAAoB,CAAC;QAC5C,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,kCAAkC,CAAC;QAC5D,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,gBAAgB,GAAG,GAAG,CAAC;IAC7D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What a `run:` value is allowed to be: a command, not a sentence.
3
+ *
4
+ * A gate or hook `run` names something to execute. Bindings derive a script
5
+ * file name from it, so prose there became files named `E;`, `fail:` and
6
+ * `outputs.py)` — and a " / " in a sentence produced an empty name that
7
+ * crashed scaffold with EISDIR. Both the validator (V96) and every binding go
8
+ * through these two functions so they cannot disagree.
9
+ *
10
+ * @module
11
+ */
12
+ /**
13
+ * A `run:` split into the interpreter it names (if any), its script, and the
14
+ * script's arguments. The script is the first word, or the second when the
15
+ * first is an interpreter (`python3 x.py`, `/opt/homebrew/bin/python3 ./y.py`).
16
+ *
17
+ * The script name used to be the text after the LAST slash of the whole line,
18
+ * so an argument became the script: `python3 tool.py check show/SLUG` planned
19
+ * a stub named `SLUG`, and `pxframes.py gate OUT/frames-critic.json` one named
20
+ * `frames-critic.json`.
21
+ */
22
+ export declare function runParts(run: string): {
23
+ interpreter?: string;
24
+ script: string;
25
+ args: string[];
26
+ };
27
+ /**
28
+ * Whether a script word names a file in the project (run where it is) rather
29
+ * than one in the topology skill's scripts/ folder (scaffolded by basename).
30
+ * `./x`, `/x` and any path with a folder count, except the historical
31
+ * `scripts/x.sh`, which has always meant the skill's own scripts folder.
32
+ */
33
+ export declare function isProjectPath(script?: string): boolean;
34
+ /**
35
+ * The script file name a binding writes for `run`, or null when `run` does not
36
+ * name one: the basename of the script word (see runParts).
37
+ */
38
+ export declare function scriptNameFromRun(run: string | undefined): string | null;
39
+ /**
40
+ * Why `run` reads as prose rather than a command, or null when it looks like a
41
+ * command. Deliberately conservative: it flags only what a shell would reject
42
+ * or what no command contains, so real commands are never refused.
43
+ */
44
+ export declare function proseInRun(run: string): string | null;
45
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/parser/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAU9F;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,SAAK,GAAG,OAAO,CAGlD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBrD"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * What a `run:` value is allowed to be: a command, not a sentence.
3
+ *
4
+ * A gate or hook `run` names something to execute. Bindings derive a script
5
+ * file name from it, so prose there became files named `E;`, `fail:` and
6
+ * `outputs.py)` — and a " / " in a sentence produced an empty name that
7
+ * crashed scaffold with EISDIR. Both the validator (V96) and every binding go
8
+ * through these two functions so they cannot disagree.
9
+ *
10
+ * @module
11
+ */
12
+ /** One path-ish token: optional ./ ../ ~/ or / prefix, then safe characters (dotfolders such as .claude/ included). */
13
+ const EXECUTABLE = /^(?:\.{1,2}\/|~\/|\/)?[A-Za-z0-9_.][A-Za-z0-9_.\/@+-]*$/;
14
+ /** A file name a binding may write under its scripts/ directory. */
15
+ const SCRIPT_FILE = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
16
+ /** Words that run the script named after them. */
17
+ const INTERPRETER = /^(?:python[0-9.]*|bash|sh|zsh|node|bun|deno|ruby|perl|tsx)$/;
18
+ const SCRIPT_EXT = /\.(?:py|sh|ts|mjs|js)$/;
19
+ /**
20
+ * A `run:` split into the interpreter it names (if any), its script, and the
21
+ * script's arguments. The script is the first word, or the second when the
22
+ * first is an interpreter (`python3 x.py`, `/opt/homebrew/bin/python3 ./y.py`).
23
+ *
24
+ * The script name used to be the text after the LAST slash of the whole line,
25
+ * so an argument became the script: `python3 tool.py check show/SLUG` planned
26
+ * a stub named `SLUG`, and `pxframes.py gate OUT/frames-critic.json` one named
27
+ * `frames-critic.json`.
28
+ */
29
+ export function runParts(run) {
30
+ const words = run.trim().split(/\s+/);
31
+ const first = words[0] ?? "";
32
+ const named = words.length > 1 &&
33
+ !words[1].startsWith("-") && // `bash -c '...'` runs no script file
34
+ !SCRIPT_EXT.test(first) &&
35
+ (INTERPRETER.test(first.replace(/^.*\//, "")) || isProjectPath(words[1]));
36
+ const [script = "", ...args] = named ? words.slice(1) : words;
37
+ return { ...(named ? { interpreter: first } : {}), script, args };
38
+ }
39
+ /**
40
+ * Whether a script word names a file in the project (run where it is) rather
41
+ * than one in the topology skill's scripts/ folder (scaffolded by basename).
42
+ * `./x`, `/x` and any path with a folder count, except the historical
43
+ * `scripts/x.sh`, which has always meant the skill's own scripts folder.
44
+ */
45
+ export function isProjectPath(script = "") {
46
+ if (script.startsWith(".") || script.startsWith("/"))
47
+ return true;
48
+ return script.includes("/") && !script.startsWith("scripts/");
49
+ }
50
+ /**
51
+ * The script file name a binding writes for `run`, or null when `run` does not
52
+ * name one: the basename of the script word (see runParts).
53
+ */
54
+ export function scriptNameFromRun(run) {
55
+ if (!run)
56
+ return null;
57
+ // A slash standing alone is prose ("0.0 / 0.5 / 3.0 s"), never a path.
58
+ if (/\s\/\s/.test(run))
59
+ return null;
60
+ const name = runParts(run).script.replace(/^.*\//, "");
61
+ return SCRIPT_FILE.test(name) ? name : null;
62
+ }
63
+ /**
64
+ * Why `run` reads as prose rather than a command, or null when it looks like a
65
+ * command. Deliberately conservative: it flags only what a shell would reject
66
+ * or what no command contains, so real commands are never refused.
67
+ */
68
+ export function proseInRun(run) {
69
+ const text = run.trim();
70
+ if (text === "")
71
+ return "it is empty";
72
+ if (/\r|\n/.test(text))
73
+ return "it spans several lines";
74
+ const first = text.split(/\s+/)[0];
75
+ if (/[:;,]$/.test(first))
76
+ return `its first word "${first}" ends in punctuation`;
77
+ if (!EXECUTABLE.test(first))
78
+ return `its first word "${first}" is not a command or path`;
79
+ // Judge only what the shell would see unquoted.
80
+ const bare = text.replace(/'[^']*'|"[^"]*"/g, "''");
81
+ if (/[^\x00-\x7F]/.test(bare))
82
+ return "it contains non-ASCII text outside quotes";
83
+ if (/\s->\s|\s=>\s|\s<=\s|\s>=\s/.test(bare))
84
+ return "it contains an arrow or comparison (->, >=, <=)";
85
+ if (/(^|[^$])\(/.test(bare))
86
+ return "it contains an unquoted parenthesis";
87
+ if (scriptNameFromRun(text) === null)
88
+ return "no script file name can be derived from it";
89
+ return null;
90
+ }
91
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/parser/run.ts"],"names":[],"mappings":"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"}
@@ -44,5 +44,17 @@ export declare function findBackEdges(edges: EdgeDef[], nodeIds: Set<string>): E
44
44
  * @param ast - The parsed topology AST.
45
45
  * @returns An array of validation results. An empty array means no issues found.
46
46
  */
47
+ /**
48
+ * Engine events a `type: mod` hook may subscribe to (Claude Code function
49
+ * hooks, early access, CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1).
50
+ *
51
+ * PROVEN: a Mod was loaded and fired on these (Claude Code 2.1.283).
52
+ * SEEN: the name is in the 2.1.283 binary but no Mod has been observed on it,
53
+ * so V95 accepts it with a warning rather than an error.
54
+ */
55
+ export declare const MOD_EVENTS_PROVEN: readonly ["tool.call", "prompt.submit"];
56
+ export declare const MOD_EVENTS_SEEN: readonly ["prompt.context", "session.start", "session.end", "model.complete", "message.received"];
57
+ /** Hook types the grammar defines (`spec/grammar.md` §4.16). */
58
+ export declare const HOOK_TYPES: readonly ["command", "prompt", "mod"];
47
59
  export declare function validate(ast: TopologyAST): ValidationResult[];
48
60
  //# sourceMappingURL=validator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/parser/validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EACV,WAAW,EAKX,OAAO,EAkBR,MAAM,UAAU,CAAC;AAMlB,mCAAmC;AACnC,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAyMD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAyD/E;AAo0FD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,gBAAgB,EAAE,CAgG7D"}
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/parser/validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAEV,WAAW,EAKX,OAAO,EAkBR,MAAM,UAAU,CAAC;AAMlB,mCAAmC;AACnC,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAyMD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAyD/E;AAi8FD;;;;;GAKG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,yCAA0C,CAAC;AACzE,eAAO,MAAM,eAAe,mGAElB,CAAC;AAEX,gEAAgE;AAChE,eAAO,MAAM,UAAU,uCAAwC,CAAC;AAyHhE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,gBAAgB,EAAE,CAsG7D"}
@@ -20,6 +20,8 @@
20
20
  */
21
21
  import { MISSING } from "./index.js";
22
22
  import { gateAnchors } from "./ast.js";
23
+ import { proseInRun } from "./run.js";
24
+ import { unsafePathReason } from "./paths.js";
23
25
  // ---------------------------------------------------------------------------
24
26
  // Reserved keywords (from spec section 7)
25
27
  // ---------------------------------------------------------------------------
@@ -489,7 +491,7 @@ function v10PromptsExist(ast) {
489
491
  }
490
492
  return results;
491
493
  }
492
- /** V11: If agent A writes X and agent B reads X, a path A -> B must exist in flow. */
494
+ /** V11: If agent A writes X and agent B reads X, a path A -> B must exist in flow (unless either runs on demand). */
493
495
  function v11ReadWriteConsistency(ast) {
494
496
  const results = [];
495
497
  // Build a reachability map using BFS from each node
@@ -554,7 +556,16 @@ function v11ReadWriteConsistency(ast) {
554
556
  const keyWriters = writers.get(key);
555
557
  if (!keyWriters)
556
558
  continue; // no writer = could be external, skip
557
- const hasPath = keyWriters.some((writerId) => isReachable(writerId, node.id) ||
559
+ // An on-demand agent (invocation: manual) sits outside the flow by
560
+ // definition (V4), so no flow path can exist to or from it — the order
561
+ // is the user's. Demanding one made every hand-built project, whose
562
+ // subagents are all on demand, fail on import.
563
+ const manual = (id) => {
564
+ const n = ast.nodes.find((x) => x.id === id);
565
+ return !!n && isAgent(n) && n.invocation === "manual";
566
+ };
567
+ const hasPath = manual(node.id) || keyWriters.some((writerId) => manual(writerId) ||
568
+ isReachable(writerId, node.id) ||
558
569
  (coGroupMembers.get(node.id)?.has(writerId) ?? false));
559
570
  if (!hasPath) {
560
571
  results.push({
@@ -984,6 +995,121 @@ function v91RequiredFieldPlaceholder(ast) {
984
995
  * `handoff-overlap-empty` pre-flagged ambiguity; having it here makes it work
985
996
  * under `scaffold` too, and for anyone who never runs `plan`.
986
997
  */
998
+ /**
999
+ * V96: a `run:` value is a command, not a sentence.
1000
+ *
1001
+ * Gates and hooks compile to scripts named after their `run`. Prose there
1002
+ * passed validation, then scaffold wrote junk files named `E;` and `fail:`
1003
+ * and crashed with EISDIR on a " / " — after it had already written other
1004
+ * files. The explanation belongs in `description`; `run` keeps the command.
1005
+ */
1006
+ function v96RunIsCommand(ast) {
1007
+ const results = [];
1008
+ const check = (owner, run, nodeId) => {
1009
+ if (run === undefined)
1010
+ return;
1011
+ const why = proseInRun(run);
1012
+ if (!why)
1013
+ return;
1014
+ results.push({
1015
+ rule: "V96",
1016
+ level: "error",
1017
+ message: `${owner} "run" is not a command — ${why}. Keep only the command in ` +
1018
+ `"run" (e.g. run: "scripts/check.sh") and move the explanation into "description"`,
1019
+ ...(nodeId ? { node: nodeId, line: lookupLine(ast, nodeId) } : {}),
1020
+ });
1021
+ };
1022
+ // A prompt hook's run: is the prompt itself, so prose is what it should be.
1023
+ const isCommand = (hook) => hook.type !== "prompt";
1024
+ for (const node of ast.nodes) {
1025
+ if (isGate(node))
1026
+ check(`Gate "${node.id}"`, node.run, node.id);
1027
+ if (node.type === "agent") {
1028
+ for (const hook of (node.hooks ?? []).filter(isCommand)) {
1029
+ check(`Hook "${hook.name}" on agent "${node.id}"`, hook.run, node.id);
1030
+ }
1031
+ }
1032
+ }
1033
+ for (const hook of (ast.hooks ?? []).filter(isCommand))
1034
+ check(`Hook "${hook.name}"`, hook.run);
1035
+ return results;
1036
+ }
1037
+ /**
1038
+ * V97: a `file:` is a safe project-relative path.
1039
+ *
1040
+ * `file:` marks a node's internals as hand-written at that path; scaffold
1041
+ * never writes it. A path outside the project ('..', absolute) or with an
1042
+ * unsafe name could not be honoured, so it is refused here, not at scaffold.
1043
+ */
1044
+ function v97FileIsProjectPath(ast) {
1045
+ const results = [];
1046
+ const check = (owner, file, nodeId) => {
1047
+ if (file === undefined)
1048
+ return;
1049
+ const why = unsafePathReason(file.replace(/^\.\//, ""));
1050
+ if (!why)
1051
+ return;
1052
+ results.push({
1053
+ rule: "V97",
1054
+ level: "error",
1055
+ message: `${owner} "file" is not a project-relative path — ${why}. Name the file from the project root, e.g. file: ".claude/agents/reviewer.md"`,
1056
+ ...(nodeId ? { node: nodeId, line: lookupLine(ast, nodeId) } : {}),
1057
+ });
1058
+ };
1059
+ for (const node of ast.nodes)
1060
+ check(`Node "${node.id}"`, node.file, node.id);
1061
+ for (const skill of ast.skills ?? [])
1062
+ check(`Skill "${skill.id}"`, skill.file);
1063
+ for (const trigger of ast.triggers ?? [])
1064
+ check(`Command "${trigger.name}"`, trigger.file);
1065
+ return results;
1066
+ }
1067
+ /**
1068
+ * V98: a block that belongs inside a container is not written beside it.
1069
+ *
1070
+ * `gate x { }` outside `gates { }` (likewise hook, tool, job, command, level)
1071
+ * was dropped by the parser with no message. Collected at parse time.
1072
+ */
1073
+ function v98BlockInsideItsContainer(ast) {
1074
+ return ast._misplacedBlockErrors ?? [];
1075
+ }
1076
+ /**
1077
+ * V99: an agent's `skills:` and `mcp-servers:`, and a skill's `agent:`, name
1078
+ * something this topology declares.
1079
+ *
1080
+ * A misspelled name compiled to a reference Claude Code could not resolve,
1081
+ * with no message. A warning, not an error: the name may be real outside the
1082
+ * topology (a skill on disk, a user-level MCP server, a built-in agent).
1083
+ */
1084
+ const BUILT_IN_AGENTS = new Set(["general-purpose", "Explore", "Plan"]);
1085
+ function v99ReferencesDeclared(ast) {
1086
+ const results = [];
1087
+ const skills = new Set([ast.topology.name, ...(ast.skills ?? []).map((s) => s.id)]);
1088
+ const servers = new Set([...Object.keys(ast.mcpServers ?? {}), ...(ast.stores ?? []).map((s) => s.id)]);
1089
+ const agents = new Set(ast.nodes.filter((n) => n.type === "agent" || n.type === "group").map((n) => n.id));
1090
+ const warn = (message, nodeId) => results.push({ rule: "V99", level: "warning", message, ...(nodeId ? { node: nodeId, line: lookupLine(ast, nodeId) } : {}) });
1091
+ for (const node of ast.nodes) {
1092
+ if (node.type !== "agent")
1093
+ continue;
1094
+ const agent = node;
1095
+ for (const skill of agent.skills ?? []) {
1096
+ if (!skills.has(skill)) {
1097
+ 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);
1098
+ }
1099
+ }
1100
+ for (const server of agent.mcpServers ?? []) {
1101
+ if (!servers.has(server)) {
1102
+ warn(`Agent "${agent.id}" uses MCP server "${server}", which mcp-servers { } does not declare — it resolves only if configured outside this project`, agent.id);
1103
+ }
1104
+ }
1105
+ }
1106
+ for (const skill of ast.skills ?? []) {
1107
+ if (skill.agent && !agents.has(skill.agent) && !BUILT_IN_AGENTS.has(skill.agent)) {
1108
+ 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(", ")})`);
1109
+ }
1110
+ }
1111
+ return results;
1112
+ }
987
1113
  function v93EdgeHandoffDeclared(ast) {
988
1114
  const results = [];
989
1115
  const byId = new Map(ast.nodes.map((n) => [n.id, n]));
@@ -2939,6 +3065,140 @@ function v88CustodyRefsValid(ast) {
2939
3065
  * @param ast - The parsed topology AST.
2940
3066
  * @returns An array of validation results. An empty array means no issues found.
2941
3067
  */
3068
+ /**
3069
+ * Engine events a `type: mod` hook may subscribe to (Claude Code function
3070
+ * hooks, early access, CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1).
3071
+ *
3072
+ * PROVEN: a Mod was loaded and fired on these (Claude Code 2.1.283).
3073
+ * SEEN: the name is in the 2.1.283 binary but no Mod has been observed on it,
3074
+ * so V95 accepts it with a warning rather than an error.
3075
+ */
3076
+ export const MOD_EVENTS_PROVEN = ["tool.call", "prompt.submit"];
3077
+ export const MOD_EVENTS_SEEN = [
3078
+ "prompt.context", "session.start", "session.end", "model.complete", "message.received",
3079
+ ];
3080
+ /** Hook types the grammar defines (`spec/grammar.md` §4.16). */
3081
+ export const HOOK_TYPES = ["command", "prompt", "mod"];
3082
+ /** Every hook in the topology — global first, then per-agent — with its owner. */
3083
+ function allHooks(ast) {
3084
+ const out = ast.hooks.map((hook) => ({ hook, where: hook.name }));
3085
+ for (const n of ast.nodes) {
3086
+ if (n.type !== "agent" || !n.hooks)
3087
+ continue;
3088
+ for (const hook of n.hooks)
3089
+ out.push({ hook, where: `${n.id}.${hook.name}`, agent: n.id });
3090
+ }
3091
+ return out;
3092
+ }
3093
+ /**
3094
+ * V94: a hook's `type` must be one the grammar defines.
3095
+ *
3096
+ * `type: mod` passed silently before the grammar knew it, and the claude-code
3097
+ * binding wrote it into settings.json as a bash command. A misspelt type
3098
+ * (`type: comand`) did the same.
3099
+ */
3100
+ function v94HookTypeKnown(ast) {
3101
+ const results = [];
3102
+ for (const { hook, where, agent } of allHooks(ast)) {
3103
+ const type = hook.type ?? "command";
3104
+ if (!HOOK_TYPES.includes(type)) {
3105
+ results.push({
3106
+ rule: "V94",
3107
+ level: "error",
3108
+ message: `Hook "${where}" has type "${type}" — a hook is one of: ${HOOK_TYPES.join(", ")}. ` +
3109
+ `Use "mod" for a TypeScript module that runs inside the engine.`,
3110
+ node: where,
3111
+ });
3112
+ continue;
3113
+ }
3114
+ if (type === "mod" && agent) {
3115
+ results.push({
3116
+ rule: "V94",
3117
+ level: "error",
3118
+ message: `Hook "${where}" is a mod inside an agent — a Mod runs in the engine for the whole session; declare it in the topology's hooks block.`,
3119
+ node: where,
3120
+ });
3121
+ }
3122
+ if (hook.flag !== undefined && type !== "mod") {
3123
+ results.push({
3124
+ rule: "V94",
3125
+ level: "error",
3126
+ message: `Hook "${where}" sets flag: but is type "${type}" — flag: is the env switch of a type: mod hook.`,
3127
+ node: where,
3128
+ });
3129
+ }
3130
+ else if (hook.flag !== undefined && !/^[A-Z_][A-Z0-9_]*$/.test(hook.flag)) {
3131
+ results.push({
3132
+ rule: "V94",
3133
+ level: "error",
3134
+ message: `Hook "${where}" has flag "${hook.flag}" — a flag is an env variable name (e.g. MY_MOD_ON).`,
3135
+ node: where,
3136
+ });
3137
+ }
3138
+ }
3139
+ return results;
3140
+ }
3141
+ /**
3142
+ * V95: a hook's `on:` must be an event its type can subscribe to.
3143
+ *
3144
+ * A mod subscribes to a dotted engine event (`tool.call`); a command or prompt
3145
+ * hook to a PascalCase lifecycle event (`PreToolUse`, spec §4.16: "any
3146
+ * identifier in PascalCase"). Mixing them is a hook that never fires. A value
3147
+ * with whitespace is left to V90 (a swallowed field), not reported twice.
3148
+ */
3149
+ function v95HookEventKnown(ast) {
3150
+ const results = [];
3151
+ const proven = MOD_EVENTS_PROVEN;
3152
+ const seen = MOD_EVENTS_SEEN;
3153
+ for (const { hook, where } of allHooks(ast)) {
3154
+ const type = hook.type ?? "command";
3155
+ const on = hook.on.trim();
3156
+ if (/\s/.test(on))
3157
+ continue; // V90
3158
+ if (!on) {
3159
+ results.push({ rule: "V95", level: "error", message: `Hook "${where}" has no on: — name the event it fires on.`, node: where });
3160
+ continue;
3161
+ }
3162
+ if (type === "mod") {
3163
+ if (!hook.run) {
3164
+ results.push({ rule: "V95", level: "error", message: `Mod "${where}" has no run: — name its module (e.g. run: "hooks/redact.ts").`, node: where });
3165
+ }
3166
+ if (proven.includes(on))
3167
+ continue;
3168
+ if (seen.includes(on)) {
3169
+ results.push({
3170
+ rule: "V95",
3171
+ level: "warning",
3172
+ message: `Mod "${where}" subscribes to "${on}" — the engine names this event, but no Mod has been observed firing on it. Proven: ${proven.join(", ")}.`,
3173
+ node: where,
3174
+ });
3175
+ continue;
3176
+ }
3177
+ const hint = /^[A-Z]/.test(on)
3178
+ ? ` "${on}" is a command-hook event; a mod subscribes to an engine event.`
3179
+ : "";
3180
+ results.push({
3181
+ rule: "V95",
3182
+ level: "error",
3183
+ message: `Mod "${where}" subscribes to unknown engine event "${on}".${hint} Engine events: ${[...proven, ...seen].join(", ")}.`,
3184
+ node: where,
3185
+ });
3186
+ continue;
3187
+ }
3188
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(on)) {
3189
+ const hint = proven.includes(on) || seen.includes(on)
3190
+ ? ` "${on}" is an engine event — set type: mod to subscribe a module to it.`
3191
+ : "";
3192
+ results.push({
3193
+ rule: "V95",
3194
+ level: "error",
3195
+ message: `Hook "${where}" (type ${type}) fires on "${on}" — a ${type} hook takes a PascalCase event (e.g. PreToolUse, AgentStop).${hint}`,
3196
+ node: where,
3197
+ });
3198
+ }
3199
+ }
3200
+ return results;
3201
+ }
2942
3202
  export function validate(ast) {
2943
3203
  return [
2944
3204
  ...v1UniqueNames(ast),
@@ -2970,6 +3230,12 @@ export function validate(ast) {
2970
3230
  ...v91RequiredFieldPlaceholder(ast),
2971
3231
  ...v92EdgeTwoBrackets(ast),
2972
3232
  ...v93EdgeHandoffDeclared(ast),
3233
+ ...v94HookTypeKnown(ast),
3234
+ ...v95HookEventKnown(ast),
3235
+ ...v96RunIsCommand(ast),
3236
+ ...v97FileIsProjectPath(ast),
3237
+ ...v98BlockInsideItsContainer(ast),
3238
+ ...v99ReferencesDeclared(ast),
2973
3239
  ...v25BounceBackAdvisory(ast),
2974
3240
  ...v26ActionKindEnum(ast),
2975
3241
  ...v27AgentPermissionsEnum(ast),