@united-workforce/cli 0.3.0 → 0.5.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 (319) hide show
  1. package/README.md +45 -11
  2. package/dist/.build-fingerprint +1 -0
  3. package/dist/__tests__/adapter-json-roundtrip.test.js +17 -7
  4. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  5. package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
  6. package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
  7. package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
  8. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
  9. package/dist/__tests__/build-step-entry.test.d.ts +2 -0
  10. package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
  11. package/dist/__tests__/build-step-entry.test.js +173 -0
  12. package/dist/__tests__/build-step-entry.test.js.map +1 -0
  13. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
  14. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
  15. package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
  16. package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
  17. package/dist/__tests__/concurrency.test.d.ts +2 -0
  18. package/dist/__tests__/concurrency.test.d.ts.map +1 -0
  19. package/dist/__tests__/concurrency.test.js +196 -0
  20. package/dist/__tests__/concurrency.test.js.map +1 -0
  21. package/dist/__tests__/config.test.js +26 -302
  22. package/dist/__tests__/config.test.js.map +1 -1
  23. package/dist/__tests__/current-role.test.js +7 -6
  24. package/dist/__tests__/current-role.test.js.map +1 -1
  25. package/dist/__tests__/e2e-mock-agent.test.js +43 -30
  26. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  27. package/dist/__tests__/format-text-default.test.d.ts +2 -0
  28. package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
  29. package/dist/__tests__/format-text-default.test.js +43 -0
  30. package/dist/__tests__/format-text-default.test.js.map +1 -0
  31. package/dist/__tests__/format-text-registry.test.d.ts +2 -0
  32. package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
  33. package/dist/__tests__/format-text-registry.test.js +158 -0
  34. package/dist/__tests__/format-text-registry.test.js.map +1 -0
  35. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
  36. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
  37. package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
  38. package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
  39. package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
  40. package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
  41. package/dist/__tests__/log-text-renderer.test.js +265 -0
  42. package/dist/__tests__/log-text-renderer.test.js.map +1 -0
  43. package/dist/__tests__/moderator-evaluate.test.js +9 -50
  44. package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
  45. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
  46. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
  47. package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
  48. package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
  49. package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
  50. package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
  51. package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
  52. package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
  53. package/dist/__tests__/pid-recycling.test.d.ts +2 -0
  54. package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
  55. package/dist/__tests__/pid-recycling.test.js +273 -0
  56. package/dist/__tests__/pid-recycling.test.js.map +1 -0
  57. package/dist/__tests__/prompt.test.js +365 -2
  58. package/dist/__tests__/prompt.test.js.map +1 -1
  59. package/dist/__tests__/resolve-head-hash.test.js +12 -4
  60. package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
  61. package/dist/__tests__/setup-agent-discovery.test.js +21 -30
  62. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  63. package/dist/__tests__/setup-complexity.test.js +2 -168
  64. package/dist/__tests__/setup-complexity.test.js.map +1 -1
  65. package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
  66. package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
  67. package/dist/__tests__/setup-no-llm.test.js +52 -0
  68. package/dist/__tests__/setup-no-llm.test.js.map +1 -0
  69. package/dist/__tests__/solve-issue-tea-worktree.test.js +27 -28
  70. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
  71. package/dist/__tests__/step-ask.test.d.ts +2 -0
  72. package/dist/__tests__/step-ask.test.d.ts.map +1 -0
  73. package/dist/__tests__/step-ask.test.js +507 -0
  74. package/dist/__tests__/step-ask.test.js.map +1 -0
  75. package/dist/__tests__/step-show-json.test.js +1 -0
  76. package/dist/__tests__/step-show-json.test.js.map +1 -1
  77. package/dist/__tests__/step-timing.test.js +2 -0
  78. package/dist/__tests__/step-timing.test.js.map +1 -1
  79. package/dist/__tests__/store-global-cas.test.js +2 -2
  80. package/dist/__tests__/store-global-cas.test.js.map +1 -1
  81. package/dist/__tests__/store-unified-threads.test.js +28 -26
  82. package/dist/__tests__/store-unified-threads.test.js.map +1 -1
  83. package/dist/__tests__/thread-cancel-status.test.js +25 -19
  84. package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
  85. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
  86. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
  87. package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
  88. package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
  89. package/dist/__tests__/thread-list-filters.test.js +354 -17
  90. package/dist/__tests__/thread-list-filters.test.js.map +1 -1
  91. package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
  92. package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
  93. package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
  94. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
  95. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
  96. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
  97. package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
  98. package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
  99. package/dist/__tests__/thread-poke.test.d.ts +2 -0
  100. package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
  101. package/dist/__tests__/thread-poke.test.js +422 -0
  102. package/dist/__tests__/thread-poke.test.js.map +1 -0
  103. package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
  104. package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
  105. package/dist/__tests__/thread-resume.test.js +21 -15
  106. package/dist/__tests__/thread-resume.test.js.map +1 -1
  107. package/dist/__tests__/thread-show-status.test.js +17 -28
  108. package/dist/__tests__/thread-show-status.test.js.map +1 -1
  109. package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
  110. package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
  111. package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
  112. package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
  113. package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
  114. package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
  115. package/dist/__tests__/thread-suspend-step.test.js +13 -16
  116. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  117. package/dist/__tests__/thread-suspended-display.test.js +10 -22
  118. package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
  119. package/dist/__tests__/thread-test-helpers.d.ts +7 -0
  120. package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
  121. package/dist/__tests__/thread-test-helpers.js +13 -0
  122. package/dist/__tests__/thread-test-helpers.js.map +1 -1
  123. package/dist/__tests__/thread.test.js +15 -13
  124. package/dist/__tests__/thread.test.js.map +1 -1
  125. package/dist/__tests__/validate-semantic.test.js +105 -23
  126. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  127. package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
  128. package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
  129. package/dist/__tests__/workflow-list-recursive.test.js +286 -0
  130. package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
  131. package/dist/__tests__/workflow-resolution.test.js +46 -28
  132. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  133. package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
  134. package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
  135. package/dist/__tests__/workflow-show-resolution.test.js +213 -0
  136. package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
  137. package/dist/__tests__/workflow-validate.test.d.ts +2 -0
  138. package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
  139. package/dist/__tests__/workflow-validate.test.js +707 -0
  140. package/dist/__tests__/workflow-validate.test.js.map +1 -0
  141. package/dist/__tests__/write-envelope.test.d.ts +2 -0
  142. package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
  143. package/dist/__tests__/write-envelope.test.js +201 -0
  144. package/dist/__tests__/write-envelope.test.js.map +1 -0
  145. package/dist/background/background.d.ts +22 -1
  146. package/dist/background/background.d.ts.map +1 -1
  147. package/dist/background/background.js +83 -6
  148. package/dist/background/background.js.map +1 -1
  149. package/dist/background/index.d.ts +1 -1
  150. package/dist/background/index.d.ts.map +1 -1
  151. package/dist/background/index.js +1 -1
  152. package/dist/background/index.js.map +1 -1
  153. package/dist/background/types.d.ts +1 -0
  154. package/dist/background/types.d.ts.map +1 -1
  155. package/dist/cli.js +120 -62
  156. package/dist/cli.js.map +1 -1
  157. package/dist/commands/config.d.ts +3 -1
  158. package/dist/commands/config.d.ts.map +1 -1
  159. package/dist/commands/config.js +17 -31
  160. package/dist/commands/config.js.map +1 -1
  161. package/dist/commands/prompt.d.ts.map +1 -1
  162. package/dist/commands/prompt.js +57 -31
  163. package/dist/commands/prompt.js.map +1 -1
  164. package/dist/commands/setup.d.ts +12 -39
  165. package/dist/commands/setup.d.ts.map +1 -1
  166. package/dist/commands/setup.js +72 -303
  167. package/dist/commands/setup.js.map +1 -1
  168. package/dist/commands/step.d.ts +44 -1
  169. package/dist/commands/step.d.ts.map +1 -1
  170. package/dist/commands/step.js +255 -11
  171. package/dist/commands/step.js.map +1 -1
  172. package/dist/commands/thread.d.ts +16 -3
  173. package/dist/commands/thread.d.ts.map +1 -1
  174. package/dist/commands/thread.js +423 -142
  175. package/dist/commands/thread.js.map +1 -1
  176. package/dist/commands/workflow.d.ts +9 -1
  177. package/dist/commands/workflow.d.ts.map +1 -1
  178. package/dist/commands/workflow.js +126 -6
  179. package/dist/commands/workflow.js.map +1 -1
  180. package/dist/concurrency/concurrency.d.ts +34 -0
  181. package/dist/concurrency/concurrency.d.ts.map +1 -0
  182. package/dist/concurrency/concurrency.js +216 -0
  183. package/dist/concurrency/concurrency.js.map +1 -0
  184. package/dist/concurrency/index.d.ts +3 -0
  185. package/dist/concurrency/index.d.ts.map +1 -0
  186. package/dist/concurrency/index.js +2 -0
  187. package/dist/concurrency/index.js.map +1 -0
  188. package/dist/concurrency/types.d.ts +19 -0
  189. package/dist/concurrency/types.d.ts.map +1 -0
  190. package/dist/concurrency/types.js +2 -0
  191. package/dist/concurrency/types.js.map +1 -0
  192. package/dist/format.d.ts +69 -2
  193. package/dist/format.d.ts.map +1 -1
  194. package/dist/format.js +198 -1
  195. package/dist/format.js.map +1 -1
  196. package/dist/moderator/__tests__/evaluate.test.js +31 -17
  197. package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
  198. package/dist/moderator/evaluate.d.ts.map +1 -1
  199. package/dist/moderator/evaluate.js +4 -16
  200. package/dist/moderator/evaluate.js.map +1 -1
  201. package/dist/moderator/index.d.ts +1 -2
  202. package/dist/moderator/index.d.ts.map +1 -1
  203. package/dist/moderator/index.js +0 -1
  204. package/dist/moderator/index.js.map +1 -1
  205. package/dist/moderator/types.d.ts +6 -10
  206. package/dist/moderator/types.d.ts.map +1 -1
  207. package/dist/moderator/types.js +1 -3
  208. package/dist/moderator/types.js.map +1 -1
  209. package/dist/output-mappers.d.ts +122 -0
  210. package/dist/output-mappers.d.ts.map +1 -0
  211. package/dist/output-mappers.js +134 -0
  212. package/dist/output-mappers.js.map +1 -0
  213. package/dist/schemas.d.ts +6 -1
  214. package/dist/schemas.d.ts.map +1 -1
  215. package/dist/schemas.js +34 -5
  216. package/dist/schemas.js.map +1 -1
  217. package/dist/store.d.ts +28 -9
  218. package/dist/store.d.ts.map +1 -1
  219. package/dist/store.js +75 -16
  220. package/dist/store.js.map +1 -1
  221. package/dist/text-renderers.d.ts +30 -0
  222. package/dist/text-renderers.d.ts.map +1 -0
  223. package/dist/text-renderers.js +251 -0
  224. package/dist/text-renderers.js.map +1 -0
  225. package/dist/validate-semantic.d.ts.map +1 -1
  226. package/dist/validate-semantic.js +95 -61
  227. package/dist/validate-semantic.js.map +1 -1
  228. package/dist/validate.d.ts +6 -0
  229. package/dist/validate.d.ts.map +1 -1
  230. package/dist/validate.js +24 -0
  231. package/dist/validate.js.map +1 -1
  232. package/examples/brainstorm.yaml +130 -0
  233. package/examples/debate.yaml +169 -0
  234. package/examples/socratic-questioning.yaml +112 -0
  235. package/package.json +9 -10
  236. package/src/__tests__/adapter-json-roundtrip.test.ts +16 -7
  237. package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
  238. package/src/__tests__/build-step-entry.test.ts +203 -0
  239. package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
  240. package/src/__tests__/concurrency.test.ts +266 -0
  241. package/src/__tests__/config.test.ts +33 -321
  242. package/src/__tests__/current-role.test.ts +7 -6
  243. package/src/__tests__/e2e-mock-agent.test.ts +65 -30
  244. package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
  245. package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
  246. package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
  247. package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
  248. package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
  249. package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
  250. package/src/__tests__/format-text-default.test.ts +49 -0
  251. package/src/__tests__/format-text-registry.test.ts +173 -0
  252. package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
  253. package/src/__tests__/log-text-renderer.test.ts +294 -0
  254. package/src/__tests__/moderator-evaluate.test.ts +9 -52
  255. package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
  256. package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
  257. package/src/__tests__/pid-recycling.test.ts +329 -0
  258. package/src/__tests__/prompt.test.ts +443 -2
  259. package/src/__tests__/resolve-head-hash.test.ts +11 -4
  260. package/src/__tests__/setup-agent-discovery.test.ts +26 -51
  261. package/src/__tests__/setup-complexity.test.ts +1 -203
  262. package/src/__tests__/setup-no-llm.test.ts +68 -0
  263. package/src/__tests__/solve-issue-tea-worktree.test.ts +27 -31
  264. package/src/__tests__/step-ask.test.ts +677 -0
  265. package/src/__tests__/step-show-json.test.ts +1 -0
  266. package/src/__tests__/step-timing.test.ts +2 -0
  267. package/src/__tests__/store-global-cas.test.ts +2 -2
  268. package/src/__tests__/store-unified-threads.test.ts +30 -27
  269. package/src/__tests__/thread-cancel-status.test.ts +27 -20
  270. package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
  271. package/src/__tests__/thread-list-filters.test.ts +443 -17
  272. package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
  273. package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
  274. package/src/__tests__/thread-poke.test.ts +554 -0
  275. package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
  276. package/src/__tests__/thread-resume.test.ts +20 -15
  277. package/src/__tests__/thread-show-status.test.ts +17 -29
  278. package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
  279. package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
  280. package/src/__tests__/thread-suspend-step.test.ts +13 -16
  281. package/src/__tests__/thread-suspended-display.test.ts +10 -22
  282. package/src/__tests__/thread-test-helpers.ts +15 -1
  283. package/src/__tests__/thread.test.ts +14 -14
  284. package/src/__tests__/validate-semantic.test.ts +118 -33
  285. package/src/__tests__/workflow-list-recursive.test.ts +370 -0
  286. package/src/__tests__/workflow-resolution.test.ts +48 -29
  287. package/src/__tests__/workflow-show-resolution.test.ts +286 -0
  288. package/src/__tests__/workflow-validate.test.ts +828 -0
  289. package/src/__tests__/write-envelope.test.ts +257 -0
  290. package/src/background/background.ts +88 -6
  291. package/src/background/index.ts +2 -0
  292. package/src/background/types.ts +1 -0
  293. package/src/cli.ts +184 -77
  294. package/src/commands/config.ts +16 -33
  295. package/src/commands/prompt.ts +57 -31
  296. package/src/commands/setup.ts +80 -358
  297. package/src/commands/step.ts +339 -12
  298. package/src/commands/thread.ts +511 -171
  299. package/src/commands/workflow.ts +155 -4
  300. package/src/concurrency/concurrency.ts +245 -0
  301. package/src/concurrency/index.ts +10 -0
  302. package/src/concurrency/types.ts +19 -0
  303. package/src/format.ts +282 -2
  304. package/src/moderator/__tests__/evaluate.test.ts +34 -17
  305. package/src/moderator/evaluate.ts +5 -17
  306. package/src/moderator/index.ts +1 -6
  307. package/src/moderator/types.ts +6 -14
  308. package/src/output-mappers.ts +254 -0
  309. package/src/schemas.ts +51 -5
  310. package/src/store.ts +86 -20
  311. package/src/text-renderers.ts +355 -0
  312. package/src/validate-semantic.ts +125 -73
  313. package/src/validate.ts +27 -0
  314. package/dist/__tests__/setup-validate.test.d.ts +0 -2
  315. package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
  316. package/dist/__tests__/setup-validate.test.js +0 -108
  317. package/dist/__tests__/setup-validate.test.js.map +0 -1
  318. package/src/__tests__/setup-validate.test.ts +0 -148
  319. /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Per-command text renderers — the per-command registry from spec
3
+ * `cli-format-text-renderer-registry.md`.
4
+ *
5
+ * Each renderer accepts the command's payload (already mapped via the
6
+ * output-mappers module) and returns a human-readable string. Renderers must
7
+ * never throw on partial/missing data and must never return `undefined`.
8
+ *
9
+ * Distinct from the existing Liquid template registry in `format.ts`: this
10
+ * registry is plain JS functions (the spec contract is
11
+ * `Record<string, (data: unknown) => string>`). The Liquid templates remain
12
+ * the primary rendering path inside `writeEnvelope`; these renderers are the
13
+ * fallback contract surface so callers can resolve `text` rendering without
14
+ * needing access to a CAS store.
15
+ */
16
+
17
+ type ThreadListItem = {
18
+ threadId: string;
19
+ workflowHash: string;
20
+ workflowName: string | null;
21
+ status: string;
22
+ currentRole: string | null;
23
+ startedAt: number | null;
24
+ completedAt: number | null;
25
+ };
26
+
27
+ type ThreadListPayload = { items: ThreadListItem[] };
28
+
29
+ type ThreadStatusPayload = {
30
+ threadId: string;
31
+ workflowHash: string;
32
+ head: string | null;
33
+ status: string;
34
+ currentRole: string | null;
35
+ suspendedRole: string | null;
36
+ suspendMessage: string | null;
37
+ done: boolean;
38
+ };
39
+
40
+ type ThreadStartPayload = {
41
+ threadId: string;
42
+ workflowHash: string;
43
+ };
44
+
45
+ type WorkflowListItem = {
46
+ name: string;
47
+ hash: string;
48
+ source: string;
49
+ description: string;
50
+ };
51
+
52
+ type WorkflowListPayload = { items: WorkflowListItem[] };
53
+
54
+ type WorkflowDetailPayload = {
55
+ name: string;
56
+ hash: string;
57
+ version: number;
58
+ description: string;
59
+ roles: Record<string, { description: string; goal: string }>;
60
+ graph: Record<string, Record<string, { role: string; prompt: string }>>;
61
+ };
62
+
63
+ type StepListItem = {
64
+ hash: string;
65
+ role: string;
66
+ durationMs: number | null;
67
+ };
68
+
69
+ type StepListPayload = {
70
+ threadId: string;
71
+ items: StepListItem[];
72
+ };
73
+
74
+ type ThreadCancelPayload = {
75
+ thread: string;
76
+ cancelled: boolean;
77
+ };
78
+
79
+ type ThreadStopPayload = {
80
+ thread: string;
81
+ stopped: boolean;
82
+ };
83
+
84
+ type StepDetailPayload = {
85
+ hash: string;
86
+ role: string;
87
+ agent: string;
88
+ status: string;
89
+ startedAtMs: number | null;
90
+ completedAtMs: number | null;
91
+ durationMs: number | null;
92
+ frontmatter: Record<string, unknown>;
93
+ turns: Array<{ role: string; content: string; timestamp: number | null }>;
94
+ };
95
+
96
+ function asObject(data: unknown): Record<string, unknown> {
97
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
98
+ return data as Record<string, unknown>;
99
+ }
100
+ return {};
101
+ }
102
+
103
+ function asString(value: unknown, fallback = "-"): string {
104
+ if (typeof value === "string" && value.length > 0) return value;
105
+ return fallback;
106
+ }
107
+
108
+ function asArray(value: unknown): unknown[] {
109
+ return Array.isArray(value) ? value : [];
110
+ }
111
+
112
+ function formatDuration(durationMs: unknown): string {
113
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) return "-";
114
+ if (durationMs >= 1000) return `${(durationMs / 1000).toFixed(1)}s`;
115
+ return `${durationMs}ms`;
116
+ }
117
+
118
+ function pad(s: string, width: number): string {
119
+ if (s.length >= width) return s.slice(0, width);
120
+ return s + " ".repeat(width - s.length);
121
+ }
122
+
123
+ function formatTimestamp(ts: unknown): string {
124
+ if (typeof ts !== "number" || !Number.isFinite(ts)) return "-";
125
+ const d = new Date(ts);
126
+ if (Number.isNaN(d.getTime())) return "-";
127
+ const pad2 = (n: number): string => n.toString().padStart(2, "0");
128
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
129
+ }
130
+
131
+ export function renderThreadList(data: unknown): string {
132
+ const payload = asObject(data) as Partial<ThreadListPayload>;
133
+ const items = asArray(payload.items) as ThreadListItem[];
134
+ const lines: string[] = [
135
+ "THREAD WORKFLOW STATUS ROLE STARTED",
136
+ ];
137
+ for (const item of items) {
138
+ const it = asObject(item);
139
+ const threadId = asString(it.threadId);
140
+ const workflowHash = asString(it.workflowHash);
141
+ const status = pad(asString(it.status), 9);
142
+ const role = pad(asString(it.currentRole), 10);
143
+ const started = formatTimestamp(it.startedAt);
144
+ lines.push(`${threadId} ${workflowHash} ${status} ${role} ${started}`);
145
+ }
146
+ return lines.join("\n");
147
+ }
148
+
149
+ export function renderThreadShow(data: unknown): string {
150
+ const p = asObject(data) as Partial<ThreadStatusPayload>;
151
+ const status = asString(p.status);
152
+ const role =
153
+ status === "suspended" && typeof p.suspendedRole === "string" && p.suspendedRole.length > 0
154
+ ? p.suspendedRole
155
+ : asString(p.currentRole);
156
+ const head = asString(p.head);
157
+ const lines = [
158
+ `Thread ${asString(p.threadId)}`,
159
+ `Workflow ${asString(p.workflowHash)}`,
160
+ `Status ${status}`,
161
+ `Role ${role}`,
162
+ `Head ${head}`,
163
+ ];
164
+ if (
165
+ status === "suspended" &&
166
+ typeof p.suspendMessage === "string" &&
167
+ p.suspendMessage.length > 0
168
+ ) {
169
+ lines.push(`Suspend ${p.suspendMessage}`);
170
+ }
171
+ return lines.join("\n");
172
+ }
173
+
174
+ export function renderThreadStart(data: unknown): string {
175
+ const p = asObject(data) as Partial<ThreadStartPayload>;
176
+ return `Thread ${asString(p.threadId)}\nWorkflow ${asString(p.workflowHash)}`;
177
+ }
178
+
179
+ export function renderWorkflowList(data: unknown): string {
180
+ const payload = asObject(data) as Partial<WorkflowListPayload>;
181
+ const items = asArray(payload.items) as WorkflowListItem[];
182
+ const lines: string[] = ["NAME HASH SOURCE DESCRIPTION"];
183
+ for (const item of items) {
184
+ const it = asObject(item);
185
+ const name = pad(asString(it.name), 13);
186
+ const hash = asString(it.hash);
187
+ const source = pad(asString(it.source), 10);
188
+ const description = asString(it.description, "");
189
+ lines.push(`${name} ${hash} ${source} ${description}`);
190
+ }
191
+ return lines.join("\n");
192
+ }
193
+
194
+ export function renderWorkflowShow(data: unknown): string {
195
+ const p = asObject(data) as Partial<WorkflowDetailPayload>;
196
+ const roles =
197
+ p.roles !== null && typeof p.roles === "object" && !Array.isArray(p.roles)
198
+ ? Object.keys(p.roles)
199
+ : [];
200
+ const lines = [
201
+ `Workflow ${asString(p.name)}`,
202
+ `Version ${typeof p.version === "number" ? p.version : "-"}`,
203
+ `Hash ${asString(p.hash)}`,
204
+ `Roles ${roles.join(", ")}`,
205
+ ];
206
+ if (typeof p.description === "string" && p.description.length > 0) {
207
+ lines.push(`Description ${p.description}`);
208
+ }
209
+ return lines.join("\n");
210
+ }
211
+
212
+ export function renderStepList(data: unknown): string {
213
+ const payload = asObject(data) as Partial<StepListPayload>;
214
+ const items = asArray(payload.items) as StepListItem[];
215
+ const lines: string[] = ["HASH ROLE DURATION"];
216
+ for (const item of items) {
217
+ const it = asObject(item);
218
+ const hash = asString(it.hash);
219
+ const role = pad(asString(it.role), 10);
220
+ const dur = formatDuration(it.durationMs);
221
+ lines.push(`${hash} ${role} ${dur}`);
222
+ }
223
+ return lines.join("\n");
224
+ }
225
+
226
+ export function renderStepShow(data: unknown): string {
227
+ const p = asObject(data) as Partial<StepDetailPayload>;
228
+ return [
229
+ `Step ${asString(p.hash)}`,
230
+ `Role ${asString(p.role)}`,
231
+ `Agent ${asString(p.agent)}`,
232
+ `Status ${asString(p.status)}`,
233
+ `Duration ${formatDuration(p.durationMs)}`,
234
+ ].join("\n");
235
+ }
236
+
237
+ export function renderThreadCancel(data: unknown): string {
238
+ const p = asObject(data) as Partial<ThreadCancelPayload>;
239
+ const cancelled = typeof p.cancelled === "boolean" ? (p.cancelled ? "yes" : "no") : "-";
240
+ return [
241
+ `Thread ${asString(p.thread)}`,
242
+ `Status cancelled`,
243
+ `Cancelled ${cancelled}`,
244
+ ].join("\n");
245
+ }
246
+
247
+ export function renderThreadStop(data: unknown): string {
248
+ const p = asObject(data) as Partial<ThreadStopPayload>;
249
+ const stopped = typeof p.stopped === "boolean" ? (p.stopped ? "yes" : "no") : "-";
250
+ return [`Thread ${asString(p.thread)}`, `Stopped ${stopped}`].join("\n");
251
+ }
252
+
253
+ // ── Config renderers ────────────────────────────────────────────────
254
+
255
+ /**
256
+ * Flatten a nested object into dot-notation key-value lines.
257
+ * Arrays are rendered as compact JSON; scalars as strings.
258
+ */
259
+ function flattenConfig(obj: Record<string, unknown>, prefix: string): string[] {
260
+ const lines: string[] = [];
261
+ for (const [key, value] of Object.entries(obj)) {
262
+ const fullKey = prefix ? `${prefix}.${key}` : key;
263
+ if (Array.isArray(value)) {
264
+ lines.push(`${fullKey}\t${JSON.stringify(value)}`);
265
+ } else if (value !== null && typeof value === "object") {
266
+ lines.push(...flattenConfig(value as Record<string, unknown>, fullKey));
267
+ } else {
268
+ lines.push(`${fullKey}\t${String(value)}`);
269
+ }
270
+ }
271
+ return lines;
272
+ }
273
+
274
+ export function renderConfigList(data: unknown): string {
275
+ const obj = asObject(data);
276
+ if (Object.keys(obj).length === 0) return "";
277
+ return flattenConfig(obj as Record<string, unknown>, "").join("\n");
278
+ }
279
+
280
+ export function renderConfigGet(data: unknown): string {
281
+ const obj = asObject(data) as Record<string, unknown>;
282
+ const value = obj.value;
283
+ if (value === null || value === undefined) return "";
284
+ if (typeof value === "object" && !Array.isArray(value)) {
285
+ return flattenConfig(value as Record<string, unknown>, "").join("\n");
286
+ }
287
+ if (Array.isArray(value)) return JSON.stringify(value);
288
+ return String(value);
289
+ }
290
+
291
+ export function renderConfigSet(data: unknown): string {
292
+ const obj = asObject(data) as Record<string, unknown>;
293
+ const key = asString(obj.key as string | undefined);
294
+ const value = obj.value;
295
+ const rendered = Array.isArray(value) ? JSON.stringify(value) : String(value ?? "");
296
+ return `${key} = ${rendered}`;
297
+ }
298
+
299
+ // ── Log renderers ───────────────────────────────────────────────────
300
+
301
+ type LogListItem = {
302
+ name: string;
303
+ size: number;
304
+ date: string;
305
+ };
306
+
307
+ type LogEntry = {
308
+ ts: string;
309
+ pid: string;
310
+ tag: string;
311
+ msg: string;
312
+ thread: string | null;
313
+ workflow: string | null;
314
+ };
315
+
316
+ function formatSize(bytes: unknown): string {
317
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return "-";
318
+ if (bytes < 1024) return `${bytes}B`;
319
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
320
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
321
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
322
+ }
323
+
324
+ export function renderLogList(data: unknown): string {
325
+ const items = asArray(data) as LogListItem[];
326
+ if (items.length === 0) return "No log files.";
327
+ const lines: string[] = ["DATE SIZE NAME"];
328
+ for (const item of items) {
329
+ const it = asObject(item);
330
+ const date = pad(asString(it.date), 11);
331
+ const size = pad(formatSize(it.size), 9);
332
+ const name = asString(it.name);
333
+ lines.push(`${date} ${size} ${name}`);
334
+ }
335
+ return lines.join("\n");
336
+ }
337
+
338
+ export function renderLogShow(data: unknown): string {
339
+ const items = asArray(data) as LogEntry[];
340
+ if (items.length === 0) return "No log entries.";
341
+ const lines: string[] = [];
342
+ for (const item of items) {
343
+ const it = asObject(item);
344
+ const ts = asString(it.ts);
345
+ const pid = asString(it.pid);
346
+ const tag = asString(it.tag);
347
+ const msg = asString(it.msg, "");
348
+ const thread = typeof it.thread === "string" && it.thread.length > 0 ? it.thread : null;
349
+ const parts = [ts, `pid=${pid}`, tag];
350
+ if (thread !== null) parts.push(`thread=${thread}`);
351
+ parts.push(msg);
352
+ lines.push(parts.join(" "));
353
+ }
354
+ return lines.join("\n");
355
+ }
@@ -1,21 +1,11 @@
1
1
  import type { WorkflowPayload } from "@united-workforce/protocol";
2
+ import { Liquid } from "liquidjs";
2
3
 
3
4
  type SchemaObj = Record<string, unknown>;
4
5
 
5
6
  const RESERVED_NAMES = new Set(["$START", "$END", "$SUSPEND"]);
6
- const PSEUDO_TARGETS = new Set(["$END", "$SUSPEND"]);
7
-
8
- /** Extract mustache variable names from a prompt string. */
9
- function extractMustacheVars(prompt: string): string[] {
10
- const vars: string[] = [];
11
- const re = /\{\{\{?([^}]+)\}\}\}?/g;
12
- let m: RegExpExecArray | null = re.exec(prompt);
13
- while (m !== null) {
14
- vars.push(m[1]);
15
- m = re.exec(prompt);
16
- }
17
- return vars;
18
- }
7
+ const PSEUDO_TARGETS = new Set(["$END"]);
8
+ const SUSPEND_TARGET = "$SUSPEND";
19
9
 
20
10
  /** Check if a frontmatter schema is a oneOf (multi-exit) type. */
21
11
  function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } {
@@ -42,13 +32,6 @@ function getConstStatuses(fm: SchemaObj): string[] {
42
32
  return [];
43
33
  }
44
34
 
45
- /** Get property names from a schema object. */
46
- function getPropertyNames(schema: SchemaObj): Set<string> {
47
- const props = schema.properties;
48
- if (typeof props !== "object" || props === null) return new Set();
49
- return new Set(Object.keys(props as Record<string, unknown>));
50
- }
51
-
52
35
  /** Extract $status const values from oneOf variants. */
53
36
  function getOneOfStatuses(variants: SchemaObj[]): string[] {
54
37
  const statuses: string[] = [];
@@ -64,6 +47,76 @@ function getOneOfStatuses(variants: SchemaObj[]): string[] {
64
47
  return statuses;
65
48
  }
66
49
 
50
+ /** Generate mock data from schema property names for template rendering. */
51
+ function generateMockData(schema: SchemaObj): Record<string, string> {
52
+ const mock: Record<string, string> = {};
53
+ const props = schema.properties as Record<string, SchemaObj> | undefined;
54
+ if (!props) return mock;
55
+ for (const key of Object.keys(props)) {
56
+ mock[key] = `mock_${key}`;
57
+ }
58
+ // _body is engine-injected (markdown body after frontmatter), not in the schema.
59
+ // Always provide it so templates referencing {{ _body }} pass strict validation.
60
+ mock._body = "mock__body";
61
+ return mock;
62
+ }
63
+
64
+ /** Extract variable name from a LiquidJS UndefinedVariableError message. */
65
+ function extractVarName(err: unknown): string {
66
+ const msg = String(err);
67
+ const match = msg.match(/undefined variable: ([^,\s]+)/);
68
+ return match ? match[1] : "unknown";
69
+ }
70
+
71
+ /** Validate edge templates using LiquidJS strict-render for a multi-exit role. */
72
+ function validateMultiExitTemplates(
73
+ roleName: string,
74
+ graphEntry: Record<string, { role: string; prompt: string }>,
75
+ variants: SchemaObj[],
76
+ errors: string[],
77
+ ): void {
78
+ const strictEngine = new Liquid({ strictVariables: true });
79
+
80
+ for (const [status, target] of Object.entries(graphEntry)) {
81
+ const variant = variants.find((v) => {
82
+ const props = v.properties as Record<string, SchemaObj> | undefined;
83
+ return props?.$status?.const === status;
84
+ });
85
+ if (!variant) continue;
86
+ const mockData = generateMockData(variant);
87
+ try {
88
+ strictEngine.parseAndRenderSync(target.prompt, mockData);
89
+ } catch (err) {
90
+ const varName = extractVarName(err);
91
+ errors.push(
92
+ `template variable "${varName}" not found in role "${roleName}" variant "${status}"`,
93
+ );
94
+ }
95
+ }
96
+ }
97
+
98
+ /** Validate edge templates using LiquidJS strict-render for a flat schema. */
99
+ function validateFlatTemplates(
100
+ roleName: string,
101
+ graphEntry: Record<string, { role: string; prompt: string }>,
102
+ fm: SchemaObj,
103
+ errors: string[],
104
+ ): void {
105
+ const strictEngine = new Liquid({ strictVariables: true });
106
+ const mockData = generateMockData(fm);
107
+
108
+ for (const [status, target] of Object.entries(graphEntry)) {
109
+ try {
110
+ strictEngine.parseAndRenderSync(target.prompt, mockData);
111
+ } catch (err) {
112
+ const varName = extractVarName(err);
113
+ errors.push(
114
+ `template variable "${varName}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
115
+ );
116
+ }
117
+ }
118
+ }
119
+
67
120
  /** Check reserved names and role/graph reference integrity. */
68
121
  function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
69
122
  const roleNames = new Set(Object.keys(payload.roles));
@@ -89,6 +142,27 @@ function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
89
142
  }
90
143
  }
91
144
 
145
+ /** Validate each graph edge's target role, including the removed $SUSPEND target. */
146
+ function checkEdgeTargets(
147
+ payload: WorkflowPayload,
148
+ roleNames: Set<string>,
149
+ errors: string[],
150
+ ): void {
151
+ for (const [node, statusMap] of Object.entries(payload.graph)) {
152
+ for (const [status, target] of Object.entries(statusMap)) {
153
+ if (target.role === SUSPEND_TARGET) {
154
+ errors.push(
155
+ `edge ${node}→${status}: "${SUSPEND_TARGET}" is no longer a valid graph target. Emit $status: "${SUSPEND_TARGET}" from the "${node}" role output instead.`,
156
+ );
157
+ continue;
158
+ }
159
+ if (!PSEUDO_TARGETS.has(target.role) && !roleNames.has(target.role)) {
160
+ errors.push(`edge ${node}→${status}: unknown target role "${target.role}"`);
161
+ }
162
+ }
163
+ }
164
+ }
165
+
92
166
  /** Check $START/$END constraints, edge targets, and reachability. */
93
167
  function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
94
168
  const roleNames = new Set(Object.keys(payload.roles));
@@ -107,17 +181,13 @@ function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
107
181
  errors.push("$END must not have outgoing edges");
108
182
  }
109
183
 
110
- if (graphNodes.has("$SUSPEND")) {
111
- errors.push("$SUSPEND must not have outgoing edges");
184
+ if (graphNodes.has(SUSPEND_TARGET)) {
185
+ errors.push(
186
+ `"${SUSPEND_TARGET}" is no longer a valid graph node — it is now an engine-level reserved $status. Emit $status: "${SUSPEND_TARGET}" from a role output instead.`,
187
+ );
112
188
  }
113
189
 
114
- for (const [node, statusMap] of Object.entries(payload.graph)) {
115
- for (const [status, target] of Object.entries(statusMap)) {
116
- if (!PSEUDO_TARGETS.has(target.role) && !roleNames.has(target.role)) {
117
- errors.push(`edge ${node}→${status}: unknown target role "${target.role}"`);
118
- }
119
- }
120
- }
190
+ checkEdgeTargets(payload, roleNames, errors);
121
191
 
122
192
  checkReachability(roleNames, collectReachableRoles(payload.graph), errors);
123
193
  }
@@ -207,31 +277,33 @@ function checkStatusEdges(
207
277
  }
208
278
  }
209
279
 
210
- /** Check mustache variables for multi-exit role. */
211
- function checkMultiExitMustache(
212
- roleName: string,
213
- graphEntry: Record<string, { role: string; prompt: string }>,
214
- variants: SchemaObj[],
215
- errors: string[],
216
- ): void {
217
- for (const [status, target] of Object.entries(graphEntry)) {
218
- const vars = extractMustacheVars(target.prompt);
219
- const variant = variants.find((v) => {
220
- const props = v.properties as Record<string, SchemaObj> | undefined;
221
- return props?.$status?.const === status;
222
- });
223
- if (!variant) continue;
224
- const propNames = getPropertyNames(variant);
225
- for (const v of vars) {
226
- if (v === "$status") continue;
227
- if (!propNames.has(v)) {
228
- errors.push(`prompt variable "${v}" not found in role "${roleName}" variant "${status}"`);
280
+ /** Reserved property names that must not appear in frontmatter schemas. */
281
+ const RESERVED_PROPERTIES = new Set(["_body"]);
282
+
283
+ /** Check that frontmatter schemas do not define reserved property names. */
284
+ function checkReservedProperties(roleName: string, fm: unknown, errors: string[]): void {
285
+ const schemasToCheck: SchemaObj[] = [];
286
+
287
+ if (isOneOfSchema(fm)) {
288
+ schemasToCheck.push(...(fm.oneOf as SchemaObj[]));
289
+ } else if (typeof fm === "object" && fm !== null) {
290
+ schemasToCheck.push(fm as SchemaObj);
291
+ }
292
+
293
+ for (const schema of schemasToCheck) {
294
+ const props = schema.properties as Record<string, unknown> | undefined;
295
+ if (!props) continue;
296
+ for (const key of Object.keys(props)) {
297
+ if (RESERVED_PROPERTIES.has(key)) {
298
+ errors.push(
299
+ `role "${roleName}" frontmatter must not define reserved property "${key}" — it is injected by the engine`,
300
+ );
229
301
  }
230
302
  }
231
303
  }
232
304
  }
233
305
 
234
- /** Check status-edge consistency and mustache for each role. */
306
+ /** Check status-edge consistency and template vars for each role. */
235
307
  function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void {
236
308
  for (const [roleName, role] of Object.entries(payload.roles)) {
237
309
  if (RESERVED_NAMES.has(roleName)) continue;
@@ -241,18 +313,19 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
241
313
  const fm = role.frontmatter as unknown;
242
314
  const graphKeys = new Set(Object.keys(graphEntry));
243
315
 
316
+ checkReservedProperties(roleName, fm, errors);
317
+
244
318
  if (isOneOfSchema(fm)) {
245
319
  const variants = fm.oneOf as SchemaObj[];
246
320
  const statuses = getOneOfStatuses(variants);
247
321
 
248
322
  checkOneOfDiscriminant(roleName, variants, statuses, errors);
249
323
  checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
250
- checkMultiExitMustache(roleName, graphEntry, variants, errors);
324
+ validateMultiExitTemplates(roleName, graphEntry, variants, errors);
251
325
  } else if (hasStatusConst(fm)) {
252
326
  const statuses = getConstStatuses(fm as SchemaObj);
253
327
  checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
254
- // For const-based flat schemas, mustache vars come from the flat properties
255
- checkFlatMustache(roleName, graphEntry, fm as SchemaObj, errors);
328
+ validateFlatTemplates(roleName, graphEntry, fm as SchemaObj, errors);
256
329
  } else {
257
330
  errors.push(
258
331
  `role "${roleName}" must define "$status" as const (or oneOf with const) in frontmatter`,
@@ -261,27 +334,6 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
261
334
  }
262
335
  }
263
336
 
264
- /** Check mustache vars in all edge prompts against flat schema properties. */
265
- function checkFlatMustache(
266
- roleName: string,
267
- graphEntry: Record<string, { role: string; prompt: string }>,
268
- fm: SchemaObj,
269
- errors: string[],
270
- ): void {
271
- const propNames = getPropertyNames(fm);
272
- for (const [status, target] of Object.entries(graphEntry)) {
273
- const vars = extractMustacheVars(target.prompt);
274
- for (const v of vars) {
275
- if (v === "$status") continue;
276
- if (!propNames.has(v)) {
277
- errors.push(
278
- `prompt variable "${v}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
279
- );
280
- }
281
- }
282
- }
283
- }
284
-
285
337
  /**
286
338
  * Validate a parsed WorkflowPayload for semantic correctness.
287
339
  * Returns an array of error messages. Empty array = valid.
package/src/validate.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { basename, dirname } from "node:path";
2
2
  import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
3
+ import { CURRENT_WORKFLOW_VERSION } from "@united-workforce/protocol";
3
4
 
4
5
  const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
5
6
 
@@ -113,12 +114,26 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
113
114
  if (typeof raw.name !== "string" || typeof raw.description !== "string") {
114
115
  return null;
115
116
  }
117
+ // version is optional in legacy YAML — falls back to CURRENT_WORKFLOW_VERSION.
118
+ // When present, it MUST be an integer (booleans, strings, floats are rejected).
119
+ if (raw.version !== undefined) {
120
+ if (
121
+ typeof raw.version !== "number" ||
122
+ !Number.isInteger(raw.version) ||
123
+ typeof raw.version === "boolean"
124
+ ) {
125
+ return null;
126
+ }
127
+ }
116
128
  if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
117
129
  return null;
118
130
  }
119
131
 
120
132
  // Normalize location field: undefined → null
121
133
  const normalized = { ...raw } as WorkflowPayload;
134
+ if (normalized.version === undefined || normalized.version === null) {
135
+ normalized.version = CURRENT_WORKFLOW_VERSION;
136
+ }
122
137
  for (const roleName of Object.keys(normalized.graph)) {
123
138
  const statusMap = normalized.graph[roleName];
124
139
  if (statusMap !== undefined) {
@@ -135,3 +150,15 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
135
150
 
136
151
  return normalized;
137
152
  }
153
+
154
+ /**
155
+ * Returns true when the parsed YAML document had no top-level `version` field.
156
+ * Used by `uwf workflow add` to emit a deprecation warning while still
157
+ * accepting legacy workflow YAML.
158
+ */
159
+ export function isMissingVersion(raw: unknown): boolean {
160
+ if (!isRecord(raw)) {
161
+ return false;
162
+ }
163
+ return raw.version === undefined;
164
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=setup-validate.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup-validate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup-validate.test.ts"],"names":[],"mappings":""}