@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,213 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, resolve as resolvePath } from "node:path";
3
+
4
+ import type { JSONSchema } from "@ocas/core";
5
+ import { putSchema, validate } from "@ocas/core";
6
+ import type { CasRef, RoleDefinition, Target, WorkflowPayload } from "@united-workforce/protocol";
7
+ import { parse } from "yaml";
8
+ import { createIncludeTag } from "../include.js";
9
+
10
+ import {
11
+ createUwfStore,
12
+ discoverProjectWorkflows,
13
+ findRegistryName,
14
+ loadWorkflowRegistry,
15
+ resolveWorkflowHash,
16
+ saveWorkflowRegistry,
17
+ type UwfStore,
18
+ } from "../store.js";
19
+ import { checkWorkflowFilenameConsistency, parseWorkflowPayload } from "../validate.js";
20
+ import { validateWorkflow } from "../validate-semantic.js";
21
+
22
+ export type WorkflowOrigin = "local" | "global";
23
+
24
+ export type WorkflowListEntry = {
25
+ name: string;
26
+ hash: CasRef;
27
+ origin: WorkflowOrigin;
28
+ };
29
+
30
+ export type WorkflowAddOutput = {
31
+ name: string;
32
+ hash: CasRef;
33
+ };
34
+
35
+ export type WorkflowShowOutput = {
36
+ hash: CasRef;
37
+ name: string | null;
38
+ type: CasRef;
39
+ payload: WorkflowPayload;
40
+ timestamp: number;
41
+ };
42
+
43
+ function fail(message: string): never {
44
+ process.stderr.write(`${message}\n`);
45
+ process.exit(1);
46
+ }
47
+
48
+ function isJsonSchema(value: unknown): value is JSONSchema {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value);
50
+ }
51
+
52
+ /** Normalize graph: validate each status → target mapping. */
53
+ function normalizeGraph(
54
+ graph: Record<string, Record<string, Target>>,
55
+ ): Record<string, Record<string, Target>> {
56
+ const result: Record<string, Record<string, Target>> = {};
57
+ for (const [node, statusMap] of Object.entries(graph)) {
58
+ const normalized: Record<string, Target> = {};
59
+ for (const [status, target] of Object.entries(statusMap)) {
60
+ if (typeof target.prompt !== "string" || target.prompt.trim() === "") {
61
+ fail(`graph[${node}][${status}] → "${target.role}": prompt is required (non-empty string)`);
62
+ }
63
+ normalized[status] = {
64
+ role: target.role,
65
+ prompt: target.prompt,
66
+ location: target.location ?? null,
67
+ };
68
+ }
69
+ result[node] = normalized;
70
+ }
71
+ return result;
72
+ }
73
+
74
+ async function resolveFrontmatterRef(
75
+ uwf: UwfStore,
76
+ roleName: string,
77
+ frontmatter: unknown,
78
+ ): Promise<CasRef> {
79
+ if (!isJsonSchema(frontmatter)) {
80
+ fail(`role "${roleName}": frontmatter must be a JSON Schema object`);
81
+ }
82
+ const schema: JSONSchema =
83
+ frontmatter.title === undefined ? { ...frontmatter, title: roleName } : frontmatter;
84
+ return putSchema(uwf.store, schema);
85
+ }
86
+
87
+ export async function materializeWorkflowPayload(
88
+ uwf: UwfStore,
89
+ raw: WorkflowPayload,
90
+ ): Promise<WorkflowPayload> {
91
+ const roles: Record<string, RoleDefinition> = {};
92
+ for (const [roleName, role] of Object.entries(raw.roles)) {
93
+ const frontmatter = await resolveFrontmatterRef(
94
+ uwf,
95
+ `${raw.name}.${roleName}`,
96
+ role.frontmatter,
97
+ );
98
+ roles[roleName] = {
99
+ description: role.description,
100
+ goal: role.goal,
101
+ capabilities: role.capabilities,
102
+ procedure: role.procedure,
103
+ output: role.output,
104
+ frontmatter,
105
+ };
106
+ }
107
+ return {
108
+ name: raw.name,
109
+ description: raw.description,
110
+ roles,
111
+ graph: normalizeGraph(raw.graph),
112
+ };
113
+ }
114
+
115
+ export async function cmdWorkflowAdd(
116
+ storageRoot: string,
117
+ filePath: string,
118
+ ): Promise<WorkflowAddOutput> {
119
+ let text: string;
120
+ try {
121
+ text = await readFile(filePath, "utf8");
122
+ } catch {
123
+ fail(`file not found: ${filePath}`);
124
+ }
125
+
126
+ let raw: unknown;
127
+ try {
128
+ raw = parse(text, {
129
+ customTags: [createIncludeTag(dirname(resolvePath(filePath)))],
130
+ }) as unknown;
131
+ } catch (e) {
132
+ fail(`invalid YAML: ${e instanceof Error ? e.message : String(e)}`);
133
+ }
134
+
135
+ const payload = parseWorkflowPayload(raw);
136
+ if (payload === null) {
137
+ fail("invalid workflow YAML: expected WorkflowPayload shape");
138
+ }
139
+
140
+ const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
141
+ if (filenameError !== null) {
142
+ fail(filenameError);
143
+ }
144
+
145
+ const semanticErrors = validateWorkflow(payload);
146
+ if (semanticErrors.length > 0) {
147
+ fail(`workflow validation failed:\n${semanticErrors.map((e) => ` - ${e}`).join("\n")}`);
148
+ }
149
+
150
+ const uwf = await createUwfStore(storageRoot);
151
+ const materialized = await materializeWorkflowPayload(uwf, payload);
152
+
153
+ const hash = await uwf.store.cas.put(uwf.schemas.workflow, materialized);
154
+ const node = uwf.store.cas.get(hash);
155
+ if (node === null || !validate(uwf.store, node)) {
156
+ fail("stored workflow failed schema validation");
157
+ }
158
+
159
+ saveWorkflowRegistry(uwf.varStore, materialized.name, hash);
160
+
161
+ return { name: materialized.name, hash };
162
+ }
163
+
164
+ export async function cmdWorkflowShow(
165
+ storageRoot: string,
166
+ id: string,
167
+ ): Promise<WorkflowShowOutput> {
168
+ const uwf = await createUwfStore(storageRoot);
169
+ const registry = loadWorkflowRegistry(uwf.varStore);
170
+ const hash = resolveWorkflowHash(registry, id);
171
+
172
+ const node = uwf.store.cas.get(hash);
173
+ if (node === null) {
174
+ fail(`CAS node not found: ${hash}`);
175
+ }
176
+ if (node.type !== uwf.schemas.workflow) {
177
+ fail(`node ${hash} is not a Workflow (type ${node.type})`);
178
+ }
179
+
180
+ const payload = node.payload as WorkflowPayload;
181
+ return {
182
+ hash,
183
+ name: findRegistryName(registry, hash),
184
+ type: node.type,
185
+ payload,
186
+ timestamp: node.timestamp,
187
+ };
188
+ }
189
+
190
+ export async function cmdWorkflowList(
191
+ storageRoot: string,
192
+ projectRoot: string,
193
+ ): Promise<WorkflowListEntry[]> {
194
+ const uwf = await createUwfStore(storageRoot);
195
+ const localEntries = await discoverProjectWorkflows(projectRoot);
196
+ const registry = loadWorkflowRegistry(uwf.varStore);
197
+
198
+ const result: WorkflowListEntry[] = [];
199
+ const localNames = new Set<string>();
200
+
201
+ for (const entry of localEntries) {
202
+ localNames.add(entry.name);
203
+ result.push({ name: entry.name, hash: "(local)", origin: "local" });
204
+ }
205
+
206
+ for (const [name, hash] of Object.entries(registry)) {
207
+ if (!localNames.has(name)) {
208
+ result.push({ name, hash, origin: "global" });
209
+ }
210
+ }
211
+
212
+ return result;
213
+ }
package/src/format.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { stringify } from "yaml";
2
+
3
+ export type OutputFormat = "json" | "yaml";
4
+
5
+ export function formatOutput(data: unknown, format: OutputFormat): string {
6
+ switch (format) {
7
+ case "json":
8
+ return JSON.stringify(data);
9
+ case "yaml":
10
+ return stringify(data, { aliasDuplicateObjects: false }).trimEnd();
11
+ }
12
+ }
package/src/include.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, extname, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+
5
+ /**
6
+ * Create a YAML customTags entry for !include that resolves file paths
7
+ * relative to the given base directory.
8
+ *
9
+ * Security: resolved paths must stay within baseDir (path traversal prevention).
10
+ * Nested !include in .yaml/.yml files is supported (customTags passed recursively).
11
+ */
12
+ export function createIncludeTag(baseDir: string) {
13
+ const resolvedBase = resolve(baseDir);
14
+ return {
15
+ tag: "!include",
16
+ resolve(str: string) {
17
+ const filePath = resolve(resolvedBase, str);
18
+ // Path traversal guard: resolved path must be inside baseDir
19
+ if (!filePath.startsWith(`${resolvedBase}/`) && filePath !== resolvedBase) {
20
+ throw new Error(
21
+ `!include path traversal blocked: "${str}" resolves outside base directory`,
22
+ );
23
+ }
24
+ const content = readFileSync(filePath, "utf8");
25
+ const ext = extname(filePath).toLowerCase();
26
+ if (ext === ".json") {
27
+ return JSON.parse(content);
28
+ }
29
+ if (ext === ".yaml" || ext === ".yml") {
30
+ // Pass customTags recursively so nested !include works,
31
+ // scoped to the included file's directory
32
+ return parseYaml(content, { customTags: [createIncludeTag(dirname(filePath))] });
33
+ }
34
+ return content;
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,199 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { evaluate } from "../evaluate.js";
3
+ import { isSuspendResult } from "../types.js";
4
+
5
+ describe("Edge prompt template variable resolution", () => {
6
+ test("returns error when rendered prompt is empty string", () => {
7
+ const graph = {
8
+ $START: {
9
+ _: { role: "classifier", prompt: "{{{userPrompt}}}", location: null },
10
+ },
11
+ };
12
+
13
+ const result = evaluate(graph, "$START", {});
14
+
15
+ expect(result.ok).toBe(false);
16
+ if (!result.ok) {
17
+ expect(result.error.message).toContain("prompt");
18
+ expect(result.error.message).toContain("empty");
19
+ }
20
+ });
21
+
22
+ test("returns error when rendered prompt is whitespace-only", () => {
23
+ const graph = {
24
+ $START: {
25
+ _: { role: "classifier", prompt: " {{{userPrompt}}} ", location: null },
26
+ },
27
+ };
28
+
29
+ const result = evaluate(graph, "$START", {});
30
+
31
+ expect(result.ok).toBe(false);
32
+ if (!result.ok) {
33
+ expect(result.error.message).toContain("prompt");
34
+ expect(result.error.message).toContain("empty");
35
+ }
36
+ });
37
+
38
+ test("succeeds when all template variables resolve to non-empty values", () => {
39
+ const graph = {
40
+ $START: {
41
+ _: { role: "classifier", prompt: "{{{userPrompt}}}", location: null },
42
+ },
43
+ };
44
+
45
+ const result = evaluate(graph, "$START", { userPrompt: "Fix the bug" });
46
+
47
+ expect(result.ok).toBe(true);
48
+ if (result.ok) {
49
+ expect(result.value.prompt).toBe("Fix the bug");
50
+ }
51
+ });
52
+
53
+ test("succeeds with static (no-variable) prompt", () => {
54
+ const graph = {
55
+ $START: {
56
+ _: { role: "classifier", prompt: "Classify this input", location: null },
57
+ },
58
+ };
59
+
60
+ const result = evaluate(graph, "$START", {});
61
+
62
+ expect(result.ok).toBe(true);
63
+ if (result.ok) {
64
+ expect(result.value.prompt).toBe("Classify this input");
65
+ }
66
+ });
67
+
68
+ test("succeeds when prompt has mix of static text and unresolved variables", () => {
69
+ const graph = {
70
+ $START: {
71
+ _: { role: "classifier", prompt: "Please handle: {{{userPrompt}}}", location: null },
72
+ },
73
+ };
74
+
75
+ const result = evaluate(graph, "$START", {});
76
+
77
+ expect(result.ok).toBe(true);
78
+ if (result.ok) {
79
+ expect(result.value.prompt).toBe("Please handle: ");
80
+ }
81
+ });
82
+
83
+ test("returns error when ALL variables missing and no static text remains", () => {
84
+ const graph = {
85
+ $START: {
86
+ _: { role: "classifier", prompt: "{{{a}}}{{{b}}}", location: null },
87
+ },
88
+ };
89
+
90
+ const result = evaluate(graph, "$START", {});
91
+
92
+ expect(result.ok).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe("Moderator location resolution", () => {
97
+ test("returns null location when edge has no location field", () => {
98
+ const graph = {
99
+ planner: {
100
+ ready: {
101
+ role: "coder",
102
+ prompt: "Implement the code",
103
+ location: null,
104
+ },
105
+ },
106
+ };
107
+
108
+ const result = evaluate(graph, "planner", { $status: "ready" });
109
+
110
+ expect(result.ok).toBe(true);
111
+ if (result.ok && !isSuspendResult(result.value)) {
112
+ expect(result.value.location).toBe(null);
113
+ }
114
+ });
115
+
116
+ test("resolves static location string", () => {
117
+ const graph = {
118
+ planner: {
119
+ ready: {
120
+ role: "coder",
121
+ prompt: "Implement the code",
122
+ location: "/static/path",
123
+ },
124
+ },
125
+ };
126
+
127
+ const result = evaluate(graph, "planner", { $status: "ready" });
128
+
129
+ expect(result.ok).toBe(true);
130
+ if (result.ok && !isSuspendResult(result.value)) {
131
+ expect(result.value.location).toBe("/static/path");
132
+ }
133
+ });
134
+
135
+ test("resolves mustache template location", () => {
136
+ const graph = {
137
+ planner: {
138
+ ready: {
139
+ role: "coder",
140
+ prompt: "Implement the code",
141
+ location: "{{{repoPath}}}",
142
+ },
143
+ },
144
+ };
145
+
146
+ const result = evaluate(graph, "planner", {
147
+ $status: "ready",
148
+ repoPath: "/home/user/repo",
149
+ });
150
+
151
+ expect(result.ok).toBe(true);
152
+ if (result.ok && !isSuspendResult(result.value)) {
153
+ expect(result.value.location).toBe("/home/user/repo");
154
+ }
155
+ });
156
+
157
+ test("resolves mustache template with multiple variables", () => {
158
+ const graph = {
159
+ planner: {
160
+ ready: {
161
+ role: "coder",
162
+ prompt: "Implement the code",
163
+ location: "{{{basePath}}}/{{{projectName}}}",
164
+ },
165
+ },
166
+ };
167
+
168
+ const result = evaluate(graph, "planner", {
169
+ $status: "ready",
170
+ basePath: "/home/user",
171
+ projectName: "myproject",
172
+ });
173
+
174
+ expect(result.ok).toBe(true);
175
+ if (result.ok && !isSuspendResult(result.value)) {
176
+ expect(result.value.location).toBe("/home/user/myproject");
177
+ }
178
+ });
179
+
180
+ test("handles missing template variable gracefully", () => {
181
+ const graph = {
182
+ planner: {
183
+ ready: {
184
+ role: "coder",
185
+ prompt: "Implement the code",
186
+ location: "{{{repoPath}}}",
187
+ },
188
+ },
189
+ };
190
+
191
+ const result = evaluate(graph, "planner", { $status: "ready" });
192
+
193
+ expect(result.ok).toBe(true);
194
+ if (result.ok && !isSuspendResult(result.value)) {
195
+ // Mustache renders missing variables as empty string
196
+ expect(result.value.location).toBe("");
197
+ }
198
+ });
199
+ });
@@ -0,0 +1,80 @@
1
+ import type { Target } from "@united-workforce/protocol";
2
+ import mustache from "mustache";
3
+
4
+ import type { EvaluateResult, Result } from "./types.js";
5
+
6
+ // Disable HTML escaping — prompts are plain text, not HTML.
7
+ mustache.escape = (text: string) => text;
8
+
9
+ const START_ROLE = "$START";
10
+ const SUSPEND_ROLE = "$SUSPEND";
11
+ // $START is a special entry node with no agent output — it always uses this key.
12
+ const START_STATUS = "_";
13
+
14
+ type LastOutput = Record<string, unknown>;
15
+
16
+ const STATUS_KEY = "$status";
17
+
18
+ export function evaluate(
19
+ graph: Record<string, Record<string, Target>>,
20
+ lastRole: string,
21
+ lastOutput: LastOutput,
22
+ ): Result<EvaluateResult, Error> {
23
+ let status: string;
24
+ if (lastRole === START_ROLE) {
25
+ status = START_STATUS;
26
+ } else if (typeof lastOutput[STATUS_KEY] === "string") {
27
+ status = lastOutput[STATUS_KEY] as string;
28
+ } else {
29
+ return {
30
+ ok: false,
31
+ error: new Error(`agent output for role "${lastRole}" is missing required "$status" string`),
32
+ };
33
+ }
34
+
35
+ const roleTargets = graph[lastRole];
36
+ if (roleTargets === undefined) {
37
+ return {
38
+ ok: false,
39
+ error: new Error(`no transitions defined for role "${lastRole}"`),
40
+ };
41
+ }
42
+
43
+ const target = roleTargets[status];
44
+ if (target === undefined) {
45
+ return {
46
+ ok: false,
47
+ error: new Error(`no transition for role "${lastRole}" with status "${status}"`),
48
+ };
49
+ }
50
+
51
+ try {
52
+ const prompt = mustache.render(target.prompt, lastOutput);
53
+ if (prompt.trim() === "") {
54
+ return {
55
+ ok: false,
56
+ error: new Error(
57
+ `edge prompt resolved to empty string for role "${target.role}" (template: "${target.prompt}"). Check that upstream output includes required variables.`,
58
+ ),
59
+ };
60
+ }
61
+ if (target.role === SUSPEND_ROLE) {
62
+ return {
63
+ ok: true,
64
+ value: {
65
+ action: "suspend",
66
+ suspendedRole: lastRole,
67
+ prompt,
68
+ },
69
+ };
70
+ }
71
+
72
+ const location = target.location !== null ? mustache.render(target.location, lastOutput) : null;
73
+ return { ok: true, value: { role: target.role, prompt, location } };
74
+ } catch (error) {
75
+ return {
76
+ ok: false,
77
+ error: error instanceof Error ? error : new Error(String(error)),
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,7 @@
1
+ export { evaluate } from "./evaluate.js";
2
+ export type {
3
+ EvaluateResult,
4
+ EvaluateRouteResult,
5
+ EvaluateSuspendResult,
6
+ } from "./types.js";
7
+ export { isSuspendResult } from "./types.js";
@@ -0,0 +1,24 @@
1
+ export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
2
+
3
+ /** Moderator routes the thread to a real role (or `$END`). */
4
+ export type EvaluateRouteResult = {
5
+ role: string;
6
+ prompt: string;
7
+ /** Resolved working directory from edge location field (null = inherit thread cwd). */
8
+ location: string | null;
9
+ };
10
+
11
+ /** Moderator routes the thread to `$SUSPEND` — waiting for external input. */
12
+ export type EvaluateSuspendResult = {
13
+ action: "suspend";
14
+ /** Role whose output triggered the suspend transition. */
15
+ suspendedRole: string;
16
+ prompt: string;
17
+ };
18
+
19
+ /** The result of moderator evaluation. */
20
+ export type EvaluateResult = EvaluateRouteResult | EvaluateSuspendResult;
21
+
22
+ export function isSuspendResult(result: EvaluateResult): result is EvaluateSuspendResult {
23
+ return "action" in result && result.action === "suspend";
24
+ }
package/src/schemas.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { Hash, Store } from "@ocas/core";
2
+ import { putSchema } from "@ocas/core";
3
+ import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@united-workforce/protocol";
4
+
5
+ export const TEXT_SCHEMA = { type: "string" as const };
6
+
7
+ export type UwfSchemaHashes = {
8
+ workflow: Hash;
9
+ startNode: Hash;
10
+ stepNode: Hash;
11
+ text: Hash;
12
+ };
13
+
14
+ /**
15
+ * Register Workflow, StartNode, and StepNode JSON Schemas in the CAS store.
16
+ * Idempotent: safe to call on every CLI invocation.
17
+ */
18
+ export async function registerUwfSchemas(store: Store): Promise<UwfSchemaHashes> {
19
+ const [workflow, startNode, stepNode, text] = await Promise.all([
20
+ putSchema(store, WORKFLOW_SCHEMA),
21
+ putSchema(store, START_NODE_SCHEMA),
22
+ putSchema(store, STEP_NODE_SCHEMA),
23
+ putSchema(store, TEXT_SCHEMA),
24
+ ]);
25
+ return { workflow, startNode, stepNode, text };
26
+ }