@archmax-ai/harness 0.0.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 (351) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/agent.d.ts +174 -0
  4. package/dist/agent.js +140 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/assembly/compose.d.ts +170 -0
  7. package/dist/assembly/compose.js +391 -0
  8. package/dist/assembly/compose.js.map +1 -0
  9. package/dist/assembly/delegation-registry.d.ts +5 -0
  10. package/dist/assembly/delegation-registry.js +118 -0
  11. package/dist/assembly/delegation-registry.js.map +1 -0
  12. package/dist/assembly/index.d.ts +192 -0
  13. package/dist/assembly/index.js +265 -0
  14. package/dist/assembly/index.js.map +1 -0
  15. package/dist/assembly/plain.d.ts +7 -0
  16. package/dist/assembly/plain.js +41 -0
  17. package/dist/assembly/plain.js.map +1 -0
  18. package/dist/authoring-skill/archmax-harness/README.md +76 -0
  19. package/dist/authoring-skill/archmax-harness/SKILL.md +241 -0
  20. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +626 -0
  21. package/dist/authoring-skill/archmax-harness/references/cases.md +41 -0
  22. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +51 -0
  23. package/dist/authoring-skill/archmax-harness/references/hook-and-test-scripts.md +1070 -0
  24. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +82 -0
  25. package/dist/authoring-skill/archmax-harness/references/wiring.md +101 -0
  26. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +1631 -0
  27. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +241 -0
  28. package/dist/behaviour/support.d.ts +232 -0
  29. package/dist/behaviour/support.js +339 -0
  30. package/dist/behaviour/support.js.map +1 -0
  31. package/dist/cli/banner.d.ts +11 -0
  32. package/dist/cli/banner.js +29 -0
  33. package/dist/cli/banner.js.map +1 -0
  34. package/dist/cli/bootstrap.d.ts +33 -0
  35. package/dist/cli/bootstrap.js +153 -0
  36. package/dist/cli/bootstrap.js.map +1 -0
  37. package/dist/cli/command.d.ts +39 -0
  38. package/dist/cli/command.js +97 -0
  39. package/dist/cli/command.js.map +1 -0
  40. package/dist/cli/state-flow.d.ts +46 -0
  41. package/dist/cli/state-flow.js +268 -0
  42. package/dist/cli/state-flow.js.map +1 -0
  43. package/dist/cli/style.d.ts +41 -0
  44. package/dist/cli/style.js +64 -0
  45. package/dist/cli/style.js.map +1 -0
  46. package/dist/cli/turn.d.ts +41 -0
  47. package/dist/cli/turn.js +161 -0
  48. package/dist/cli/turn.js.map +1 -0
  49. package/dist/cli.d.ts +10 -0
  50. package/dist/cli.js +426 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/core/code-description.d.ts +22 -0
  53. package/dist/core/code-description.js +38 -0
  54. package/dist/core/code-description.js.map +1 -0
  55. package/dist/core/deepagents.d.ts +183 -0
  56. package/dist/core/deepagents.js +128 -0
  57. package/dist/core/deepagents.js.map +1 -0
  58. package/dist/core/events.d.ts +350 -0
  59. package/dist/core/events.js +205 -0
  60. package/dist/core/events.js.map +1 -0
  61. package/dist/core/frontmatter.d.ts +15 -0
  62. package/dist/core/frontmatter.js +32 -0
  63. package/dist/core/frontmatter.js.map +1 -0
  64. package/dist/core/listing-redact.d.ts +37 -0
  65. package/dist/core/listing-redact.js +88 -0
  66. package/dist/core/listing-redact.js.map +1 -0
  67. package/dist/core/match.d.ts +26 -0
  68. package/dist/core/match.js +75 -0
  69. package/dist/core/match.js.map +1 -0
  70. package/dist/core/message-readers.d.ts +84 -0
  71. package/dist/core/message-readers.js +159 -0
  72. package/dist/core/message-readers.js.map +1 -0
  73. package/dist/core/messages.d.ts +49 -0
  74. package/dist/core/messages.js +85 -0
  75. package/dist/core/messages.js.map +1 -0
  76. package/dist/core/mount-prompt.d.ts +28 -0
  77. package/dist/core/mount-prompt.js +55 -0
  78. package/dist/core/mount-prompt.js.map +1 -0
  79. package/dist/core/mount-redact.d.ts +28 -0
  80. package/dist/core/mount-redact.js +32 -0
  81. package/dist/core/mount-redact.js.map +1 -0
  82. package/dist/core/mounts.d.ts +88 -0
  83. package/dist/core/mounts.js +147 -0
  84. package/dist/core/mounts.js.map +1 -0
  85. package/dist/core/path-mapping.d.ts +52 -0
  86. package/dist/core/path-mapping.js +113 -0
  87. package/dist/core/path-mapping.js.map +1 -0
  88. package/dist/core/platform-prompt.generated.d.ts +2 -0
  89. package/dist/core/platform-prompt.generated.js +4 -0
  90. package/dist/core/platform-prompt.generated.js.map +1 -0
  91. package/dist/core/prompt.d.ts +42 -0
  92. package/dist/core/prompt.js +75 -0
  93. package/dist/core/prompt.js.map +1 -0
  94. package/dist/core/session-id.d.ts +38 -0
  95. package/dist/core/session-id.js +61 -0
  96. package/dist/core/session-id.js.map +1 -0
  97. package/dist/core/session-store.d.ts +85 -0
  98. package/dist/core/session-store.js +149 -0
  99. package/dist/core/session-store.js.map +1 -0
  100. package/dist/core/session-zone.d.ts +54 -0
  101. package/dist/core/session-zone.js +82 -0
  102. package/dist/core/session-zone.js.map +1 -0
  103. package/dist/core/skill-prompt.d.ts +31 -0
  104. package/dist/core/skill-prompt.js +54 -0
  105. package/dist/core/skill-prompt.js.map +1 -0
  106. package/dist/core/skill-redact.d.ts +15 -0
  107. package/dist/core/skill-redact.js +27 -0
  108. package/dist/core/skill-redact.js.map +1 -0
  109. package/dist/core/skills.d.ts +172 -0
  110. package/dist/core/skills.js +214 -0
  111. package/dist/core/skills.js.map +1 -0
  112. package/dist/core/tool-mocks.d.ts +23 -0
  113. package/dist/core/tool-mocks.js +39 -0
  114. package/dist/core/tool-mocks.js.map +1 -0
  115. package/dist/core/tool-telemetry.d.ts +16 -0
  116. package/dist/core/tool-telemetry.js +40 -0
  117. package/dist/core/tool-telemetry.js.map +1 -0
  118. package/dist/core/usage.d.ts +114 -0
  119. package/dist/core/usage.js +226 -0
  120. package/dist/core/usage.js.map +1 -0
  121. package/dist/core/workspace-context.d.ts +136 -0
  122. package/dist/core/workspace-context.js +178 -0
  123. package/dist/core/workspace-context.js.map +1 -0
  124. package/dist/core/workspace-prompt.d.ts +7 -0
  125. package/dist/core/workspace-prompt.js +102 -0
  126. package/dist/core/workspace-prompt.js.map +1 -0
  127. package/dist/core/workspace-router.d.ts +61 -0
  128. package/dist/core/workspace-router.js +145 -0
  129. package/dist/core/workspace-router.js.map +1 -0
  130. package/dist/core/workspace.d.ts +85 -0
  131. package/dist/core/workspace.js +193 -0
  132. package/dist/core/workspace.js.map +1 -0
  133. package/dist/core/zones.d.ts +181 -0
  134. package/dist/core/zones.js +202 -0
  135. package/dist/core/zones.js.map +1 -0
  136. package/dist/env.d.ts +99 -0
  137. package/dist/env.js +271 -0
  138. package/dist/env.js.map +1 -0
  139. package/dist/index.d.ts +265 -0
  140. package/dist/index.js +197 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/kernel/kernel.d.ts +192 -0
  143. package/dist/kernel/kernel.js +751 -0
  144. package/dist/kernel/kernel.js.map +1 -0
  145. package/dist/lifecycle/hook-shape.d.ts +44 -0
  146. package/dist/lifecycle/hook-shape.js +66 -0
  147. package/dist/lifecycle/hook-shape.js.map +1 -0
  148. package/dist/lifecycle/runner.d.ts +177 -0
  149. package/dist/lifecycle/runner.js +372 -0
  150. package/dist/lifecycle/runner.js.map +1 -0
  151. package/dist/lifecycle/transcript.d.ts +27 -0
  152. package/dist/lifecycle/transcript.js +43 -0
  153. package/dist/lifecycle/transcript.js.map +1 -0
  154. package/dist/machine/allow.d.ts +65 -0
  155. package/dist/machine/allow.js +107 -0
  156. package/dist/machine/allow.js.map +1 -0
  157. package/dist/machine/delegation.d.ts +9 -0
  158. package/dist/machine/delegation.js +10 -0
  159. package/dist/machine/delegation.js.map +1 -0
  160. package/dist/machine/diagnostic.d.ts +14 -0
  161. package/dist/machine/diagnostic.js +2 -0
  162. package/dist/machine/diagnostic.js.map +1 -0
  163. package/dist/machine/lint-spec.d.ts +4 -0
  164. package/dist/machine/lint-spec.js +266 -0
  165. package/dist/machine/lint-spec.js.map +1 -0
  166. package/dist/machine/load-spec.d.ts +90 -0
  167. package/dist/machine/load-spec.js +120 -0
  168. package/dist/machine/load-spec.js.map +1 -0
  169. package/dist/machine/machine.d.ts +273 -0
  170. package/dist/machine/machine.js +551 -0
  171. package/dist/machine/machine.js.map +1 -0
  172. package/dist/machine/mount-grants.d.ts +25 -0
  173. package/dist/machine/mount-grants.js +24 -0
  174. package/dist/machine/mount-grants.js.map +1 -0
  175. package/dist/machine/slug.d.ts +14 -0
  176. package/dist/machine/slug.js +17 -0
  177. package/dist/machine/slug.js.map +1 -0
  178. package/dist/machine/spec-schema.d.ts +662 -0
  179. package/dist/machine/spec-schema.js +717 -0
  180. package/dist/machine/spec-schema.js.map +1 -0
  181. package/dist/machine/tool-names.d.ts +98 -0
  182. package/dist/machine/tool-names.js +144 -0
  183. package/dist/machine/tool-names.js.map +1 -0
  184. package/dist/machine/triggers.d.ts +114 -0
  185. package/dist/machine/triggers.js +165 -0
  186. package/dist/machine/triggers.js.map +1 -0
  187. package/dist/machine/types.d.ts +186 -0
  188. package/dist/machine/types.js +12 -0
  189. package/dist/machine/types.js.map +1 -0
  190. package/dist/machine/validate-spec.d.ts +42 -0
  191. package/dist/machine/validate-spec.js +29 -0
  192. package/dist/machine/validate-spec.js.map +1 -0
  193. package/dist/machine/variables.d.ts +196 -0
  194. package/dist/machine/variables.js +415 -0
  195. package/dist/machine/variables.js.map +1 -0
  196. package/dist/public/cli.d.ts +15 -0
  197. package/dist/public/cli.js +14 -0
  198. package/dist/public/cli.js.map +1 -0
  199. package/dist/public/messages.d.ts +33 -0
  200. package/dist/public/messages.js +33 -0
  201. package/dist/public/messages.js.map +1 -0
  202. package/dist/public/sandbox.d.ts +99 -0
  203. package/dist/public/sandbox.js +32 -0
  204. package/dist/public/sandbox.js.map +1 -0
  205. package/dist/public/spec.d.ts +147 -0
  206. package/dist/public/spec.js +149 -0
  207. package/dist/public/spec.js.map +1 -0
  208. package/dist/public/testing.d.ts +71 -0
  209. package/dist/public/testing.js +39 -0
  210. package/dist/public/testing.js.map +1 -0
  211. package/dist/rubrics/rubrics.d.ts +135 -0
  212. package/dist/rubrics/rubrics.js +145 -0
  213. package/dist/rubrics/rubrics.js.map +1 -0
  214. package/dist/runtime/contract.d.ts +73 -0
  215. package/dist/runtime/contract.js +96 -0
  216. package/dist/runtime/contract.js.map +1 -0
  217. package/dist/sandbox/assets/parts/core.js +22 -0
  218. package/dist/sandbox/assets/parts/lifecycle-hook.js +47 -0
  219. package/dist/sandbox/bridge.d.ts +16 -0
  220. package/dist/sandbox/bridge.js +95 -0
  221. package/dist/sandbox/bridge.js.map +1 -0
  222. package/dist/sandbox/executor.d.ts +46 -0
  223. package/dist/sandbox/executor.js +119 -0
  224. package/dist/sandbox/executor.js.map +1 -0
  225. package/dist/sandbox/imports.d.ts +33 -0
  226. package/dist/sandbox/imports.js +67 -0
  227. package/dist/sandbox/imports.js.map +1 -0
  228. package/dist/sandbox/prelude.d.ts +12 -0
  229. package/dist/sandbox/prelude.js +51 -0
  230. package/dist/sandbox/prelude.js.map +1 -0
  231. package/dist/sandbox/ptc-gateway.d.ts +104 -0
  232. package/dist/sandbox/ptc-gateway.js +167 -0
  233. package/dist/sandbox/ptc-gateway.js.map +1 -0
  234. package/dist/sandbox/runtime.d.ts +59 -0
  235. package/dist/sandbox/runtime.js +51 -0
  236. package/dist/sandbox/runtime.js.map +1 -0
  237. package/dist/sandbox/tools.d.ts +34 -0
  238. package/dist/sandbox/tools.js +147 -0
  239. package/dist/sandbox/tools.js.map +1 -0
  240. package/dist/sessions/operations.d.ts +79 -0
  241. package/dist/sessions/operations.js +121 -0
  242. package/dist/sessions/operations.js.map +1 -0
  243. package/dist/sessions/resolve.d.ts +97 -0
  244. package/dist/sessions/resolve.js +145 -0
  245. package/dist/sessions/resolve.js.map +1 -0
  246. package/dist/sessions/resume.d.ts +242 -0
  247. package/dist/sessions/resume.js +288 -0
  248. package/dist/sessions/resume.js.map +1 -0
  249. package/dist/sessions/scope.d.ts +99 -0
  250. package/dist/sessions/scope.js +145 -0
  251. package/dist/sessions/scope.js.map +1 -0
  252. package/dist/sessions/summary.d.ts +65 -0
  253. package/dist/sessions/summary.js +56 -0
  254. package/dist/sessions/summary.js.map +1 -0
  255. package/dist/testing/assertions.d.ts +30 -0
  256. package/dist/testing/assertions.js +231 -0
  257. package/dist/testing/assertions.js.map +1 -0
  258. package/dist/testing/case-document.d.ts +141 -0
  259. package/dist/testing/case-document.js +51 -0
  260. package/dist/testing/case-document.js.map +1 -0
  261. package/dist/testing/case-schema.d.ts +29 -0
  262. package/dist/testing/case-schema.js +519 -0
  263. package/dist/testing/case-schema.js.map +1 -0
  264. package/dist/testing/discovery.d.ts +10 -0
  265. package/dist/testing/discovery.js +24 -0
  266. package/dist/testing/discovery.js.map +1 -0
  267. package/dist/testing/driver.d.ts +78 -0
  268. package/dist/testing/driver.js +117 -0
  269. package/dist/testing/driver.js.map +1 -0
  270. package/dist/testing/grade.d.ts +116 -0
  271. package/dist/testing/grade.js +261 -0
  272. package/dist/testing/grade.js.map +1 -0
  273. package/dist/testing/interpreter.d.ts +55 -0
  274. package/dist/testing/interpreter.js +103 -0
  275. package/dist/testing/interpreter.js.map +1 -0
  276. package/dist/testing/mock-middleware.d.ts +5 -0
  277. package/dist/testing/mock-middleware.js +33 -0
  278. package/dist/testing/mock-middleware.js.map +1 -0
  279. package/dist/testing/runner.d.ts +205 -0
  280. package/dist/testing/runner.js +266 -0
  281. package/dist/testing/runner.js.map +1 -0
  282. package/dist/testing/target.d.ts +27 -0
  283. package/dist/testing/target.js +21 -0
  284. package/dist/testing/target.js.map +1 -0
  285. package/dist/validate/validate.d.ts +41 -0
  286. package/dist/validate/validate.js +669 -0
  287. package/dist/validate/validate.js.map +1 -0
  288. package/dist/workflow/agent-tools.d.ts +10 -0
  289. package/dist/workflow/agent-tools.js +10 -0
  290. package/dist/workflow/agent-tools.js.map +1 -0
  291. package/dist/workflow/checkpointer.d.ts +52 -0
  292. package/dist/workflow/checkpointer.js +200 -0
  293. package/dist/workflow/checkpointer.js.map +1 -0
  294. package/dist/workflow/control-tools.d.ts +174 -0
  295. package/dist/workflow/control-tools.js +468 -0
  296. package/dist/workflow/control-tools.js.map +1 -0
  297. package/dist/workflow/governance.d.ts +104 -0
  298. package/dist/workflow/governance.js +546 -0
  299. package/dist/workflow/governance.js.map +1 -0
  300. package/dist/workflow/middleware.d.ts +85 -0
  301. package/dist/workflow/middleware.js +283 -0
  302. package/dist/workflow/middleware.js.map +1 -0
  303. package/dist/workflow/on-error.d.ts +25 -0
  304. package/dist/workflow/on-error.js +58 -0
  305. package/dist/workflow/on-error.js.map +1 -0
  306. package/dist/workflow/parks.d.ts +46 -0
  307. package/dist/workflow/parks.js +513 -0
  308. package/dist/workflow/parks.js.map +1 -0
  309. package/dist/workflow/paths.d.ts +67 -0
  310. package/dist/workflow/paths.js +108 -0
  311. package/dist/workflow/paths.js.map +1 -0
  312. package/dist/workflow/prompt-cache.d.ts +70 -0
  313. package/dist/workflow/prompt-cache.js +103 -0
  314. package/dist/workflow/prompt-cache.js.map +1 -0
  315. package/dist/workflow/prompt-pruning.d.ts +20 -0
  316. package/dist/workflow/prompt-pruning.js +83 -0
  317. package/dist/workflow/prompt-pruning.js.map +1 -0
  318. package/dist/workflow/render-prompt.d.ts +45 -0
  319. package/dist/workflow/render-prompt.js +140 -0
  320. package/dist/workflow/render-prompt.js.map +1 -0
  321. package/dist/workflow/runtime-config.d.ts +7 -0
  322. package/dist/workflow/runtime-config.js +9 -0
  323. package/dist/workflow/runtime-config.js.map +1 -0
  324. package/dist/workflow/session-artifacts.d.ts +90 -0
  325. package/dist/workflow/session-artifacts.js +134 -0
  326. package/dist/workflow/session-artifacts.js.map +1 -0
  327. package/dist/workflow/session-view.d.ts +31 -0
  328. package/dist/workflow/session-view.js +94 -0
  329. package/dist/workflow/session-view.js.map +1 -0
  330. package/dist/workflow/snapshot.d.ts +14 -0
  331. package/dist/workflow/snapshot.js +97 -0
  332. package/dist/workflow/snapshot.js.map +1 -0
  333. package/dist/workflow/state.d.ts +347 -0
  334. package/dist/workflow/state.js +322 -0
  335. package/dist/workflow/state.js.map +1 -0
  336. package/dist/workflow/sub-workflow.d.ts +174 -0
  337. package/dist/workflow/sub-workflow.js +392 -0
  338. package/dist/workflow/sub-workflow.js.map +1 -0
  339. package/dist/workflow/tool-service.d.ts +48 -0
  340. package/dist/workflow/tool-service.js +291 -0
  341. package/dist/workflow/tool-service.js.map +1 -0
  342. package/dist/workflow/turn-boundary.d.ts +39 -0
  343. package/dist/workflow/turn-boundary.js +128 -0
  344. package/dist/workflow/turn-boundary.js.map +1 -0
  345. package/dist/workflow/turn-runner.d.ts +40 -0
  346. package/dist/workflow/turn-runner.js +122 -0
  347. package/dist/workflow/turn-runner.js.map +1 -0
  348. package/dist/workflow/workflow-tools.d.ts +46 -0
  349. package/dist/workflow/workflow-tools.js +90 -0
  350. package/dist/workflow/workflow-tools.js.map +1 -0
  351. package/package.json +119 -0
@@ -0,0 +1,717 @@
1
+ /**
2
+ * The complete `workflow.yaml` schema, in Zod — the one source for every rule a
3
+ * single document has to satisfy. Two layers: the **shape** (`machineSpecSchema`,
4
+ * every object `.strict()` so a misspelled key is an error naming the key) and
5
+ * the **cross-references inside the document** (`refineSpec`), which run once
6
+ * the shape holds. `types.ts` infers its types from here; `load-spec.ts` and
7
+ * `validate` both parse through `parseMachineSpec`. Warnings live in `lint-spec.ts`.
8
+ */
9
+ import { z } from "zod";
10
+ import { normalizeAllowEntry } from "./allow.js";
11
+ import { normalizeHooks } from "../lifecycle/hook-shape.js";
12
+ import { isSlug } from "./slug.js";
13
+ import { MANUAL_TRIGGER, parseSessionPath, stateTriggerIds } from "./triggers.js";
14
+ import { parseReferences, referenceError, TITLE_VARIABLE, TRIGGER_VARIABLE, VARIABLE_NAME_PATTERN, } from "./variables.js";
15
+ // --- Shared scalars ------------------------------------------------------------
16
+ const NON_EMPTY = "must be a non-empty string";
17
+ const nonEmptyString = z
18
+ .string({ error: NON_EMPTY })
19
+ .refine((value) => value.trim() !== "", NON_EMPTY);
20
+ const POSITIVE = "must be a positive number";
21
+ const positiveInt = z.number({ error: POSITIVE }).int(POSITIVE).positive(POSITIVE);
22
+ /**
23
+ * A model id, and only an id: the one spelling of "which model runs this" in the
24
+ * schema, shared by `settings.model`, a state's `model` and a rubric's. Never an
25
+ * endpoint, credentials or sampling knobs — a declared id is resolved over the
26
+ * endpoint and credentials the assembly is already configured with, so an object
27
+ * value is refused with the sentence that says where those belong. The id itself
28
+ * is opaque: only the endpoint can judge it, so a new model needs no release.
29
+ */
30
+ const modelId = z
31
+ .string({
32
+ error: "must be a model id string (e.g. 'gpt-5-mini'). Only the id is declared here — the " +
33
+ "endpoint, credentials and sampling (temperature, max tokens) are the assembly's " +
34
+ "configuration (ARCHMAX_* or a host's modelFactory), not a workflow's.",
35
+ })
36
+ .refine((value) => value.trim() !== "", NON_EMPTY);
37
+ /** A run-variable name: snake_case, never dotted. */
38
+ const variableName = z
39
+ .string({ error: (issue) => `'${String(issue.input)}' is not a valid run-variable name` })
40
+ .regex(VARIABLE_NAME_PATTERN, {
41
+ error: (issue) => `'${String(issue.input)}' is not a valid run-variable name. Use lowercase letters, ` +
42
+ `digits and underscores, starting with a letter (e.g. 'order_id').`,
43
+ });
44
+ /** A skill slug: a bundle directory name, never a path or glob. */
45
+ const skillSlug = z
46
+ .string({ error: (issue) => `is not a skill slug: ${JSON.stringify(issue.input)}` })
47
+ .refine(isSlug, {
48
+ error: (issue) => `'${String(issue.input)}' is not a skill slug. Entries are bundle directory names in ` +
49
+ `kebab-case (e.g. 'order-data') — never paths or globs; scope paths within an enabled ` +
50
+ `skill with a 'tools.allow' entry instead.`,
51
+ });
52
+ /**
53
+ * A mount name: a key of the host's mount table with its slashes stripped
54
+ * (`reference`, `catalogs/eu`, `AGENTS.md`) — the token a `mounts` list governs
55
+ * by. Interior separators are kept, so a nested key is nameable; a glob, a
56
+ * leading slash and a `..` segment are refused, because a mount list names
57
+ * mounts and a path pattern is what `tools.allow` takes.
58
+ */
59
+ const mountName = z
60
+ .string({ error: (issue) => `is not a mount name: ${JSON.stringify(issue.input)}` })
61
+ .refine((value) => isMountName(value), {
62
+ error: (issue) => `'${String(issue.input)}' is not a mount name. Entries are the keys of the workspace's ` +
63
+ `mount table with their slashes stripped (e.g. 'reference' or 'catalogs/eu') — never ` +
64
+ `globs, leading slashes or '..' segments; scope paths within an enabled mount with a ` +
65
+ `'tools.allow' entry instead.`,
66
+ });
67
+ /**
68
+ * How a state may use a mount it is given: `read` refuses writes there,
69
+ * `read_write` permits them where the host declared the mount writable.
70
+ *
71
+ * The host's `readOnly` posture is a **ceiling**, not a default to argue with: a
72
+ * mount served read-only stays read-only wherever it appears, and `read_write`
73
+ * on one is inert (reported by `validate`). What this key adds is the other
74
+ * direction — narrowing a writable mount to reads in the states that only need
75
+ * to read it — which is the direction every level of this schema may move.
76
+ */
77
+ export const mountAccessSchema = z.enum(["read", "read_write"], {
78
+ error: (issue) => `unknown mount access '${String(issue.input)}' (expected 'read' or 'read_write')`,
79
+ });
80
+ /**
81
+ * One entry of a mounts **grant** list: a bare mount name, taking the write
82
+ * posture the host declared, or a mapping naming the access this level gives.
83
+ * A `forbid` entry is a name only — a denial is total, so it has no access to
84
+ * qualify.
85
+ */
86
+ const MOUNT_GRANT_ENTRY = "must be a mount name, or '{ mount: <name>, access: read | read_write }' to name the access " +
87
+ "this level gives";
88
+ export const mountGrantEntrySchema = z.union([
89
+ mountName,
90
+ z
91
+ .object({
92
+ mount: mountName,
93
+ access: mountAccessSchema.optional(),
94
+ })
95
+ .strict(),
96
+ ], {
97
+ error: (issue) => `${MOUNT_GRANT_ENTRY}; got ${JSON.stringify(issue.input)}`,
98
+ });
99
+ const MOUNT_GRANT_LIST = "must be a list of mount names, each optionally as " +
100
+ "'{ mount: <name>, access: read | read_write }'";
101
+ /** Whether a string is a well-formed mount name. */
102
+ function isMountName(value) {
103
+ if (value === "" || value !== value.trim())
104
+ return false;
105
+ if (/[*?[\]{}]/.test(value))
106
+ return false;
107
+ if (value.startsWith("/") || value.endsWith("/"))
108
+ return false;
109
+ return value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
110
+ }
111
+ /** A dotted session path over the run's variables (`triggers.-1.conversationId`). */
112
+ const sessionPath = z
113
+ .string({
114
+ error: "must be a dotted path string (e.g. 'conversation_id' or 'triggers.-1.conversationId').",
115
+ })
116
+ .superRefine((value, ctx) => {
117
+ const parsed = parseSessionPath(value);
118
+ if (parsed.error)
119
+ ctx.addIssue({ code: "custom", message: parsed.error });
120
+ });
121
+ /**
122
+ * @see SpecMetadata
123
+ *
124
+ * One loose, named slot for host data the runtime never reads (an authoring UI's
125
+ * canvas state, a node's position) — rather than a `.catchall()`, which would
126
+ * switch off the strictness this file exists for. Accepted at three positions:
127
+ * the spec root, a state, and a rubric declaration.
128
+ */
129
+ export const specMetadataSchema = z.looseObject({});
130
+ /**
131
+ * @see RubricDeclaration
132
+ *
133
+ * A grading rubric: the standard a state's exit is measured against, declared
134
+ * **inline on the hook that applies it**. `instructions` is the only required
135
+ * key — the same word a state uses for the prose telling a model what to do.
136
+ *
137
+ * There is deliberately no name: a rubric is not a first-order item a hook
138
+ * points at, it is part of the state's own declaration, so reading the state
139
+ * tells you the whole standard without a lookup. The cost is duplication when
140
+ * two states want the same grader, which is accepted: a state is legible on its
141
+ * own, and nothing silently changes what one state grades against by editing
142
+ * something elsewhere. There is no `title` either (nothing routes to a rubric)
143
+ * and no `description`; a host label goes in `metadata`.
144
+ */
145
+ export const rubricDeclarationSchema = z
146
+ .object({
147
+ instructions: nonEmptyString,
148
+ max_iterations: z.number().int().min(0).optional(),
149
+ model: modelId.optional(),
150
+ metadata: specMetadataSchema.optional(),
151
+ })
152
+ .strict();
153
+ // --- Hooks ---------------------------------------------------------------------
154
+ /** Keys a hook may carry *alongside* its kind, which therefore never name one. */
155
+ const RESERVED_HOOK_KEYS = new Set(["max_iterations"]);
156
+ const HOOK_SCRIPT_PATH = /^hooks\/[^/].*\.(js|mjs)$/;
157
+ /**
158
+ * Whether a hook script path stays inside the workflow's `hooks/` directory.
159
+ * Checked by segment, not only by prefix: `hooks/../escape.js` satisfies a naive
160
+ * "starts with hooks/" rule while resolving outside it.
161
+ */
162
+ function isConfinedHookScript(path) {
163
+ return HOOK_SCRIPT_PATH.test(path) && !path.split("/").includes("..");
164
+ }
165
+ const hookScriptPath = z
166
+ .string()
167
+ .refine(isConfinedHookScript, "must be a workflow-relative path inside 'hooks/' ending in .js or .mjs, with " +
168
+ "no '..' segment (e.g. 'hooks/check-requester.js'). Runtime scripts the agent " +
169
+ "runs with archmax_run live in a skill bundle instead.");
170
+ /**
171
+ * The one wording for a hook that is not a usable tagged object.
172
+ *
173
+ * Deliberately free of per-key special cases: a retired spelling
174
+ * (`subagent`, `max_corrections`) is reported as the unrecognized key it is, the
175
+ * way a typo is, so the schema carries no memory of every name it ever had.
176
+ */
177
+ function describeHookInput(input) {
178
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
179
+ return "must be a { script } or { rubric } entry";
180
+ }
181
+ const record = input;
182
+ const kinds = Object.keys(record).filter((key) => !RESERVED_HOOK_KEYS.has(key));
183
+ if (kinds.length !== 1) {
184
+ return "must be a single-key tagged object (plus an optional 'max_iterations' sidecar)";
185
+ }
186
+ return ("must be a hook: { script: 'hooks/<file>.js' }, { rubric: { instructions: … } }, or " +
187
+ "{ <custom-kind>: '<target>' } for an executor registered at assembly");
188
+ }
189
+ /**
190
+ * One lifecycle hook. The built-in kinds are strict; the custom-kind branch (a
191
+ * record of strings, for an executor registered at assembly) is fenced so it
192
+ * cannot rescue a malformed built-in — `{ script: <bad path> }` must fail the
193
+ * script branch and stay failed.
194
+ */
195
+ export const hookSchema = z.union([
196
+ z
197
+ .object({ script: hookScriptPath, max_iterations: z.number().int().min(0).optional() })
198
+ .strict(),
199
+ // A rubric hook carries the whole grader inline, so its budget is declared
200
+ // inside the declaration rather than as a sidecar beside it: there is only
201
+ // one place to put it, which is the point of inlining.
202
+ z.object({ rubric: rubricDeclarationSchema }).strict(),
203
+ z
204
+ .record(z.string(), z.string())
205
+ .refine((value) => !("script" in value) &&
206
+ !("rubric" in value) &&
207
+ Object.keys(value).filter((key) => !RESERVED_HOOK_KEYS.has(key)).length === 1, { error: (issue) => describeHookInput(issue.input) }),
208
+ ], { error: (issue) => describeHookInput(issue.input) });
209
+ /** @see HookSpec */
210
+ export const hookSpecSchema = z.union([hookSchema, z.array(hookSchema)], {
211
+ error: (issue) => Array.isArray(issue.input)
212
+ ? "every entry must be a { script } or { rubric } entry"
213
+ : describeHookInput(issue.input),
214
+ });
215
+ // --- States --------------------------------------------------------------------
216
+ /** @see TransitionType */
217
+ export const transitionTypeSchema = z.enum(["approve", "reject", "refine", "none"], {
218
+ error: (issue) => `unknown type '${String(issue.input)}' (expected approve|reject|refine|none)`,
219
+ });
220
+ /**
221
+ * Why a `description` is required rather than advisory: the agent is disclosed
222
+ * the active state's edges and nothing else of the graph, so this string is the
223
+ * whole of what it knows about where an edge leads. A blank one hands it a bare
224
+ * slug and asks it to route — an unnavigable graph, which is worse to warn about
225
+ * than to refuse. One message covers missing, empty, blank and mistyped, and the
226
+ * issue path (`states.<slug>.transitions.<index>.description`) names the edge.
227
+ */
228
+ const TRANSITION_DESCRIPTION = `must be a non-empty string: it is the only thing the agent is told about this edge — no ` +
229
+ `attribute of the target state is disclosed to it — so an undescribed edge cannot be chosen. ` +
230
+ `Say when to take it (e.g. 'Refunds over $50'), not where it goes.`;
231
+ /** @see MachineTransition */
232
+ export const machineTransitionSchema = z
233
+ .object({
234
+ to: nonEmptyString,
235
+ description: z
236
+ .string({ error: TRANSITION_DESCRIPTION })
237
+ .refine((value) => value.trim() !== "", TRANSITION_DESCRIPTION),
238
+ type: transitionTypeSchema.optional(),
239
+ })
240
+ .strict();
241
+ /** @see StateBudget */
242
+ export const stateBudgetSchema = z
243
+ .object({
244
+ maxTurns: positiveInt.optional(),
245
+ timeoutMs: positiveInt.optional(),
246
+ maxParks: positiveInt.optional(),
247
+ })
248
+ .strict();
249
+ /** @see AllowEntry */
250
+ export const allowEntrySchema = z.union([
251
+ z.string(),
252
+ z
253
+ .object({
254
+ tool: z.string({ error: "must name a tool" }),
255
+ args: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
256
+ paths: z.union([z.string(), z.array(z.string())]).optional(),
257
+ connection: z.string().optional(),
258
+ // Free-form: records, for the host that authored the entry, where the tool
259
+ // came from. Nothing in the SDK reads it.
260
+ source: z.string().optional(),
261
+ })
262
+ .strict(),
263
+ ]);
264
+ /**
265
+ * @see ForbidEntry
266
+ *
267
+ * The same entry grammar as {@link allowEntrySchema} — one derivation, one
268
+ * matcher, one guard validation — with one addition the deny side needs and the
269
+ * grant side must not have: the tool may be `*`, meaning **every** tool. That is
270
+ * how a path is denied across the board (`{ tool: "*", paths: [logs/**] }`),
271
+ * where a wildcard can only ever close the surface further. A `*` in an `allow`
272
+ * list is a document-level error (`refineSpec`): allow-only governance means an
273
+ * author names what a state may do.
274
+ */
275
+ export const forbidEntrySchema = allowEntrySchema;
276
+ /** The tool name that denies every tool. Accepted in a forbid entry only. */
277
+ export const FORBID_ANY_TOOL = "*";
278
+ /**
279
+ * @see ToolsBlock — a **state's** tools block.
280
+ *
281
+ * `allow` adds to the workflow's `tools.allow_always`; `forbid` denies here,
282
+ * beating every grant that would otherwise reach this state — the workflow's
283
+ * always-on grant, this state's own `allow`, and the always-on surface alike.
284
+ * Deny beats allow, and no narrower level widens a denial.
285
+ */
286
+ export const toolsBlockSchema = z
287
+ .object({
288
+ allow: z.array(allowEntrySchema).optional(),
289
+ forbid: z.array(forbidEntrySchema).optional(),
290
+ })
291
+ .strict();
292
+ /**
293
+ * @see SkillsBlock — a **state's** skills block, and only a state's: the spec
294
+ * root has its own shape ({@link workflowSkillsConfigSchema}), the way `tools`
295
+ * already splits its two positions.
296
+ *
297
+ * `allow` is optional rather than defaulted because `skills:` is itself optional:
298
+ * an absent list and `allow: []` mean the same thing — nothing this state adds —
299
+ * so the default belongs where the set is resolved
300
+ * (`WorkflowMachine.enabledSkills`), not in the parse, where a whole absent block
301
+ * would slip past it anyway.
302
+ */
303
+ export const skillsBlockSchema = z
304
+ .object({
305
+ allow: z.array(skillSlug, { error: "must be a list of skill slugs" }).optional(),
306
+ forbid: z.array(skillSlug, { error: "must be a list of skill slugs" }).optional(),
307
+ })
308
+ .strict();
309
+ /**
310
+ * @see MountsBlock — a **state's** mounts block, and only a state's: the spec
311
+ * root has its own shape ({@link workflowMountsConfigSchema}), the way `tools`
312
+ * and `skills` already split their two positions.
313
+ *
314
+ * The block governs the mount names the **host** declared governed
315
+ * (`MountSpec.governed`); an ungoverned mount is visible in every state without
316
+ * a grant, and `forbid` is what subtracts it. Precedence is the one rule every
317
+ * capability here follows: `allow` adds to the root's `allow_always`, and a
318
+ * denial from either level beats every grant.
319
+ *
320
+ * `allow` is optional rather than defaulted for the same reason `skills.allow`
321
+ * is: an absent list and `allow: []` both add nothing, so the default belongs
322
+ * where the set is resolved (`WorkflowMachine.enabledMounts`).
323
+ */
324
+ export const mountsBlockSchema = z
325
+ .object({
326
+ allow: z.array(mountGrantEntrySchema, { error: MOUNT_GRANT_LIST }).optional(),
327
+ forbid: z.array(mountName, { error: "must be a list of mount names" }).optional(),
328
+ })
329
+ .strict();
330
+ /** @see StateType */
331
+ export const stateTypeSchema = z.enum(["agent", "human"], {
332
+ error: (issue) => `unknown state type '${String(issue.input)}' (expected 'agent' or 'human')`,
333
+ });
334
+ // --- Triggers ------------------------------------------------------------------
335
+ /** One half of a trigger's signature: a list of distinct run-variable names. */
336
+ function signatureSchema(key) {
337
+ return z
338
+ .array(variableName, {
339
+ error: `must be a list of run-variable names (e.g. '${key}: [order_id]'). A signature names ` +
340
+ `the contract only — what a variable holds belongs in the state instructions that set it.`,
341
+ })
342
+ .superRefine((names, ctx) => {
343
+ const seen = new Set();
344
+ names.forEach((name, index) => {
345
+ if (seen.has(name)) {
346
+ ctx.addIssue({ code: "custom", path: [index], message: `'${name}' is listed more than once.` });
347
+ }
348
+ seen.add(name);
349
+ // Two reserved names, refused as returns for opposite reasons — one the
350
+ // runtime always sets, one that must not cross a sub-run boundary upward.
351
+ if (key === "returns" && name === TRIGGER_VARIABLE) {
352
+ ctx.addIssue({
353
+ code: "custom",
354
+ path: [index],
355
+ message: `'${name}' is the built-in trigger variable, set by the harness and locked, so a ` +
356
+ `run cannot declare it as something it produces.`,
357
+ });
358
+ }
359
+ if (key === "returns" && name === TITLE_VARIABLE) {
360
+ ctx.addIssue({
361
+ code: "custom",
362
+ path: [index],
363
+ message: `'${name}' is the built-in title variable, naming the task a run is doing. A ` +
364
+ `sub-run's title describes the sub-run, so returning it would silently rename the ` +
365
+ `task of whoever called it.`,
366
+ });
367
+ }
368
+ });
369
+ });
370
+ }
371
+ /**
372
+ * The two keys a declaration refuses rather than preserves. Every other key a
373
+ * host writes rides along untouched, but these two would be *wiring*: an `entry`
374
+ * naming another state, a `name` naming another id. A loose object would keep
375
+ * them, warn once, and start the run somewhere else — so each is an error
376
+ * naming the one spelling that is left.
377
+ */
378
+ const REFUSED_TRIGGER_KEYS = {
379
+ entry: `declares 'entry'. The state a trigger's declaration sits on IS its entry state, so there ` +
380
+ `is nothing left to name: move the declaration under the 'triggers:' of the state the ` +
381
+ `trigger enters, and drop the key.`,
382
+ name: `declares 'name'. The key a declaration sits under IS the trigger id, so a 'name' would be ` +
383
+ `a second one: rename the key instead, and drop it.`,
384
+ };
385
+ /**
386
+ * @see TriggerDeclaration
387
+ *
388
+ * Loose, not strict: a declaration is host-extensible — it may carry keys the
389
+ * SDK preserves without acting on, and `lint-spec.ts` warns about them. The two
390
+ * in {@link REFUSED_TRIGGER_KEYS} are errors instead.
391
+ */
392
+ export const triggerDeclarationSchema = z
393
+ .looseObject({
394
+ session: sessionPath.optional(),
395
+ message: z
396
+ .union([z.literal(false), sessionPath], {
397
+ error: "must be a dotted path string (e.g. 'triggers.-1.text'), or false when firings carry no message.",
398
+ })
399
+ .optional(),
400
+ connection: z
401
+ .string({
402
+ error: "must be a non-empty string naming an access connection in the host's environment.",
403
+ })
404
+ .refine((value) => value.trim() !== "", "must be a non-empty string naming an access connection in the host's environment.")
405
+ .optional(),
406
+ requires: signatureSchema("requires").optional(),
407
+ returns: signatureSchema("returns").optional(),
408
+ })
409
+ .check((ctx) => {
410
+ for (const [key, message] of Object.entries(REFUSED_TRIGGER_KEYS)) {
411
+ if (!Object.hasOwn(ctx.value, key))
412
+ continue;
413
+ ctx.issues.push({ code: "custom", message, path: [key], input: ctx.value });
414
+ }
415
+ });
416
+ /**
417
+ * A state's `triggers:` — the one place a trigger is declared. The key is the
418
+ * trigger id, the state the mapping sits on is that trigger's entry state, and
419
+ * the value is the declaration. A `null` value (`manual:` with nothing under
420
+ * it) declares the id and nothing more.
421
+ */
422
+ export const stateTriggersSchema = z.record(nonEmptyString, triggerDeclarationSchema.nullable(), {
423
+ error: `must be a mapping of trigger id to its declaration (e.g. 'triggers: { ${MANUAL_TRIGGER}: }', ` +
424
+ `or an id with a 'session' path under it).`,
425
+ });
426
+ /** @see MachineState */
427
+ export const machineStateSchema = z
428
+ .object({
429
+ type: stateTypeSchema.optional(),
430
+ title: nonEmptyString.optional(),
431
+ instructions: z.string().optional(),
432
+ summary: z.string().optional(),
433
+ evidence: z.array(z.string()).optional(),
434
+ approvers: z.array(z.string()).optional(),
435
+ triggers: stateTriggersSchema.optional(),
436
+ before: hookSpecSchema.optional(),
437
+ after: hookSpecSchema.optional(),
438
+ budget: stateBudgetSchema.optional(),
439
+ model: modelId.optional(),
440
+ on_error: nonEmptyString.optional(),
441
+ tools: toolsBlockSchema.optional(),
442
+ skills: skillsBlockSchema.optional(),
443
+ mounts: mountsBlockSchema.optional(),
444
+ requires: z.array(variableName, { error: "must be a list of run-variable names" }).optional(),
445
+ metadata: specMetadataSchema.optional(),
446
+ transitions: z.array(machineTransitionSchema).optional(),
447
+ })
448
+ .strict();
449
+ /**
450
+ * A state as authored: the mapping above, or `null` for a bare `done:` terminal
451
+ * state. Preprocessed rather than unioned with `z.null()`, so a strict-key
452
+ * failure keeps its own message instead of a union's "Invalid input".
453
+ */
454
+ const authoredStateSchema = z.preprocess((value) => value ?? {}, machineStateSchema);
455
+ // --- Root blocks ---------------------------------------------------------------
456
+ /** @see WorkflowToolsConfig */
457
+ /**
458
+ * @see WorkflowToolsConfig — the spec root's tools block.
459
+ *
460
+ * `allow_always` is granted in every state; `forbid_always` is denied in every
461
+ * state and is **absolute** — no state grant, argument guard or consumer rule
462
+ * reaches past it, and a sub-workflow inherits it from every ancestor. The
463
+ * superseded `policy` block denies alongside it: denials union, because a denial
464
+ * can only ever close a surface further.
465
+ */
466
+ export const workflowToolsConfigSchema = z
467
+ .object({
468
+ allow_always: z.array(allowEntrySchema).optional(),
469
+ forbid_always: z.array(forbidEntrySchema).optional(),
470
+ })
471
+ .strict();
472
+ /**
473
+ * @see WorkflowSkillsConfig — the spec root's skills block.
474
+ *
475
+ * `allow_always` lists the slugs enabled in **every** state; a state's
476
+ * `skills.allow` adds to it, exactly as `tools.allow` adds to
477
+ * `tools.allow_always`. `forbid_always` denies its slugs in every state and
478
+ * beats every grant, the way `tools.forbid_always` does. There is one model:
479
+ * the ceiling `allow` this key used to accept is retired, so a root `allow` is
480
+ * an unrecognized key.
481
+ */
482
+ export const workflowSkillsConfigSchema = z
483
+ .object({
484
+ allow_always: z.array(skillSlug, { error: "must be a list of skill slugs" }).optional(),
485
+ forbid_always: z.array(skillSlug, { error: "must be a list of skill slugs" }).optional(),
486
+ })
487
+ .strict();
488
+ /**
489
+ * @see WorkflowMountsConfig — the spec root's mounts block.
490
+ *
491
+ * `allow_always` lists the governed mounts enabled in **every** state; a state's
492
+ * `mounts.allow` adds to it, exactly as `skills.allow` adds to
493
+ * `skills.allow_always`. `forbid_always` denies its names in every state —
494
+ * governed or not — and beats every grant, and binds every descendant session.
495
+ * There is one model: a root `allow` is an unrecognized key.
496
+ */
497
+ export const workflowMountsConfigSchema = z
498
+ .object({
499
+ allow_always: z.array(mountGrantEntrySchema, { error: MOUNT_GRANT_LIST }).optional(),
500
+ forbid_always: z.array(mountName, { error: "must be a list of mount names" }).optional(),
501
+ })
502
+ .strict();
503
+ /** @see MachinePromptCacheSettings */
504
+ export const machinePromptCacheSettingsSchema = z
505
+ .object({ enabled: z.boolean().optional(), ttl: z.string().optional() })
506
+ .strict();
507
+ /** @see MachineSettings — delegation bounds are the dispatcher's configuration, not settings here. */
508
+ export const machineSettingsSchema = z
509
+ .object({
510
+ model: modelId.optional(),
511
+ timeoutMs: z.number().int().positive().optional(),
512
+ memoryLimitBytes: z.number().int().positive().optional(),
513
+ maxPtcCalls: z.number().int().min(0).nullable().optional(),
514
+ maxResultChars: z.number().int().positive().optional(),
515
+ prompt_cache: machinePromptCacheSettingsSchema.optional(),
516
+ })
517
+ .strict();
518
+ /** @see WorkflowTestsConfig */
519
+ export const workflowTestsConfigSchema = z
520
+ .object({
521
+ maxConcurrency: z.number().int().positive().optional(),
522
+ caseTimeoutMs: z.number().int().positive().optional(),
523
+ judge: z
524
+ .object({
525
+ model: z.string().optional(),
526
+ /** Grader model construction options, applied over the environment's. */
527
+ modelOptions: z
528
+ .object({
529
+ temperature: z.number().min(0).optional(),
530
+ maxTokens: z.number().int().positive().optional(),
531
+ })
532
+ .strict()
533
+ .optional(),
534
+ })
535
+ .strict()
536
+ .optional(),
537
+ })
538
+ .strict();
539
+ /** @see RuntimeMetadata */
540
+ export const runtimeMetadataSchema = z
541
+ .object({
542
+ engine: z.string().optional(),
543
+ version: z.union([z.string(), z.number()]).optional(),
544
+ })
545
+ .strict();
546
+ /** @see SpecExtensions */
547
+ export const specExtensionsSchema = z
548
+ .object({ hooks: z.array(z.string()).optional() })
549
+ .strict();
550
+ /** The whole of `workflow.yaml`. @see MachineSpec */
551
+ export const machineSpecSchema = z
552
+ .object({
553
+ title: z.string().optional(),
554
+ instructions: nonEmptyString.optional(),
555
+ disabled: z
556
+ .boolean({
557
+ error: (issue) => `Machine spec 'disabled' must be a boolean (true or false); got ` +
558
+ `${JSON.stringify(issue.input)}, which the runtime reads fail-closed as disabled — so ` +
559
+ `this workflow starts no runs. Write 'disabled: true' to mean it, or remove the key.`,
560
+ })
561
+ .optional(),
562
+ runtime: runtimeMetadataSchema.optional(),
563
+ settings: machineSettingsSchema.optional(),
564
+ tools: workflowToolsConfigSchema.optional(),
565
+ skills: workflowSkillsConfigSchema.optional(),
566
+ mounts: workflowMountsConfigSchema.optional(),
567
+ tests: workflowTestsConfigSchema.optional(),
568
+ extensions: specExtensionsSchema.optional(),
569
+ metadata: specMetadataSchema.optional(),
570
+ states: z.record(z.string(), authoredStateSchema, {
571
+ error: "Machine spec is missing or has empty 'states'",
572
+ }),
573
+ })
574
+ .strict();
575
+ /**
576
+ * The rules that need the whole document and that the runtime cannot run
577
+ * without: every reference resolves, the start wiring is unambiguous, guards
578
+ * reference variables by a readable name. Runs after the shape has been
579
+ * accepted. Defects the runtime tolerates are `lint-spec.ts` errors instead.
580
+ */
581
+ export function refineSpec(spec) {
582
+ const issues = [];
583
+ const issue = (path, message) => issues.push({ path, message });
584
+ const states = spec.states;
585
+ if (Object.keys(states).length === 0) {
586
+ issue("states", "Machine spec is missing or has empty 'states'");
587
+ return issues;
588
+ }
589
+ for (const [slug, state] of Object.entries(states)) {
590
+ const at = `states.${slug}`;
591
+ const transitions = state.transitions ?? [];
592
+ transitions.forEach((transition, index) => {
593
+ if (!(transition.to in states)) {
594
+ issue(`${at}.transitions.${index}.to`, `State '${slug}' transitions to undefined state '${transition.to}'`);
595
+ }
596
+ });
597
+ if (state.on_error !== undefined && !(state.on_error in states)) {
598
+ issue(`${at}.on_error`, `State '${slug}' on_error routes to undefined state '${state.on_error}'`);
599
+ }
600
+ // Before hooks gate entry with ok/veto only — there is no retry loop for an
601
+ // iteration budget to bound, so the sidecar would enforce nothing.
602
+ if (normalizeHooks(state.before).some((hook) => "max_iterations" in hook)) {
603
+ issue(`${at}.before`, `State '${slug}' 'before' hook declares 'max_iterations', but before hooks are ok/veto ` +
604
+ `only ('correct' is treated as a veto); the budget applies to 'after' hooks`);
605
+ }
606
+ for (const [index, entry] of (state.tools?.allow ?? []).entries()) {
607
+ for (const message of guardReferenceIssues(`State '${slug}'`, entry)) {
608
+ issue(`${at}.tools.allow.${index}`, message);
609
+ }
610
+ if (wildcardTool(entry))
611
+ issue(`${at}.tools.allow.${index}`, WILDCARD_GRANT_MESSAGE);
612
+ }
613
+ for (const [index, entry] of (state.tools?.forbid ?? []).entries()) {
614
+ for (const message of guardReferenceIssues(`State '${slug}'`, entry)) {
615
+ issue(`${at}.tools.forbid.${index}`, message);
616
+ }
617
+ }
618
+ }
619
+ for (const [index, entry] of (spec.tools?.allow_always ?? []).entries()) {
620
+ for (const message of guardReferenceIssues("tools.allow_always", entry)) {
621
+ issue(`tools.allow_always.${index}`, message);
622
+ }
623
+ if (wildcardTool(entry))
624
+ issue(`tools.allow_always.${index}`, WILDCARD_GRANT_MESSAGE);
625
+ }
626
+ for (const [index, entry] of (spec.tools?.forbid_always ?? []).entries()) {
627
+ for (const message of guardReferenceIssues("tools.forbid_always", entry)) {
628
+ issue(`tools.forbid_always.${index}`, message);
629
+ }
630
+ }
631
+ // Start wiring: at least one start state, and no trigger claimed by two
632
+ // states. A trigger is declared in one place now, so the only ambiguity left
633
+ // is two states declaring one id — a mapping cannot repeat a key.
634
+ const owners = new Map();
635
+ for (const [slug, state] of Object.entries(states)) {
636
+ for (const id of stateTriggerIds(state)) {
637
+ owners.set(id, [...(owners.get(id) ?? []), slug]);
638
+ }
639
+ }
640
+ for (const [id, states] of owners) {
641
+ if (states.length < 2)
642
+ continue;
643
+ issue("states", id === MANUAL_TRIGGER
644
+ ? `Multiple states declare trigger '${id}' (${states.join(", ")}); a manual start state must be unambiguous`
645
+ : `Multiple states declare trigger '${id}' (${states.join(", ")}); a tool trigger must name a unique start state`);
646
+ }
647
+ if (owners.size === 0) {
648
+ issue("states", "Workflow has no start state; declare a `triggers` mapping on at least one state (e.g. `triggers: { manual: }`)");
649
+ }
650
+ // Case concurrency is not implemented; a value above 1 would silently run
651
+ // sequentially, so it is rejected here and by the test runner alike.
652
+ if ((spec.tests?.maxConcurrency ?? 1) > 1) {
653
+ issue("tests.maxConcurrency", `'tests.maxConcurrency' above 1 is not supported: case concurrency is not implemented ` +
654
+ `and offline cases run sequentially — set it to 1 or remove the field`);
655
+ }
656
+ return issues;
657
+ }
658
+ /** Whether an entry names the every-tool wildcard, which only a denial may. */
659
+ function wildcardTool(entry) {
660
+ return typeof entry === "string" ? entry === FORBID_ANY_TOOL : entry?.tool === FORBID_ANY_TOOL;
661
+ }
662
+ /**
663
+ * The one wording for a wildcard where a grant belongs. Governance is
664
+ * allow-only: a state names what it may do, so `*` would be the one entry that
665
+ * says "anything" — while on the deny side it can only close the surface
666
+ * further, which is why `forbid` accepts it.
667
+ */
668
+ const WILDCARD_GRANT_MESSAGE = `'${FORBID_ANY_TOOL}' is not a tool name a grant may use: an allow list names the tools a ` +
669
+ `state may call, one by one. The every-tool wildcard belongs in a 'forbid' or 'forbid_always' ` +
670
+ `entry, where it can only deny.`;
671
+ /** Malformed `${{…}}` references in one allow entry's argument guards. */
672
+ function guardReferenceIssues(where, entry) {
673
+ const { tool, argMatchers } = normalizeAllowEntry(entry);
674
+ if (!argMatchers)
675
+ return [];
676
+ const messages = [];
677
+ for (const globs of Object.values(argMatchers)) {
678
+ for (const glob of globs) {
679
+ for (const ref of parseReferences(glob)) {
680
+ if (!referenceError(ref))
681
+ continue;
682
+ messages.push(`${where} guards '${tool ?? "?"}' on '${ref.raw}', which is not a valid variable ` +
683
+ `reference — use \${{name}} or \${{name.path.to.value}}.`);
684
+ }
685
+ }
686
+ }
687
+ return messages;
688
+ }
689
+ // --- Entry point ---------------------------------------------------------------
690
+ /** Render a Zod issue path the way an author addresses the key. */
691
+ function formatPath(path) {
692
+ return path.map(String).join(".");
693
+ }
694
+ /**
695
+ * Parse a spec through the schema, then the document-level rules, returning
696
+ * either the typed value or the structural issues. Total rather than throwing:
697
+ * the loader reports issues as warning events, the validator as diagnostics.
698
+ * When the shape held but a document-level rule failed, the typed `spec` is
699
+ * returned beside the issues so the lint can still read it.
700
+ */
701
+ export function parseMachineSpec(value) {
702
+ const result = machineSpecSchema.safeParse(value);
703
+ if (!result.success) {
704
+ return {
705
+ ok: false,
706
+ issues: result.error.issues.map((issue) => ({
707
+ path: formatPath(issue.path),
708
+ message: issue.message,
709
+ })),
710
+ };
711
+ }
712
+ const issues = refineSpec(result.data);
713
+ return issues.length > 0
714
+ ? { ok: false, issues, spec: result.data }
715
+ : { ok: true, spec: result.data };
716
+ }
717
+ //# sourceMappingURL=spec-schema.js.map