@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,169 @@
1
+ version: 1
2
+ name: debate
3
+ description: "Multi-role structured debate with critical thinking framework and host summary."
4
+
5
+ # Shared frontmatter schema for debater roles (YAML anchor)
6
+ x-debater-frontmatter: &debater-frontmatter
7
+ type: object
8
+ oneOf:
9
+ - properties:
10
+ $status: { const: speak }
11
+ argument: { type: string }
12
+ required: [$status, argument]
13
+ - properties:
14
+ $status: { const: conceded }
15
+ reason: { type: string }
16
+ required: [$status, reason]
17
+ - properties:
18
+ $status: { const: final }
19
+ closing: { type: string }
20
+ required: [$status, closing]
21
+
22
+ roles:
23
+ proponent:
24
+ description: "Argues FOR the proposition"
25
+ goal: "Build a compelling case for the proposition through logical reasoning and evidence"
26
+ capabilities: []
27
+ procedure: |
28
+ You are an experienced scholar arguing FOR the proposition.
29
+
30
+ ## Critical Thinking Framework (execute before every speech)
31
+
32
+ ### A. Pre-speech reflection (internal, do not output)
33
+ - Does every step in my argument chain hold? Any hidden assumptions or logical gaps?
34
+ - If I were my opponent, how would I attack this? Where am I weakest?
35
+ - Does my evidence actually support my claim, or could it backfire?
36
+ - Should I go on offense or defense this round?
37
+
38
+ ### B. Evidence discipline
39
+ - Verify key numbers — watch for order-of-magnitude errors
40
+ - Assess data freshness — fast-moving fields have short half-lives
41
+ - Distinguish primary data from secondary citations, expert opinion, and common assumptions
42
+
43
+ ### C. Anti-fragility
44
+ - Anticipate counterarguments; preemptively strengthen or strategically abandon weak points
45
+ - Catch logical gaps, data misuse, or outdated claims in your opponent's reasoning
46
+
47
+ ## Rules
48
+ 1. Check Thread Progress to see how many times you have spoken.
49
+ 2. On your 3rd speech, you MUST output $status: final (closing statement).
50
+ 3. If genuinely convinced by the opponent, output $status: conceded.
51
+ 4. Otherwise output $status: speak and counter the opponent's points.
52
+ 5. Be rigorous, cite evidence, stay concise.
53
+ output: "Debate argument"
54
+ frontmatter: *debater-frontmatter
55
+
56
+ opponent:
57
+ description: "Argues AGAINST the proposition"
58
+ goal: "Build a compelling case against the proposition through logical reasoning and evidence"
59
+ capabilities: []
60
+ procedure: |
61
+ You are an experienced scholar arguing AGAINST the proposition.
62
+
63
+ ## Critical Thinking Framework (execute before every speech)
64
+
65
+ ### A. Pre-speech reflection (internal, do not output)
66
+ - Does every step in my argument chain hold? Any hidden assumptions or logical gaps?
67
+ - If I were my opponent, how would I attack this? Where am I weakest?
68
+ - Does my evidence actually support my claim, or could it backfire?
69
+ - Should I go on offense or defense this round?
70
+
71
+ ### B. Evidence discipline
72
+ - Verify key numbers — watch for order-of-magnitude errors
73
+ - Assess data freshness — fast-moving fields have short half-lives
74
+ - Distinguish primary data from secondary citations, expert opinion, and common assumptions
75
+
76
+ ### C. Anti-fragility
77
+ - Anticipate counterarguments; preemptively strengthen or strategically abandon weak points
78
+ - Catch logical gaps, data misuse, or outdated claims in your opponent's reasoning
79
+
80
+ ## Rules
81
+ 1. Check Thread Progress to see how many times you have spoken.
82
+ 2. On your 3rd speech, or when the proponent has issued a final statement, you MUST output $status: final.
83
+ 3. If genuinely convinced by the proponent, output $status: conceded.
84
+ 4. Otherwise output $status: speak and counter the proponent's points.
85
+ 5. Be rigorous, cite evidence, stay concise.
86
+ output: "Debate argument"
87
+ frontmatter: *debater-frontmatter
88
+
89
+ host:
90
+ description: "Debate moderator — delivers impartial summary and verdict"
91
+ goal: "Objectively review the debate, analyze both sides, and deliver a verdict"
92
+ capabilities: []
93
+ procedure: |
94
+ You are an experienced academic debate moderator.
95
+
96
+ ## Task
97
+ 1. Outline each side's core arguments
98
+ 2. Evaluate reasoning quality and evidence use
99
+ 3. Highlight the most impactful exchanges
100
+ 4. Analyze the deeper significance of the topic
101
+ 5. Deliver an overall verdict
102
+
103
+ ## Style
104
+ - Impartial but with independent judgment
105
+ - Substantive, not superficial
106
+ output: "Debate summary report"
107
+ frontmatter:
108
+ type: object
109
+ properties:
110
+ $status: { const: done }
111
+ summary: { type: string }
112
+ highlights: { type: string }
113
+ verdict: { type: string }
114
+ required: [$status, summary, highlights, verdict]
115
+
116
+ reporter:
117
+ description: "Generates an HTML report of the completed debate"
118
+ goal: "Produce a self-contained, visually polished HTML report that faithfully reproduces the entire debate with no paraphrasing."
119
+ capabilities: [run_command, write_file, read_file]
120
+ procedure: |
121
+ You are a report generator for a structured debate.
122
+
123
+ ## Steps
124
+ 1. Read Thread Progress to get the thread ID.
125
+ 2. Run: `uwf thread read <thread-id>` to get the full debate transcript.
126
+ 3. Design and write a self-contained HTML file that presents the debate beautifully.
127
+ 4. Save to `/tmp/debate-report-<thread-id>.html`.
128
+
129
+ ## HTML Report Requirements
130
+ - Title: the debate proposition
131
+ - Two-column layout comparing proponent vs opponent arguments round by round
132
+ - Key exchanges highlighted with visual callouts
133
+ - Host's verdict and summary in a prominent section
134
+ - If a side conceded, make it visually clear
135
+ - Clean typography, responsive layout, dark/light theme friendly
136
+ - All content verbatim from `uwf thread read` — do NOT paraphrase
137
+
138
+ ## Rules
139
+ 1. You MUST use `uwf thread read` to get the data — do NOT rely on context.
140
+ 2. Completely self-contained (inline styles, no CDN links).
141
+ 3. Output the file path in your response body.
142
+ output: "Confirmation that the report was generated, with the file path."
143
+ frontmatter:
144
+ type: object
145
+ properties:
146
+ $status: { const: done }
147
+ reportPath: { type: string }
148
+ required: [$status, reportPath]
149
+
150
+ graph:
151
+ $START:
152
+ new: { role: proponent, prompt: "The debate begins. You are arguing FOR the proposition. Present your opening argument." }
153
+ resume: { role: proponent, prompt: "The debate continues." }
154
+
155
+ proponent:
156
+ speak: { role: opponent, prompt: "Proponent argues:\n\n{{ argument }}\n\nYou are the opponent. Counter this argument." }
157
+ conceded: { role: host, prompt: "The proponent conceded: {{ reason }}\n\nPlease summarize the debate." }
158
+ final: { role: opponent, prompt: "Proponent's closing statement:\n\n{{ closing }}\n\nYou are the opponent. Deliver your final response." }
159
+
160
+ opponent:
161
+ speak: { role: proponent, prompt: "Opponent argues:\n\n{{ argument }}\n\nYou are the proponent. Counter this argument." }
162
+ conceded: { role: host, prompt: "The opponent conceded: {{ reason }}\n\nPlease summarize the debate." }
163
+ final: { role: host, prompt: "Opponent's closing statement:\n\n{{ closing }}\n\nThe debate is over. Please summarize." }
164
+
165
+ host:
166
+ done: { role: reporter, prompt: "Debate summary complete.\n\nSummary: {{ summary }}\nHighlights: {{ highlights }}\nVerdict: {{ verdict }}\n\nGenerate a polished HTML report of the entire debate." }
167
+
168
+ reporter:
169
+ done: { role: "$END", prompt: "Report generated." }
@@ -0,0 +1,112 @@
1
+ version: 1
2
+ name: socratic-questioning
3
+ description: "Socratic dialogue — a questioner helps the thinker examine their beliefs through probing questions."
4
+
5
+ roles:
6
+ thinker:
7
+ description: "Holds and defends a position, refining it through dialogue"
8
+ goal: "Articulate and examine your position honestly. When a question exposes a weakness, acknowledge it and refine your view rather than deflecting."
9
+ capabilities: []
10
+ procedure: |
11
+ You are a thoughtful person exploring your own beliefs through dialogue.
12
+
13
+ ## Rules
14
+ 1. Check Thread Progress to see how many times you have responded.
15
+ 2. On your 3rd response, you MUST output $status: synthesis (your refined position).
16
+ 3. Be honest — if a question reveals a gap, say so and adjust.
17
+ 4. Build on previous exchanges, don't repeat yourself.
18
+ 5. Keep responses focused (under 300 words).
19
+ output: "Your response to the question, or your final synthesis."
20
+ frontmatter:
21
+ oneOf:
22
+ - properties:
23
+ $status: { const: respond }
24
+ insight: { type: string }
25
+ required: [$status, insight]
26
+ - properties:
27
+ $status: { const: synthesis }
28
+ revisedPosition: { type: string }
29
+ keyInsights: { type: string }
30
+ required: [$status, revisedPosition, keyInsights]
31
+
32
+ questioner:
33
+ description: "Asks probing questions to help the thinker examine assumptions"
34
+ goal: "Guide the thinker to deeper understanding through questions — never lecture, never argue, never assert your own position."
35
+ capabilities: []
36
+ procedure: |
37
+ You are a Socratic questioner. Your only tool is the question.
38
+
39
+ ## Questioning techniques
40
+ - **Clarification**: "What do you mean by...?" / "Can you give an example?"
41
+ - **Assumptions**: "What are you assuming here?" / "Why do you believe that's true?"
42
+ - **Evidence**: "What evidence supports this?" / "How would you respond to someone who says...?"
43
+ - **Implications**: "If that's true, what follows?" / "What would change if you're wrong?"
44
+ - **Perspective**: "How might someone in a different situation see this?"
45
+
46
+ ## Rules
47
+ 1. Ask 1-2 questions per turn, not more.
48
+ 2. NEVER state your own opinion or argue a position.
49
+ 3. Build on the thinker's previous answer — don't jump to unrelated topics.
50
+ 4. When the thinker reaches a synthesis, output $status: done.
51
+ 5. Keep questions concise and sharp.
52
+ output: "Your probing question(s)."
53
+ frontmatter:
54
+ oneOf:
55
+ - properties:
56
+ $status: { const: ask }
57
+ question: { type: string }
58
+ required: [$status, question]
59
+ - properties:
60
+ $status: { const: done }
61
+ summary: { type: string }
62
+ required: [$status, summary]
63
+
64
+ reporter:
65
+ description: "Generates an HTML report of the completed Socratic dialogue"
66
+ goal: "Produce a self-contained, visually polished HTML report that faithfully reproduces the entire dialogue with no paraphrasing."
67
+ capabilities: [run_command, write_file, read_file]
68
+ procedure: |
69
+ You are a report generator for a Socratic questioning session.
70
+
71
+ ## Steps
72
+ 1. Read Thread Progress to get the thread ID.
73
+ 2. Run: `uwf thread read <thread-id>` to get the full dialogue transcript.
74
+ 3. Design and write a self-contained HTML file that presents the dialogue beautifully.
75
+ 4. Save to `/tmp/socratic-report-<thread-id>.html`.
76
+
77
+ ## HTML Report Requirements
78
+ - Title: the original topic/position being examined
79
+ - Each exchange as a dialogue card (questioner question → thinker response)
80
+ - Visual distinction between questions and reflections
81
+ - The thinker's initial position and final synthesis prominently displayed
82
+ - A "journey" section showing how the position evolved
83
+ - Clean typography, responsive layout, dark/light theme friendly
84
+ - All content verbatim from `uwf thread read` — do NOT paraphrase
85
+
86
+ ## Rules
87
+ 1. You MUST use `uwf thread read` to get the data — do NOT rely on context.
88
+ 2. Completely self-contained (inline styles, no CDN links).
89
+ 3. Output the file path in your response body.
90
+ output: "Confirmation that the report was generated, with the file path."
91
+ frontmatter:
92
+ type: object
93
+ properties:
94
+ $status: { const: done }
95
+ reportPath: { type: string }
96
+ required: [$status, reportPath]
97
+
98
+ graph:
99
+ $START:
100
+ new: { role: thinker, prompt: "State your initial position on the topic. Be clear about what you believe and why." }
101
+ resume: { role: thinker, prompt: "Continue the dialogue from where you left off." }
102
+
103
+ thinker:
104
+ respond: { role: questioner, prompt: "The thinker says:\n\n{{ _body }}\n\nAsk a probing question to help them examine this further." }
105
+ synthesis: { role: questioner, prompt: "The thinker has reached a synthesis:\n\n{{ _body }}\n\nBriefly summarize the journey of this dialogue." }
106
+
107
+ questioner:
108
+ ask: { role: thinker, prompt: "The questioner asks:\n\n{{ _body }}\n\nReflect on this question and respond honestly." }
109
+ done: { role: reporter, prompt: "Dialogue complete.\n\nSummary: {{ summary }}\n\nGenerate a polished HTML report of the entire Socratic dialogue." }
110
+
111
+ reporter:
112
+ done: { role: "$END", prompt: "Report generated." }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@united-workforce/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
7
+ "examples",
7
8
  "package.json"
8
9
  ],
9
10
  "type": "module",
@@ -11,22 +12,20 @@
11
12
  "uwf": "./dist/cli.js"
12
13
  },
13
14
  "dependencies": {
14
- "@ocas/core": "^0.3.0",
15
- "@ocas/fs": "^0.3.0",
15
+ "@ocas/core": "^0.5.0",
16
+ "@ocas/fs": "^0.4.1",
16
17
  "commander": "^14.0.3",
17
18
  "dotenv": "^16.6.1",
18
- "mustache": "^4.2.0",
19
+ "liquidjs": "^10.27.0",
19
20
  "yaml": "^2.8.4",
20
- "@united-workforce/protocol": "^0.1.0",
21
- "@united-workforce/util": "^0.1.3",
22
- "@united-workforce/util-agent": "^0.1.0"
21
+ "@united-workforce/util": "^0.2.0",
22
+ "@united-workforce/util-agent": "^0.2.1",
23
+ "@united-workforce/protocol": "^0.3.0"
23
24
  },
24
25
  "publishConfig": {
25
26
  "access": "public"
26
27
  },
27
- "devDependencies": {
28
- "@types/mustache": "^4.2.6"
29
- },
28
+ "devDependencies": {},
30
29
  "repository": {
31
30
  "type": "git",
32
31
  "url": "https://git.shazhou.work/shazhou/united-workforce.git",
@@ -25,12 +25,19 @@ const OUTPUT_SCHEMA = {
25
25
  // ── fixture ──────────────────────────────────────────────────────────────────
26
26
 
27
27
  let tmpDir: string;
28
+ let savedOcasHome: string | undefined;
28
29
 
29
30
  beforeEach(async () => {
31
+ savedOcasHome = process.env.OCAS_HOME;
30
32
  tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-roundtrip-test-"));
31
33
  });
32
34
 
33
35
  afterEach(async () => {
36
+ if (savedOcasHome === undefined) {
37
+ delete process.env.OCAS_HOME;
38
+ } else {
39
+ process.env.OCAS_HOME = savedOcasHome;
40
+ }
34
41
  await rm(tmpDir, { recursive: true, force: true });
35
42
  });
36
43
 
@@ -119,7 +126,7 @@ describe("C1: adapter JSON round-trip integration", () => {
119
126
  const configPath = join(tmpDir, "config.yaml");
120
127
  await writeFile(
121
128
  configPath,
122
- `defaultAgent: uwf-hermes\ndefaultModel: test-model\nagentOverrides: null\nagents: {}\nproviders: {}\nmodels: {}\n`,
129
+ `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
123
130
  );
124
131
 
125
132
  // 5. Run CLI with agent override pointing to our mock
@@ -131,7 +138,7 @@ describe("C1: adapter JSON round-trip integration", () => {
131
138
  try {
132
139
  stdout = execFileSync(
133
140
  process.execPath,
134
- [cliPath, "thread", "exec", threadId, "--agent", mockAgentPath],
141
+ [cliPath, "--format", "raw-json", "thread", "exec", threadId, "--agent", mockAgentPath],
135
142
  {
136
143
  encoding: "utf8",
137
144
  stdio: ["ignore", "pipe", "pipe"],
@@ -162,15 +169,17 @@ describe("C1: adapter JSON round-trip integration", () => {
162
169
  throw new Error(`CLI exited with code ${exitCode}\nstdout: ${stdout}\nstderr: ${stderr}`);
163
170
  }
164
171
 
165
- // Parse CLI output
172
+ // Parse CLI output (raw-json envelope value: { threadId, workflowHash, steps: [...] })
166
173
  const cliOutput = JSON.parse(stdout.trim());
167
- expect(cliOutput).toHaveProperty("thread", threadId);
168
- expect(cliOutput).toHaveProperty("head", stepHash);
169
- expect(cliOutput.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/);
174
+ expect(cliOutput).toHaveProperty("threadId", threadId);
175
+ expect(cliOutput.steps).toHaveLength(1);
176
+ const firstStep = cliOutput.steps[0];
177
+ expect(firstStep).toHaveProperty("head", stepHash);
178
+ expect(firstStep.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/);
170
179
 
171
180
  // Verify the CAS step node exists and has correct metadata
172
181
  const storeAfter = await openStore(casDir);
173
- const stepNode = storeAfter.cas.get(cliOutput.head as CasRef);
182
+ const stepNode = storeAfter.cas.get(firstStep.head as CasRef);
174
183
  expect(stepNode).not.toBeNull();
175
184
 
176
185
  const payload = stepNode!.payload as StepNodePayload;
@@ -0,0 +1,39 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { loadWorkflowConfig } from "@united-workforce/util-agent";
5
+ import { afterEach, describe, expect, test } from "vitest";
6
+
7
+ describe("agent resolution works without LLM fields in config.yaml (issue #143)", () => {
8
+ let tempDir: string;
9
+ afterEach(() => {
10
+ if (tempDir) rmSync(tempDir, { recursive: true, force: true });
11
+ });
12
+
13
+ test("loadWorkflowConfig succeeds on a minimal engine-only config", async () => {
14
+ tempDir = mkdtempSync(join(tmpdir(), "uwf-engine-cfg-"));
15
+ writeFileSync(
16
+ join(tempDir, "config.yaml"),
17
+ "agents:\n hermes: { command: uwf-hermes, args: [] }\ndefaultAgent: hermes\n",
18
+ "utf8",
19
+ );
20
+ const cfg = await loadWorkflowConfig(tempDir);
21
+ expect(cfg.defaultAgent).toBe("hermes");
22
+ expect(cfg.agents.hermes).toBeDefined();
23
+ expect(cfg.agentOverrides).toBeNull();
24
+ });
25
+
26
+ test("loadWorkflowConfig ignores legacy provider/model fields", async () => {
27
+ tempDir = mkdtempSync(join(tmpdir(), "uwf-engine-cfg-"));
28
+ writeFileSync(
29
+ join(tempDir, "config.yaml"),
30
+ "providers:\n openai: { baseUrl: x, apiKey: y }\nmodels:\n default: { provider: openai, name: gpt-4o }\ndefaultModel: default\nagents:\n hermes: { command: uwf-hermes, args: [] }\ndefaultAgent: hermes\n",
31
+ "utf8",
32
+ );
33
+ const cfg = (await loadWorkflowConfig(tempDir)) as Record<string, unknown>;
34
+ expect(cfg.defaultAgent).toBe("hermes");
35
+ expect(cfg.providers).toBeUndefined();
36
+ expect(cfg.models).toBeUndefined();
37
+ expect(cfg.defaultModel).toBeUndefined();
38
+ });
39
+ });
@@ -0,0 +1,203 @@
1
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { CasRef, StepEntry, Usage } from "@united-workforce/protocol";
5
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
6
+ import { buildStepEntry, sumStepEntryUsage } from "../commands/step.js";
7
+ import { createUwfStore, type UwfStore } from "../store.js";
8
+
9
+ let tmpDir: string;
10
+ let originalEnv: string | undefined;
11
+
12
+ beforeEach(async () => {
13
+ tmpDir = await mkdtemp(join(tmpdir(), "cli-build-step-entry-"));
14
+ originalEnv = process.env.OCAS_HOME;
15
+ process.env.OCAS_HOME = join(tmpDir, "cas");
16
+ await mkdir(process.env.OCAS_HOME, { recursive: true });
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await rm(tmpDir, { recursive: true, force: true });
21
+ if (originalEnv === undefined) {
22
+ delete process.env.OCAS_HOME;
23
+ } else {
24
+ process.env.OCAS_HOME = originalEnv;
25
+ }
26
+ });
27
+
28
+ type PutStepOptions = {
29
+ startedAtMs: number;
30
+ completedAtMs: number;
31
+ usage: Usage | null;
32
+ previousAttempts: CasRef[] | null;
33
+ };
34
+
35
+ async function setupStore(): Promise<{ uwf: UwfStore; startHash: CasRef }> {
36
+ const uwf = await createUwfStore(tmpDir);
37
+ const workflowHash = (await uwf.store.cas.put(uwf.schemas.workflow, {
38
+ name: "test-wf",
39
+ description: "desc",
40
+ roles: {},
41
+ graph: {},
42
+ })) as CasRef;
43
+ const startHash = (await uwf.store.cas.put(uwf.schemas.startNode, {
44
+ workflow: workflowHash,
45
+ prompt: "task",
46
+ cwd: "/tmp",
47
+ })) as CasRef;
48
+ return { uwf, startHash };
49
+ }
50
+
51
+ async function putStep(uwf: UwfStore, startHash: CasRef, options: PutStepOptions): Promise<CasRef> {
52
+ const outputHash = (await uwf.store.cas.put(uwf.schemas.text, "output text")) as CasRef;
53
+ const detailHash = (await uwf.store.cas.put(uwf.schemas.text, "detail text")) as CasRef;
54
+ return (await uwf.store.cas.put(uwf.schemas.stepNode, {
55
+ start: startHash,
56
+ prev: null,
57
+ role: "planner",
58
+ output: outputHash,
59
+ detail: detailHash,
60
+ agent: "uwf-mock",
61
+ edgePrompt: "",
62
+ startedAtMs: options.startedAtMs,
63
+ completedAtMs: options.completedAtMs,
64
+ cwd: "/tmp",
65
+ assembledPrompt: null,
66
+ usage: options.usage,
67
+ previousAttempts: options.previousAttempts,
68
+ })) as CasRef;
69
+ }
70
+
71
+ const usage = (
72
+ turns: number,
73
+ inputTokens: number,
74
+ outputTokens: number,
75
+ duration: number,
76
+ ): Usage => ({
77
+ turns,
78
+ inputTokens,
79
+ outputTokens,
80
+ duration,
81
+ });
82
+
83
+ describe("buildStepEntry", () => {
84
+ test("returns null for a non-StepNode hash", async () => {
85
+ const { uwf } = await setupStore();
86
+ const textHash = (await uwf.store.cas.put(uwf.schemas.text, "not a step")) as CasRef;
87
+ expect(buildStepEntry(uwf, textHash)).toBeNull();
88
+ expect(buildStepEntry(uwf, "MISSING000000" as CasRef)).toBeNull();
89
+ });
90
+
91
+ test("builds an entry with no previousAttempts and computes durationMs", async () => {
92
+ const { uwf, startHash } = await setupStore();
93
+ const stepHash = await putStep(uwf, startHash, {
94
+ startedAtMs: 1_000,
95
+ completedAtMs: 4_500,
96
+ usage: usage(2, 100, 50, 3.5),
97
+ previousAttempts: null,
98
+ });
99
+
100
+ const entry = buildStepEntry(uwf, stepHash);
101
+ expect(entry).not.toBeNull();
102
+ expect(entry?.hash).toBe(stepHash);
103
+ expect(entry?.role).toBe("planner");
104
+ expect(entry?.durationMs).toBe(3_500);
105
+ expect(entry?.usage).toEqual(usage(2, 100, 50, 3.5));
106
+ expect(entry?.previousAttempts).toBeNull();
107
+ });
108
+
109
+ test("recursively builds nested previousAttempts", async () => {
110
+ const { uwf, startHash } = await setupStore();
111
+ const first = await putStep(uwf, startHash, {
112
+ startedAtMs: 0,
113
+ completedAtMs: 100,
114
+ usage: usage(1, 10, 5, 0.1),
115
+ previousAttempts: null,
116
+ });
117
+ const second = await putStep(uwf, startHash, {
118
+ startedAtMs: 100,
119
+ completedAtMs: 300,
120
+ usage: usage(1, 20, 10, 0.2),
121
+ previousAttempts: [first],
122
+ });
123
+ const success = await putStep(uwf, startHash, {
124
+ startedAtMs: 300,
125
+ completedAtMs: 600,
126
+ usage: usage(3, 30, 15, 0.3),
127
+ previousAttempts: [second],
128
+ });
129
+
130
+ const entry = buildStepEntry(uwf, success);
131
+ expect(entry?.previousAttempts).toHaveLength(1);
132
+ const nested = entry?.previousAttempts?.[0];
133
+ expect(nested?.hash).toBe(second);
134
+ expect(nested?.previousAttempts).toHaveLength(1);
135
+ expect(nested?.previousAttempts?.[0]?.hash).toBe(first);
136
+ expect(nested?.previousAttempts?.[0]?.previousAttempts).toBeNull();
137
+ });
138
+
139
+ test("skips previousAttempts refs that do not resolve to a StepNode", async () => {
140
+ const { uwf, startHash } = await setupStore();
141
+ const success = await putStep(uwf, startHash, {
142
+ startedAtMs: 0,
143
+ completedAtMs: 100,
144
+ usage: null,
145
+ previousAttempts: ["DEADBEEF00000" as CasRef],
146
+ });
147
+
148
+ const entry = buildStepEntry(uwf, success);
149
+ expect(entry).not.toBeNull();
150
+ // The unresolvable ref is skipped, leaving no valid nested attempts.
151
+ expect(entry?.previousAttempts).toBeNull();
152
+ });
153
+ });
154
+
155
+ describe("sumStepEntryUsage", () => {
156
+ function entryWith(u: Usage | null, previousAttempts: StepEntry[] | null): StepEntry {
157
+ return {
158
+ hash: "STEP000000000" as CasRef,
159
+ role: "planner",
160
+ output: {},
161
+ detail: "DETAIL0000000" as CasRef,
162
+ agent: "uwf-mock",
163
+ timestamp: 0,
164
+ durationMs: 0,
165
+ usage: u,
166
+ previousAttempts,
167
+ };
168
+ }
169
+
170
+ test("returns zeros when usage is null and there are no attempts", () => {
171
+ expect(sumStepEntryUsage(entryWith(null, null))).toEqual({
172
+ turns: 0,
173
+ inputTokens: 0,
174
+ outputTokens: 0,
175
+ duration: 0,
176
+ });
177
+ });
178
+
179
+ test("aggregates usage across nested previousAttempts", () => {
180
+ const inner = entryWith(usage(1, 10, 5, 0.1), null);
181
+ const middle = entryWith(usage(2, 20, 10, 0.2), [inner]);
182
+ const root = entryWith(usage(3, 30, 15, 0.3), [middle]);
183
+
184
+ expect(sumStepEntryUsage(root)).toEqual({
185
+ turns: 6,
186
+ inputTokens: 60,
187
+ outputTokens: 30,
188
+ duration: expect.closeTo(0.6, 5),
189
+ });
190
+ });
191
+
192
+ test("treats null usage in nested attempts as zero", () => {
193
+ const inner = entryWith(null, null);
194
+ const root = entryWith(usage(2, 20, 10, 0.5), [inner]);
195
+
196
+ expect(sumStepEntryUsage(root)).toEqual({
197
+ turns: 2,
198
+ inputTokens: 20,
199
+ outputTokens: 10,
200
+ duration: 0.5,
201
+ });
202
+ });
203
+ });