@united-workforce/cli 0.1.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 (310) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/dist/__tests__/adapter-json-roundtrip.test.d.ts +2 -0
  4. package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +1 -0
  5. package/dist/__tests__/adapter-json-roundtrip.test.js +147 -0
  6. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -0
  7. package/dist/__tests__/config.test.d.ts +2 -0
  8. package/dist/__tests__/config.test.d.ts.map +1 -0
  9. package/dist/__tests__/config.test.js +685 -0
  10. package/dist/__tests__/config.test.js.map +1 -0
  11. package/dist/__tests__/current-role.test.d.ts +2 -0
  12. package/dist/__tests__/current-role.test.d.ts.map +1 -0
  13. package/dist/__tests__/current-role.test.js +401 -0
  14. package/dist/__tests__/current-role.test.js.map +1 -0
  15. package/dist/__tests__/e2e-mock-agent.test.d.ts +2 -0
  16. package/dist/__tests__/e2e-mock-agent.test.d.ts.map +1 -0
  17. package/dist/__tests__/e2e-mock-agent.test.js +401 -0
  18. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -0
  19. package/dist/__tests__/include-tag.test.d.ts +2 -0
  20. package/dist/__tests__/include-tag.test.d.ts.map +1 -0
  21. package/dist/__tests__/include-tag.test.js +69 -0
  22. package/dist/__tests__/include-tag.test.js.map +1 -0
  23. package/dist/__tests__/log.test.d.ts +2 -0
  24. package/dist/__tests__/log.test.d.ts.map +1 -0
  25. package/dist/__tests__/log.test.js +161 -0
  26. package/dist/__tests__/log.test.js.map +1 -0
  27. package/dist/__tests__/moderator-evaluate.test.d.ts +2 -0
  28. package/dist/__tests__/moderator-evaluate.test.d.ts.map +1 -0
  29. package/dist/__tests__/moderator-evaluate.test.js +170 -0
  30. package/dist/__tests__/moderator-evaluate.test.js.map +1 -0
  31. package/dist/__tests__/preload.d.ts +3 -0
  32. package/dist/__tests__/preload.d.ts.map +1 -0
  33. package/dist/__tests__/preload.js +6 -0
  34. package/dist/__tests__/preload.js.map +1 -0
  35. package/dist/__tests__/prompt.test.d.ts +2 -0
  36. package/dist/__tests__/prompt.test.d.ts.map +1 -0
  37. package/dist/__tests__/prompt.test.js +111 -0
  38. package/dist/__tests__/prompt.test.js.map +1 -0
  39. package/dist/__tests__/resolve-head-hash.test.d.ts +2 -0
  40. package/dist/__tests__/resolve-head-hash.test.d.ts.map +1 -0
  41. package/dist/__tests__/resolve-head-hash.test.js +66 -0
  42. package/dist/__tests__/resolve-head-hash.test.js.map +1 -0
  43. package/dist/__tests__/setup-agent-discovery.test.d.ts +2 -0
  44. package/dist/__tests__/setup-agent-discovery.test.d.ts.map +1 -0
  45. package/dist/__tests__/setup-agent-discovery.test.js +119 -0
  46. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -0
  47. package/dist/__tests__/setup-complexity.test.d.ts +2 -0
  48. package/dist/__tests__/setup-complexity.test.d.ts.map +1 -0
  49. package/dist/__tests__/setup-complexity.test.js +314 -0
  50. package/dist/__tests__/setup-complexity.test.js.map +1 -0
  51. package/dist/__tests__/setup-validate.test.d.ts +2 -0
  52. package/dist/__tests__/setup-validate.test.d.ts.map +1 -0
  53. package/dist/__tests__/setup-validate.test.js +108 -0
  54. package/dist/__tests__/setup-validate.test.js.map +1 -0
  55. package/dist/__tests__/solve-issue-tea-worktree.test.d.ts +2 -0
  56. package/dist/__tests__/solve-issue-tea-worktree.test.d.ts.map +1 -0
  57. package/dist/__tests__/solve-issue-tea-worktree.test.js +107 -0
  58. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -0
  59. package/dist/__tests__/spawn-agent-json.test.d.ts +2 -0
  60. package/dist/__tests__/spawn-agent-json.test.d.ts.map +1 -0
  61. package/dist/__tests__/spawn-agent-json.test.js +79 -0
  62. package/dist/__tests__/spawn-agent-json.test.js.map +1 -0
  63. package/dist/__tests__/step-read.test.d.ts +2 -0
  64. package/dist/__tests__/step-read.test.d.ts.map +1 -0
  65. package/dist/__tests__/step-read.test.js +561 -0
  66. package/dist/__tests__/step-read.test.js.map +1 -0
  67. package/dist/__tests__/step-show-json.test.d.ts +2 -0
  68. package/dist/__tests__/step-show-json.test.d.ts.map +1 -0
  69. package/dist/__tests__/step-show-json.test.js +311 -0
  70. package/dist/__tests__/step-show-json.test.js.map +1 -0
  71. package/dist/__tests__/step-timing.test.d.ts +2 -0
  72. package/dist/__tests__/step-timing.test.d.ts.map +1 -0
  73. package/dist/__tests__/step-timing.test.js +345 -0
  74. package/dist/__tests__/step-timing.test.js.map +1 -0
  75. package/dist/__tests__/store-global-cas.test.d.ts +2 -0
  76. package/dist/__tests__/store-global-cas.test.d.ts.map +1 -0
  77. package/dist/__tests__/store-global-cas.test.js +235 -0
  78. package/dist/__tests__/store-global-cas.test.js.map +1 -0
  79. package/dist/__tests__/store-storage-root.test.d.ts +2 -0
  80. package/dist/__tests__/store-storage-root.test.d.ts.map +1 -0
  81. package/dist/__tests__/store-storage-root.test.js +43 -0
  82. package/dist/__tests__/store-storage-root.test.js.map +1 -0
  83. package/dist/__tests__/store-unified-threads.test.d.ts +2 -0
  84. package/dist/__tests__/store-unified-threads.test.d.ts.map +1 -0
  85. package/dist/__tests__/store-unified-threads.test.js +189 -0
  86. package/dist/__tests__/store-unified-threads.test.js.map +1 -0
  87. package/dist/__tests__/thread-cancel-status.test.d.ts +2 -0
  88. package/dist/__tests__/thread-cancel-status.test.d.ts.map +1 -0
  89. package/dist/__tests__/thread-cancel-status.test.js +111 -0
  90. package/dist/__tests__/thread-cancel-status.test.js.map +1 -0
  91. package/dist/__tests__/thread-list-filters.test.d.ts +2 -0
  92. package/dist/__tests__/thread-list-filters.test.d.ts.map +1 -0
  93. package/dist/__tests__/thread-list-filters.test.js +442 -0
  94. package/dist/__tests__/thread-list-filters.test.js.map +1 -0
  95. package/dist/__tests__/thread-location.test.d.ts +2 -0
  96. package/dist/__tests__/thread-location.test.d.ts.map +1 -0
  97. package/dist/__tests__/thread-location.test.js +159 -0
  98. package/dist/__tests__/thread-location.test.js.map +1 -0
  99. package/dist/__tests__/thread-read-quota.test.d.ts +2 -0
  100. package/dist/__tests__/thread-read-quota.test.d.ts.map +1 -0
  101. package/dist/__tests__/thread-read-quota.test.js +546 -0
  102. package/dist/__tests__/thread-read-quota.test.js.map +1 -0
  103. package/dist/__tests__/thread-read-xml-tags.test.d.ts +2 -0
  104. package/dist/__tests__/thread-read-xml-tags.test.d.ts.map +1 -0
  105. package/dist/__tests__/thread-read-xml-tags.test.js +610 -0
  106. package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -0
  107. package/dist/__tests__/thread-resume.test.d.ts +2 -0
  108. package/dist/__tests__/thread-resume.test.d.ts.map +1 -0
  109. package/dist/__tests__/thread-resume.test.js +592 -0
  110. package/dist/__tests__/thread-resume.test.js.map +1 -0
  111. package/dist/__tests__/thread-show-status.test.d.ts +2 -0
  112. package/dist/__tests__/thread-show-status.test.d.ts.map +1 -0
  113. package/dist/__tests__/thread-show-status.test.js +267 -0
  114. package/dist/__tests__/thread-show-status.test.js.map +1 -0
  115. package/dist/__tests__/thread-start-cwd-cli.test.d.ts +2 -0
  116. package/dist/__tests__/thread-start-cwd-cli.test.d.ts.map +1 -0
  117. package/dist/__tests__/thread-start-cwd-cli.test.js +130 -0
  118. package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -0
  119. package/dist/__tests__/thread-step-count.test.d.ts +2 -0
  120. package/dist/__tests__/thread-step-count.test.d.ts.map +1 -0
  121. package/dist/__tests__/thread-step-count.test.js +55 -0
  122. package/dist/__tests__/thread-step-count.test.js.map +1 -0
  123. package/dist/__tests__/thread-suspend-step.test.d.ts +2 -0
  124. package/dist/__tests__/thread-suspend-step.test.d.ts.map +1 -0
  125. package/dist/__tests__/thread-suspend-step.test.js +155 -0
  126. package/dist/__tests__/thread-suspend-step.test.js.map +1 -0
  127. package/dist/__tests__/thread-suspended-display.test.d.ts +2 -0
  128. package/dist/__tests__/thread-suspended-display.test.d.ts.map +1 -0
  129. package/dist/__tests__/thread-suspended-display.test.js +247 -0
  130. package/dist/__tests__/thread-suspended-display.test.js.map +1 -0
  131. package/dist/__tests__/thread-test-helpers.d.ts +4 -0
  132. package/dist/__tests__/thread-test-helpers.d.ts.map +1 -0
  133. package/dist/__tests__/thread-test-helpers.js +23 -0
  134. package/dist/__tests__/thread-test-helpers.js.map +1 -0
  135. package/dist/__tests__/thread.test.d.ts +2 -0
  136. package/dist/__tests__/thread.test.d.ts.map +1 -0
  137. package/dist/__tests__/thread.test.js +883 -0
  138. package/dist/__tests__/thread.test.js.map +1 -0
  139. package/dist/__tests__/validate-semantic.test.d.ts +2 -0
  140. package/dist/__tests__/validate-semantic.test.d.ts.map +1 -0
  141. package/dist/__tests__/validate-semantic.test.js +408 -0
  142. package/dist/__tests__/validate-semantic.test.js.map +1 -0
  143. package/dist/__tests__/workflow-resolution.test.d.ts +2 -0
  144. package/dist/__tests__/workflow-resolution.test.d.ts.map +1 -0
  145. package/dist/__tests__/workflow-resolution.test.js +308 -0
  146. package/dist/__tests__/workflow-resolution.test.js.map +1 -0
  147. package/dist/background/background.d.ts +38 -0
  148. package/dist/background/background.d.ts.map +1 -0
  149. package/dist/background/background.js +123 -0
  150. package/dist/background/background.js.map +1 -0
  151. package/dist/background/index.d.ts +3 -0
  152. package/dist/background/index.d.ts.map +1 -0
  153. package/dist/background/index.js +2 -0
  154. package/dist/background/index.js.map +1 -0
  155. package/dist/background/types.d.ts +9 -0
  156. package/dist/background/types.d.ts.map +1 -0
  157. package/dist/background/types.js +2 -0
  158. package/dist/background/types.js.map +1 -0
  159. package/dist/cli.d.ts +3 -0
  160. package/dist/cli.d.ts.map +1 -0
  161. package/dist/cli.js +535 -0
  162. package/dist/cli.js.map +1 -0
  163. package/dist/commands/config.d.ts +41 -0
  164. package/dist/commands/config.d.ts.map +1 -0
  165. package/dist/commands/config.js +252 -0
  166. package/dist/commands/config.js.map +1 -0
  167. package/dist/commands/log.d.ts +26 -0
  168. package/dist/commands/log.d.ts.map +1 -0
  169. package/dist/commands/log.js +79 -0
  170. package/dist/commands/log.js.map +1 -0
  171. package/dist/commands/prompt.d.ts +6 -0
  172. package/dist/commands/prompt.d.ts.map +1 -0
  173. package/dist/commands/prompt.js +67 -0
  174. package/dist/commands/prompt.js.map +1 -0
  175. package/dist/commands/setup.d.ts +73 -0
  176. package/dist/commands/setup.d.ts.map +1 -0
  177. package/dist/commands/setup.js +522 -0
  178. package/dist/commands/setup.js.map +1 -0
  179. package/dist/commands/shared.d.ts +31 -0
  180. package/dist/commands/shared.d.ts.map +1 -0
  181. package/dist/commands/shared.js +154 -0
  182. package/dist/commands/shared.js.map +1 -0
  183. package/dist/commands/step.d.ts +18 -0
  184. package/dist/commands/step.d.ts.map +1 -0
  185. package/dist/commands/step.js +257 -0
  186. package/dist/commands/step.js.map +1 -0
  187. package/dist/commands/thread-time-parser.d.ts +6 -0
  188. package/dist/commands/thread-time-parser.d.ts.map +1 -0
  189. package/dist/commands/thread-time-parser.js +22 -0
  190. package/dist/commands/thread-time-parser.js.map +1 -0
  191. package/dist/commands/thread.d.ts +38 -0
  192. package/dist/commands/thread.d.ts.map +1 -0
  193. package/dist/commands/thread.js +1087 -0
  194. package/dist/commands/thread.js.map +1 -0
  195. package/dist/commands/workflow.d.ts +24 -0
  196. package/dist/commands/workflow.d.ts.map +1 -0
  197. package/dist/commands/workflow.js +138 -0
  198. package/dist/commands/workflow.js.map +1 -0
  199. package/dist/format.d.ts +3 -0
  200. package/dist/format.d.ts.map +1 -0
  201. package/dist/format.js +10 -0
  202. package/dist/format.js.map +1 -0
  203. package/dist/include.d.ts +12 -0
  204. package/dist/include.d.ts.map +1 -0
  205. package/dist/include.js +35 -0
  206. package/dist/include.js.map +1 -0
  207. package/dist/moderator/__tests__/evaluate.test.d.ts +2 -0
  208. package/dist/moderator/__tests__/evaluate.test.d.ts.map +1 -0
  209. package/dist/moderator/__tests__/evaluate.test.js +167 -0
  210. package/dist/moderator/__tests__/evaluate.test.js.map +1 -0
  211. package/dist/moderator/evaluate.d.ts +6 -0
  212. package/dist/moderator/evaluate.d.ts.map +1 -0
  213. package/dist/moderator/evaluate.js +65 -0
  214. package/dist/moderator/evaluate.js.map +1 -0
  215. package/dist/moderator/index.d.ts +4 -0
  216. package/dist/moderator/index.d.ts.map +1 -0
  217. package/dist/moderator/index.js +3 -0
  218. package/dist/moderator/index.js.map +1 -0
  219. package/dist/moderator/types.d.ts +25 -0
  220. package/dist/moderator/types.d.ts.map +1 -0
  221. package/dist/moderator/types.js +4 -0
  222. package/dist/moderator/types.js.map +1 -0
  223. package/dist/schemas.d.ts +16 -0
  224. package/dist/schemas.d.ts.map +1 -0
  225. package/dist/schemas.js +17 -0
  226. package/dist/schemas.js.map +1 -0
  227. package/dist/store.d.ts +77 -0
  228. package/dist/store.d.ts.map +1 -0
  229. package/dist/store.js +392 -0
  230. package/dist/store.js.map +1 -0
  231. package/dist/validate-semantic.d.ts +7 -0
  232. package/dist/validate-semantic.d.ts.map +1 -0
  233. package/dist/validate-semantic.js +263 -0
  234. package/dist/validate-semantic.js.map +1 -0
  235. package/dist/validate.d.ts +16 -0
  236. package/dist/validate.d.ts.map +1 -0
  237. package/dist/validate.js +115 -0
  238. package/dist/validate.js.map +1 -0
  239. package/package.json +44 -0
  240. package/src/__tests__/adapter-json-roundtrip.test.ts +181 -0
  241. package/src/__tests__/config.test.ts +740 -0
  242. package/src/__tests__/current-role.test.ts +438 -0
  243. package/src/__tests__/e2e-mock-agent.test.ts +498 -0
  244. package/src/__tests__/fixtures/e2e-completed-resume.mock.yaml +15 -0
  245. package/src/__tests__/fixtures/e2e-count.mock.yaml +19 -0
  246. package/src/__tests__/fixtures/e2e-count.workflow.yaml +45 -0
  247. package/src/__tests__/fixtures/e2e-linear.mock.yaml +13 -0
  248. package/src/__tests__/fixtures/e2e-linear.workflow.yaml +32 -0
  249. package/src/__tests__/fixtures/e2e-loop.mock.yaml +25 -0
  250. package/src/__tests__/fixtures/e2e-loop.workflow.yaml +36 -0
  251. package/src/__tests__/fixtures/e2e-mismatch.mock.yaml +16 -0
  252. package/src/__tests__/fixtures/e2e-mustache.mock.yaml +15 -0
  253. package/src/__tests__/fixtures/e2e-mustache.workflow.yaml +34 -0
  254. package/src/__tests__/fixtures/e2e-suspend.mock.yaml +14 -0
  255. package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +24 -0
  256. package/src/__tests__/include-tag.test.ts +84 -0
  257. package/src/__tests__/log.test.ts +181 -0
  258. package/src/__tests__/moderator-evaluate.test.ts +186 -0
  259. package/src/__tests__/preload.ts +7 -0
  260. package/src/__tests__/prompt.test.ts +129 -0
  261. package/src/__tests__/resolve-head-hash.test.ts +86 -0
  262. package/src/__tests__/setup-agent-discovery.test.ts +167 -0
  263. package/src/__tests__/setup-complexity.test.ts +381 -0
  264. package/src/__tests__/setup-validate.test.ts +148 -0
  265. package/src/__tests__/solve-issue-tea-worktree.test.ts +144 -0
  266. package/src/__tests__/spawn-agent-json.test.ts +100 -0
  267. package/src/__tests__/step-read.test.ts +632 -0
  268. package/src/__tests__/step-show-json.test.ts +373 -0
  269. package/src/__tests__/step-timing.test.ts +392 -0
  270. package/src/__tests__/store-global-cas.test.ts +308 -0
  271. package/src/__tests__/store-storage-root.test.ts +49 -0
  272. package/src/__tests__/store-unified-threads.test.ts +235 -0
  273. package/src/__tests__/thread-cancel-status.test.ts +138 -0
  274. package/src/__tests__/thread-list-filters.test.ts +572 -0
  275. package/src/__tests__/thread-location.test.ts +186 -0
  276. package/src/__tests__/thread-read-quota.test.ts +613 -0
  277. package/src/__tests__/thread-read-xml-tags.test.ts +717 -0
  278. package/src/__tests__/thread-resume.test.ts +710 -0
  279. package/src/__tests__/thread-show-status.test.ts +317 -0
  280. package/src/__tests__/thread-start-cwd-cli.test.ts +164 -0
  281. package/src/__tests__/thread-step-count.test.ts +70 -0
  282. package/src/__tests__/thread-suspend-step.test.ts +181 -0
  283. package/src/__tests__/thread-suspended-display.test.ts +287 -0
  284. package/src/__tests__/thread-test-helpers.ts +37 -0
  285. package/src/__tests__/thread.test.ts +1025 -0
  286. package/src/__tests__/validate-semantic.test.ts +474 -0
  287. package/src/__tests__/workflow-resolution.test.ts +421 -0
  288. package/src/background/background.ts +147 -0
  289. package/src/background/index.ts +11 -0
  290. package/src/background/types.ts +9 -0
  291. package/src/cli.ts +692 -0
  292. package/src/commands/config.ts +304 -0
  293. package/src/commands/log.ts +116 -0
  294. package/src/commands/prompt.ts +81 -0
  295. package/src/commands/setup.ts +603 -0
  296. package/src/commands/shared.ts +227 -0
  297. package/src/commands/step.ts +343 -0
  298. package/src/commands/thread-time-parser.ts +23 -0
  299. package/src/commands/thread.ts +1575 -0
  300. package/src/commands/workflow.ts +213 -0
  301. package/src/format.ts +12 -0
  302. package/src/include.ts +37 -0
  303. package/src/moderator/__tests__/evaluate.test.ts +199 -0
  304. package/src/moderator/evaluate.ts +80 -0
  305. package/src/moderator/index.ts +7 -0
  306. package/src/moderator/types.ts +24 -0
  307. package/src/schemas.ts +26 -0
  308. package/src/store.ts +479 -0
  309. package/src/validate-semantic.ts +304 -0
  310. package/src/validate.ts +137 -0
@@ -0,0 +1,474 @@
1
+ import type { WorkflowPayload } from "@united-workforce/protocol";
2
+ import { describe, expect, test } from "vitest";
3
+ import { validateWorkflow } from "../validate-semantic.js";
4
+
5
+ /** Build a valid two-role workflow that passes all checks. */
6
+ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
7
+ const base: WorkflowPayload = {
8
+ name: "test-workflow",
9
+ description: "A test workflow",
10
+ roles: {
11
+ writer: {
12
+ description: "Writes content",
13
+ goal: "Write content",
14
+ capabilities: ["writing"],
15
+ procedure: "Write it",
16
+ output: "The content",
17
+ frontmatter: {
18
+ type: "object",
19
+ properties: {
20
+ $status: { enum: ["done"] },
21
+ plan: { type: "string" },
22
+ },
23
+ required: ["$status", "plan"],
24
+ } as unknown as string,
25
+ },
26
+ reviewer: {
27
+ description: "Reviews content",
28
+ goal: "Review content",
29
+ capabilities: ["reviewing"],
30
+ procedure: "Review it",
31
+ output: "The review",
32
+ frontmatter: {
33
+ type: "object",
34
+ oneOf: [
35
+ {
36
+ properties: {
37
+ $status: { const: "approved" },
38
+ summary: { type: "string" },
39
+ },
40
+ required: ["$status", "summary"],
41
+ },
42
+ {
43
+ properties: {
44
+ $status: { const: "rejected" },
45
+ reason: { type: "string" },
46
+ },
47
+ required: ["$status", "reason"],
48
+ },
49
+ ],
50
+ } as unknown as string,
51
+ },
52
+ },
53
+ graph: {
54
+ $START: { _: { role: "writer", prompt: "Begin writing", location: null } },
55
+ writer: { done: { role: "reviewer", prompt: "Review this: {{{plan}}}", location: null } },
56
+ reviewer: {
57
+ approved: { role: "$END", prompt: "Done: {{{summary}}}", location: null },
58
+ rejected: { role: "writer", prompt: "Fix: {{{reason}}}", location: null },
59
+ },
60
+ },
61
+ };
62
+
63
+ if (!overrides) return base;
64
+ return { ...base, ...overrides };
65
+ }
66
+
67
+ describe("Suite 1: Role Reference Integrity", () => {
68
+ test("1.1 graph references unknown role", () => {
69
+ const wf = makeWorkflow();
70
+ wf.graph.nonexistent = { _: { role: "$END", prompt: "done", location: null } };
71
+ const errors = validateWorkflow(wf);
72
+ expect(errors.some((e) => e.includes('unknown role "nonexistent"'))).toBe(true);
73
+ });
74
+
75
+ test("1.2 orphan role not in graph", () => {
76
+ const wf = makeWorkflow();
77
+ wf.roles.orphan = {
78
+ description: "Orphan",
79
+ goal: "Nothing",
80
+ capabilities: [],
81
+ procedure: "None",
82
+ output: "None",
83
+ frontmatter: {
84
+ type: "object",
85
+ properties: { $status: { enum: ["done"] } },
86
+ required: ["$status"],
87
+ } as unknown as string,
88
+ };
89
+ const errors = validateWorkflow(wf);
90
+ expect(
91
+ errors.some((e) => e.includes('role "orphan" is defined but not referenced in graph')),
92
+ ).toBe(true);
93
+ });
94
+
95
+ test("1.3 $START in roles", () => {
96
+ const wf = makeWorkflow();
97
+ (wf.roles as Record<string, unknown>).$START = {
98
+ description: "Bad",
99
+ goal: "Bad",
100
+ capabilities: [],
101
+ procedure: "Bad",
102
+ output: "Bad",
103
+ frontmatter: { type: "object", properties: {}, required: [] },
104
+ };
105
+ const errors = validateWorkflow(wf);
106
+ expect(errors.some((e) => e.includes('reserved name "$START"'))).toBe(true);
107
+ });
108
+
109
+ test("1.4 $END in roles", () => {
110
+ const wf = makeWorkflow();
111
+ (wf.roles as Record<string, unknown>).$END = {
112
+ description: "Bad",
113
+ goal: "Bad",
114
+ capabilities: [],
115
+ procedure: "Bad",
116
+ output: "Bad",
117
+ frontmatter: { type: "object", properties: {}, required: [] },
118
+ };
119
+ const errors = validateWorkflow(wf);
120
+ expect(errors.some((e) => e.includes('reserved name "$END"'))).toBe(true);
121
+ });
122
+
123
+ test("1.5 valid workflow returns no errors", () => {
124
+ const wf = makeWorkflow();
125
+ const errors = validateWorkflow(wf);
126
+ expect(errors).toEqual([]);
127
+ });
128
+ });
129
+
130
+ describe("Suite 2: Graph Structure", () => {
131
+ test("2.1 $START missing from graph", () => {
132
+ const wf = makeWorkflow();
133
+ delete wf.graph.$START;
134
+ const errors = validateWorkflow(wf);
135
+ expect(errors.some((e) => e.includes("$START must be defined in graph"))).toBe(true);
136
+ });
137
+
138
+ test("2.2 $START has multiple status keys", () => {
139
+ const wf = makeWorkflow();
140
+ wf.graph.$START = {
141
+ _: { role: "writer", prompt: "Begin", location: null },
142
+ other: { role: "reviewer", prompt: "Also", location: null },
143
+ };
144
+ const errors = validateWorkflow(wf);
145
+ expect(
146
+ errors.some((e) => e.includes('$START must have exactly one edge with status "_"')),
147
+ ).toBe(true);
148
+ });
149
+
150
+ test("2.3 $START edge uses non-_ status", () => {
151
+ const wf = makeWorkflow();
152
+ wf.graph.$START = { ready: { role: "writer", prompt: "Begin", location: null } };
153
+ const errors = validateWorkflow(wf);
154
+ expect(
155
+ errors.some((e) => e.includes('$START must have exactly one edge with status "_"')),
156
+ ).toBe(true);
157
+ });
158
+
159
+ test("2.4 $END has outgoing edges", () => {
160
+ const wf = makeWorkflow();
161
+ wf.graph.$END = { _: { role: "writer", prompt: "Loop", location: null } };
162
+ const errors = validateWorkflow(wf);
163
+ expect(errors.some((e) => e.includes("$END must not have outgoing edges"))).toBe(true);
164
+ });
165
+
166
+ test("2.5 unreachable role", () => {
167
+ const wf = makeWorkflow();
168
+ wf.roles.isolated = {
169
+ description: "Isolated",
170
+ goal: "Isolated",
171
+ capabilities: [],
172
+ procedure: "Isolated",
173
+ output: "Isolated",
174
+ frontmatter: {
175
+ type: "object",
176
+ properties: { $status: { enum: ["done"] } },
177
+ required: ["$status"],
178
+ } as unknown as string,
179
+ };
180
+ wf.graph.isolated = { done: { role: "$END", prompt: "done", location: null } };
181
+ const errors = validateWorkflow(wf);
182
+ expect(errors.some((e) => e.includes('role "isolated" is not reachable from $START'))).toBe(
183
+ true,
184
+ );
185
+ });
186
+
187
+ test("2.6 edge target references invalid role", () => {
188
+ const wf = makeWorkflow();
189
+ wf.graph.writer = { done: { role: "ghost", prompt: "Go to ghost", location: null } };
190
+ const errors = validateWorkflow(wf);
191
+ expect(errors.some((e) => e.includes('unknown target role "ghost"'))).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe("Suite 3: Status-Edge Consistency", () => {
196
+ test("3.1 user role using _ graph key is rejected", () => {
197
+ const wf = makeWorkflow();
198
+ wf.graph.writer = { _: { role: "reviewer", prompt: "Review", location: null } };
199
+ const errors = validateWorkflow(wf);
200
+ expect(
201
+ errors.some((e) =>
202
+ e.includes('role "writer" must use explicit $status keys in graph, not "_"'),
203
+ ),
204
+ ).toBe(true);
205
+ });
206
+
207
+ test("3.2 user role graph key not matching $status enum", () => {
208
+ const wf = makeWorkflow();
209
+ wf.graph.writer = { wrong: { role: "reviewer", prompt: "Review", location: null } };
210
+ const errors = validateWorkflow(wf);
211
+ expect(errors.some((e) => e.includes('role "writer" graph has extra status keys: wrong'))).toBe(
212
+ true,
213
+ );
214
+ expect(errors.some((e) => e.includes('role "writer" graph is missing status keys: done'))).toBe(
215
+ true,
216
+ );
217
+ });
218
+
219
+ test("3.3 multi-exit role with extra statuses", () => {
220
+ const wf = makeWorkflow();
221
+ wf.graph.reviewer = {
222
+ approved: { role: "$END", prompt: "Done", location: null },
223
+ rejected: { role: "writer", prompt: "Fix", location: null },
224
+ timeout: { role: "$END", prompt: "Timed out", location: null },
225
+ };
226
+ const errors = validateWorkflow(wf);
227
+ expect(
228
+ errors.some((e) => e.includes('role "reviewer" graph has extra status keys: timeout')),
229
+ ).toBe(true);
230
+ });
231
+
232
+ test("3.4 multi-exit role missing a status", () => {
233
+ const wf = makeWorkflow();
234
+ wf.graph.reviewer = {
235
+ approved: { role: "$END", prompt: "Done", location: null },
236
+ };
237
+ const errors = validateWorkflow(wf);
238
+ expect(
239
+ errors.some((e) => e.includes('role "reviewer" graph is missing status keys: rejected')),
240
+ ).toBe(true);
241
+ });
242
+
243
+ test("3.5 multi-exit role with _ key", () => {
244
+ const wf = makeWorkflow();
245
+ wf.graph.reviewer = { _: { role: "$END", prompt: "Done", location: null } };
246
+ const errors = validateWorkflow(wf);
247
+ expect(
248
+ errors.some((e) =>
249
+ e.includes('role "reviewer" must use explicit $status keys in graph, not "_"'),
250
+ ),
251
+ ).toBe(true);
252
+ });
253
+ });
254
+
255
+ describe("Suite 3b: Enum-Based Multi-Exit", () => {
256
+ test("3b.1 enum multi-exit passes with matching graph keys", () => {
257
+ const wf = makeWorkflow();
258
+ wf.roles.reviewer = {
259
+ ...wf.roles.reviewer,
260
+ frontmatter: {
261
+ type: "object",
262
+ properties: {
263
+ $status: { enum: ["approved", "rejected"] },
264
+ comments: { type: "string" },
265
+ },
266
+ required: ["$status", "comments"],
267
+ } as unknown as string,
268
+ };
269
+ wf.graph.reviewer = {
270
+ approved: { role: "$END", prompt: "Done", location: null },
271
+ rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
272
+ };
273
+ const errors = validateWorkflow(wf);
274
+ expect(errors).toEqual([]);
275
+ });
276
+
277
+ test("3b.2 enum multi-exit with extra graph key", () => {
278
+ const wf = makeWorkflow();
279
+ wf.roles.reviewer = {
280
+ ...wf.roles.reviewer,
281
+ frontmatter: {
282
+ type: "object",
283
+ properties: {
284
+ $status: { enum: ["approved", "rejected"] },
285
+ comments: { type: "string" },
286
+ },
287
+ required: ["$status", "comments"],
288
+ } as unknown as string,
289
+ };
290
+ wf.graph.reviewer = {
291
+ approved: { role: "$END", prompt: "Done", location: null },
292
+ rejected: { role: "writer", prompt: "Fix", location: null },
293
+ timeout: { role: "$END", prompt: "Timed out", location: null },
294
+ };
295
+ const errors = validateWorkflow(wf);
296
+ expect(errors.some((e) => e.includes("extra status keys: timeout"))).toBe(true);
297
+ });
298
+
299
+ test("3b.3 enum multi-exit with missing graph key", () => {
300
+ const wf = makeWorkflow();
301
+ wf.roles.reviewer = {
302
+ ...wf.roles.reviewer,
303
+ frontmatter: {
304
+ type: "object",
305
+ properties: {
306
+ $status: { enum: ["approved", "rejected"] },
307
+ comments: { type: "string" },
308
+ },
309
+ required: ["$status", "comments"],
310
+ } as unknown as string,
311
+ };
312
+ wf.graph.reviewer = {
313
+ approved: { role: "$END", prompt: "Done", location: null },
314
+ };
315
+ const errors = validateWorkflow(wf);
316
+ expect(errors.some((e) => e.includes("missing status keys: rejected"))).toBe(true);
317
+ });
318
+
319
+ test("3b.4 enum with single explicit value passes", () => {
320
+ const wf = makeWorkflow();
321
+ wf.roles.writer = {
322
+ ...wf.roles.writer,
323
+ frontmatter: {
324
+ type: "object",
325
+ properties: {
326
+ $status: { enum: ["ready"] },
327
+ plan: { type: "string" },
328
+ },
329
+ required: ["$status", "plan"],
330
+ } as unknown as string,
331
+ };
332
+ wf.graph.writer = { ready: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } };
333
+ const errors = validateWorkflow(wf);
334
+ expect(errors).toEqual([]);
335
+ });
336
+
337
+ test("3b.5 enum multi-exit mustache var not in frontmatter", () => {
338
+ const wf = makeWorkflow();
339
+ wf.roles.reviewer = {
340
+ ...wf.roles.reviewer,
341
+ frontmatter: {
342
+ type: "object",
343
+ properties: {
344
+ $status: { enum: ["approved", "rejected"] },
345
+ comments: { type: "string" },
346
+ },
347
+ required: ["$status", "comments"],
348
+ } as unknown as string,
349
+ };
350
+ wf.graph.reviewer = {
351
+ approved: { role: "$END", prompt: "Done: {{{nonexistent}}}", location: null },
352
+ rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
353
+ };
354
+ const errors = validateWorkflow(wf);
355
+ expect(errors.some((e) => e.includes("nonexistent") && e.includes("not found"))).toBe(true);
356
+ });
357
+ });
358
+
359
+ describe("Suite 4: Mustache Template Variable Existence", () => {
360
+ test("4.1 prompt references nonexistent variable (enum status)", () => {
361
+ const wf = makeWorkflow();
362
+ wf.graph.writer = {
363
+ done: { role: "reviewer", prompt: "Review: {{{branch}}}", location: null },
364
+ };
365
+ const errors = validateWorkflow(wf);
366
+ expect(
367
+ errors.some(
368
+ (e) => e.includes('prompt variable "branch"') && e.includes('role "writer" frontmatter'),
369
+ ),
370
+ ).toBe(true);
371
+ });
372
+
373
+ test("4.2 prompt references nonexistent variable (multi-exit)", () => {
374
+ const wf = makeWorkflow();
375
+ wf.graph.reviewer = {
376
+ approved: { role: "$END", prompt: "Done: {{{branch}}}", location: null },
377
+ rejected: { role: "writer", prompt: "Fix: {{{reason}}}", location: null },
378
+ };
379
+ const errors = validateWorkflow(wf);
380
+ expect(
381
+ errors.some((e) =>
382
+ e.includes('prompt variable "branch" not found in role "reviewer" variant "approved"'),
383
+ ),
384
+ ).toBe(true);
385
+ });
386
+
387
+ test("4.3 valid mustache variables pass", () => {
388
+ const wf = makeWorkflow();
389
+ const errors = validateWorkflow(wf);
390
+ expect(errors).toEqual([]);
391
+ });
392
+
393
+ test("4.4 $status variable is always valid", () => {
394
+ const wf = makeWorkflow();
395
+ wf.graph.writer = { done: { role: "reviewer", prompt: "Status: {{$status}}", location: null } };
396
+ const errors = validateWorkflow(wf);
397
+ expect(errors).toEqual([]);
398
+ });
399
+ });
400
+
401
+ describe("Suite 5: oneOf Discriminant Validity", () => {
402
+ test("5.1 oneOf without $status const", () => {
403
+ const wf = makeWorkflow();
404
+ wf.roles.reviewer = {
405
+ ...wf.roles.reviewer,
406
+ frontmatter: {
407
+ type: "object",
408
+ oneOf: [
409
+ { properties: { summary: { type: "string" } }, required: ["summary"] },
410
+ { properties: { reason: { type: "string" } }, required: ["reason"] },
411
+ ],
412
+ } as unknown as string,
413
+ };
414
+ const errors = validateWorkflow(wf);
415
+ expect(
416
+ errors.some((e) => e.includes('oneOf variants must have "$status" as const discriminant')),
417
+ ).toBe(true);
418
+ });
419
+
420
+ test("5.2 oneOf with non-const $status", () => {
421
+ const wf = makeWorkflow();
422
+ wf.roles.reviewer = {
423
+ ...wf.roles.reviewer,
424
+ frontmatter: {
425
+ type: "object",
426
+ oneOf: [
427
+ {
428
+ properties: { $status: { type: "string" }, summary: { type: "string" } },
429
+ required: ["$status", "summary"],
430
+ },
431
+ {
432
+ properties: { $status: { type: "string" }, reason: { type: "string" } },
433
+ required: ["$status", "reason"],
434
+ },
435
+ ],
436
+ } as unknown as string,
437
+ };
438
+ const errors = validateWorkflow(wf);
439
+ expect(errors.some((e) => e.includes("oneOf variant $status must be a const value"))).toBe(
440
+ true,
441
+ );
442
+ });
443
+
444
+ test("5.3 valid oneOf passes", () => {
445
+ const wf = makeWorkflow();
446
+ const errors = validateWorkflow(wf);
447
+ expect(errors).toEqual([]);
448
+ });
449
+ });
450
+
451
+ describe("Suite 6: Multiple Errors Collection", () => {
452
+ test("6.1 multiple errors collected", () => {
453
+ const wf = makeWorkflow();
454
+ // orphan role
455
+ wf.roles.orphan = {
456
+ description: "Orphan",
457
+ goal: "Nothing",
458
+ capabilities: [],
459
+ procedure: "None",
460
+ output: "None",
461
+ frontmatter: {
462
+ type: "object",
463
+ properties: { $status: { enum: ["done"] } },
464
+ required: ["$status"],
465
+ } as unknown as string,
466
+ };
467
+ // unknown graph reference
468
+ wf.graph.nonexistent = { done: { role: "$END", prompt: "done", location: null } };
469
+ // bad mustache var
470
+ wf.graph.writer = { done: { role: "reviewer", prompt: "{{{badvar}}}", location: null } };
471
+ const errors = validateWorkflow(wf);
472
+ expect(errors.length).toBeGreaterThanOrEqual(3);
473
+ });
474
+ });