@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,677 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { bootstrap, putSchema } from "@ocas/core";
7
+ import { openStore } from "@ocas/fs";
8
+ import type { CasRef, ThreadId, ThreadIndexEntry } from "@united-workforce/protocol";
9
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
10
+ import { registerUwfSchemas } from "../schemas.js";
11
+ import { seedThreads } from "./thread-test-helpers.js";
12
+
13
+ const OUTPUT_SCHEMA = {
14
+ type: "object" as const,
15
+ properties: {
16
+ $status: { type: "string" as const },
17
+ note: { type: "string" as const },
18
+ },
19
+ required: ["$status"],
20
+ additionalProperties: false,
21
+ };
22
+
23
+ const DETAIL_SCHEMA = {
24
+ title: "ask-detail",
25
+ type: "object" as const,
26
+ required: ["sessionId", "model", "duration", "turnCount", "turns"],
27
+ properties: {
28
+ sessionId: { type: "string" as const },
29
+ model: { type: "string" as const },
30
+ duration: { type: "integer" as const },
31
+ turnCount: { type: "integer" as const },
32
+ turns: {
33
+ type: "array" as const,
34
+ items: { type: "string" as const, format: "ocas_ref" },
35
+ },
36
+ },
37
+ additionalProperties: false,
38
+ };
39
+
40
+ const THREAD_ID = "01ASKSTEPTEST000000000" as ThreadId;
41
+ const STEP_SESSION_ID = "ses-original-step-001";
42
+
43
+ let tmpDir: string;
44
+ let savedOcasHome: string | undefined;
45
+
46
+ beforeEach(async () => {
47
+ savedOcasHome = process.env.OCAS_HOME;
48
+ tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-ask-test-"));
49
+ });
50
+
51
+ afterEach(async () => {
52
+ if (savedOcasHome === undefined) {
53
+ delete process.env.OCAS_HOME;
54
+ } else {
55
+ process.env.OCAS_HOME = savedOcasHome;
56
+ }
57
+ await rm(tmpDir, { recursive: true, force: true });
58
+ });
59
+
60
+ type SetupOpts = {
61
+ threadStatus: ThreadIndexEntry["status"];
62
+ withDetail: boolean;
63
+ // The agent name (path or alias) to record in the head StepNode.agent field.
64
+ // Defaults to mockAgentPath.
65
+ stepAgentNameOverride: string | null;
66
+ // Pre-cached fork session-id. When provided, the cache file is written
67
+ // before running so the test can verify reuse semantics.
68
+ preCachedForkSessionId: string | null;
69
+ };
70
+
71
+ type SetupResult = {
72
+ casDir: string;
73
+ stepHash: CasRef;
74
+ startHash: CasRef;
75
+ workflowHash: CasRef;
76
+ detailHash: CasRef | null;
77
+ mockAgentPath: string;
78
+ failingAgentPath: string;
79
+ promptCapturePath: string;
80
+ modeCapturePath: string;
81
+ forkSessionCapturePath: string;
82
+ askSessionCapturePath: string;
83
+ envCapturePath: string;
84
+ };
85
+
86
+ async function setupAskFixture(opts: Partial<SetupOpts> = {}): Promise<SetupResult> {
87
+ const cfg: SetupOpts = {
88
+ threadStatus: opts.threadStatus ?? "idle",
89
+ withDetail: opts.withDetail ?? true,
90
+ stepAgentNameOverride: opts.stepAgentNameOverride ?? null,
91
+ preCachedForkSessionId: opts.preCachedForkSessionId ?? null,
92
+ };
93
+
94
+ const casDir = join(tmpDir, "cas");
95
+ await mkdir(casDir, { recursive: true });
96
+
97
+ const store = await openStore(casDir);
98
+ await bootstrap(store);
99
+ const schemas = await registerUwfSchemas(store);
100
+ const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
101
+ const detailSchemaHash = await putSchema(store, DETAIL_SCHEMA);
102
+
103
+ const workflowHash = await store.cas.put(schemas.workflow, {
104
+ name: "test-ask",
105
+ description: "ask command integration test",
106
+ roles: {
107
+ worker: {
108
+ description: "Worker",
109
+ goal: "Work",
110
+ capabilities: [],
111
+ procedure: "work",
112
+ output: "result",
113
+ frontmatter: outputSchemaHash,
114
+ },
115
+ },
116
+ graph: {
117
+ $START: {
118
+ new: { role: "worker", prompt: "Start work", location: null },
119
+ },
120
+ worker: { ok: { role: "$END", prompt: "done", location: null } },
121
+ },
122
+ });
123
+
124
+ const startHash = await store.cas.put(schemas.startNode, {
125
+ workflow: workflowHash,
126
+ prompt: "Test ask task",
127
+ cwd: tmpDir,
128
+ });
129
+
130
+ // Set OCAS_HOME so seedThreads + in-test createUwfStore calls resolve to this CAS dir.
131
+ process.env.OCAS_HOME = casDir;
132
+
133
+ // Capture file paths
134
+ const promptCapturePath = join(tmpDir, "captured-prompt.txt");
135
+ const modeCapturePath = join(tmpDir, "captured-mode.txt");
136
+ const forkSessionCapturePath = join(tmpDir, "captured-fork-session.txt");
137
+ const askSessionCapturePath = join(tmpDir, "captured-ask-session.txt");
138
+ const envCapturePath = join(tmpDir, "captured-env.txt");
139
+ const mockAgentPath = join(tmpDir, "mock-agent.sh");
140
+ const failingAgentPath = join(tmpDir, "failing-agent.sh");
141
+
142
+ // Build a detail node with sessionId so step ask can extract it
143
+ let detailHash: CasRef | null = null;
144
+ if (cfg.withDetail) {
145
+ const turnHash = await store.cas.put(detailSchemaHash, {
146
+ sessionId: STEP_SESSION_ID,
147
+ model: "test-model",
148
+ duration: 1000,
149
+ turnCount: 0,
150
+ turns: [],
151
+ });
152
+ detailHash = turnHash;
153
+ }
154
+
155
+ // Build the StepNode at thread head
156
+ const outputHash = await store.cas.put(outputSchemaHash, { $status: "ok" });
157
+ const stepHash = await store.cas.put(schemas.stepNode, {
158
+ start: startHash,
159
+ prev: null,
160
+ role: "worker",
161
+ output: outputHash,
162
+ detail: detailHash,
163
+ agent: cfg.stepAgentNameOverride ?? mockAgentPath,
164
+ edgePrompt: "Start work",
165
+ startedAtMs: 1716600000000,
166
+ completedAtMs: 1716600001000,
167
+ cwd: tmpDir,
168
+ assembledPrompt: null,
169
+ usage: null,
170
+ });
171
+
172
+ // Seed thread index entry
173
+ await seedThreads(tmpDir, {
174
+ [THREAD_ID]: {
175
+ head: stepHash,
176
+ status: cfg.threadStatus,
177
+ suspendedRole: null,
178
+ suspendMessage: null,
179
+ completedAt: cfg.threadStatus === "end" ? 1716600001000 : null,
180
+ },
181
+ });
182
+
183
+ // Pre-seed the ask session cache so reuse tests have something to find.
184
+ if (cfg.preCachedForkSessionId !== null) {
185
+ const cachePath = join(tmpDir, "cache", "mock-sessions.json");
186
+ await mkdir(dirname(cachePath), { recursive: true });
187
+ await writeFile(
188
+ cachePath,
189
+ `${JSON.stringify({ [`${stepHash}:ask`]: cfg.preCachedForkSessionId }, null, 2)}\n`,
190
+ "utf8",
191
+ );
192
+ }
193
+
194
+ // Mock agent: dispatches based on `--mode` (ask|fork|run) and captures inputs.
195
+ // - --mode ask --session <id> --prompt <text>: writes to ask capture; echoes a fixed answer to stdout
196
+ // - --mode fork --session <id>: writes to fork capture; prints "forked-from-<id>" sessionId on stdout
197
+ // - default (uwf-* style invocation): captures and echoes adapter JSON (not used in this suite)
198
+ await writeFile(
199
+ mockAgentPath,
200
+ `#!/bin/sh
201
+ mode=""
202
+ prompt=""
203
+ session=""
204
+ detail=""
205
+ while [ $# -gt 0 ]; do
206
+ case "$1" in
207
+ --mode) mode="$2"; shift 2 ;;
208
+ --prompt) prompt="$2"; shift 2 ;;
209
+ --session) session="$2"; shift 2 ;;
210
+ --detail) detail="$2"; shift 2 ;;
211
+ *) shift ;;
212
+ esac
213
+ done
214
+ printf '%s' "$mode" > '${modeCapturePath}'
215
+ printf '%s' "$prompt" > '${promptCapturePath}'
216
+ printf 'OCAS_HOME=%s\\n' "$OCAS_HOME" > '${envCapturePath}'
217
+ case "$mode" in
218
+ fork)
219
+ printf '%s' "$session" > '${forkSessionCapturePath}'
220
+ new_id="forked-from-$session"
221
+ printf '%s\\n' "$new_id"
222
+ ;;
223
+ ask)
224
+ printf '%s' "$session" > '${askSessionCapturePath}'
225
+ # Print a deterministic answer that the cmdStepAsk path will hand back.
226
+ printf 'MOCK_ANSWER prompt=%s session=%s detail=%s\\n' "$prompt" "$session" "$detail"
227
+ ;;
228
+ *)
229
+ echo "{\\"stepHash\\":\\"unused\\"}"
230
+ ;;
231
+ esac
232
+ `,
233
+ { mode: 0o755 },
234
+ );
235
+
236
+ await writeFile(
237
+ failingAgentPath,
238
+ `#!/bin/sh
239
+ echo "boom" >&2
240
+ exit 7
241
+ `,
242
+ { mode: 0o755 },
243
+ );
244
+
245
+ // Minimal config so loadWorkflowConfig succeeds.
246
+ const configPath = join(tmpDir, "config.yaml");
247
+ await writeFile(
248
+ configPath,
249
+ `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
250
+ );
251
+
252
+ return {
253
+ casDir,
254
+ stepHash,
255
+ startHash,
256
+ workflowHash,
257
+ detailHash,
258
+ mockAgentPath,
259
+ failingAgentPath,
260
+ promptCapturePath,
261
+ modeCapturePath,
262
+ forkSessionCapturePath,
263
+ askSessionCapturePath,
264
+ envCapturePath,
265
+ };
266
+ }
267
+
268
+ function runUwf(
269
+ args: string[],
270
+ casDir: string,
271
+ ): { stdout: string; stderr: string; status: number } {
272
+ const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
273
+ try {
274
+ const stdout = execFileSync(process.execPath, [cliPath, ...args], {
275
+ encoding: "utf8",
276
+ stdio: ["ignore", "pipe", "pipe"],
277
+ env: {
278
+ ...process.env,
279
+ UWF_HOME: tmpDir,
280
+ OCAS_HOME: casDir,
281
+ },
282
+ cwd: tmpDir,
283
+ timeout: 30000,
284
+ });
285
+ return { stdout, stderr: "", status: 0 };
286
+ } catch (error) {
287
+ const err = error as NodeJS.ErrnoException & {
288
+ stdout?: string | Buffer;
289
+ stderr?: string | Buffer;
290
+ status?: number;
291
+ };
292
+ return {
293
+ stdout: typeof err.stdout === "string" ? err.stdout : (err.stdout?.toString("utf8") ?? ""),
294
+ stderr: typeof err.stderr === "string" ? err.stderr : (err.stderr?.toString("utf8") ?? ""),
295
+ status: err.status ?? 1,
296
+ };
297
+ }
298
+ }
299
+
300
+ // ── Group 1: CLI argument validation ───────────────────────────────────────
301
+
302
+ describe("uwf step ask - CLI argument validation", () => {
303
+ test("1.1 missing step-hash exits non-zero", async () => {
304
+ const { casDir } = await setupAskFixture();
305
+ const result = runUwf(["step", "ask"], casDir);
306
+ expect(result.status).not.toBe(0);
307
+ });
308
+
309
+ test("1.2 missing -p flag exits non-zero", async () => {
310
+ const { casDir, stepHash } = await setupAskFixture();
311
+ const result = runUwf(["step", "ask", stepHash], casDir);
312
+ expect(result.status).not.toBe(0);
313
+ expect(result.stderr.toLowerCase()).toMatch(/required|missing|prompt/);
314
+ });
315
+
316
+ test("1.3 step-hash and -p accepted as valid invocation", async () => {
317
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
318
+ const result = runUwf(
319
+ ["step", "ask", stepHash, "-p", "why?", "--agent", mockAgentPath],
320
+ casDir,
321
+ );
322
+ expect(result.status).toBe(0);
323
+ });
324
+ });
325
+
326
+ // ── Group 2: CAS validation errors ────────────────────────────────────────
327
+
328
+ describe("uwf step ask - CAS validation errors", () => {
329
+ test("2.1 non-existent CAS hash exits non-zero with 'not found'", async () => {
330
+ const { casDir, mockAgentPath } = await setupAskFixture();
331
+ const result = runUwf(
332
+ ["step", "ask", "0000000000000", "-p", "why?", "--agent", mockAgentPath],
333
+ casDir,
334
+ );
335
+ expect(result.status).not.toBe(0);
336
+ expect(result.stderr.toLowerCase()).toContain("not found");
337
+ });
338
+
339
+ test("2.2 hash that is not a StepNode exits non-zero", async () => {
340
+ const { casDir, startHash, mockAgentPath } = await setupAskFixture();
341
+ // Use the StartNode hash — it exists but is not a StepNode
342
+ const result = runUwf(
343
+ ["step", "ask", startHash, "-p", "why?", "--agent", mockAgentPath],
344
+ casDir,
345
+ );
346
+ expect(result.status).not.toBe(0);
347
+ expect(result.stderr.toLowerCase()).toContain("not a stepnode");
348
+ });
349
+
350
+ test("2.3 step with no detail ref exits non-zero", async () => {
351
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture({ withDetail: false });
352
+ const result = runUwf(
353
+ ["step", "ask", stepHash, "-p", "why?", "--agent", mockAgentPath],
354
+ casDir,
355
+ );
356
+ expect(result.status).not.toBe(0);
357
+ expect(result.stderr.toLowerCase()).toMatch(/no detail|detail.*missing|missing.*detail/);
358
+ });
359
+ });
360
+
361
+ // ── Group 3: Successful ask (core behavior) ───────────────────────────────
362
+
363
+ describe("uwf step ask - successful ask (core)", () => {
364
+ test("3.1 stdout contains agent's response text", async () => {
365
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
366
+ const result = runUwf(
367
+ ["step", "ask", stepHash, "-p", "why tar not zip?", "--agent", mockAgentPath],
368
+ casDir,
369
+ );
370
+ expect(result.status).toBe(0);
371
+ expect(result.stdout).toContain("MOCK_ANSWER");
372
+ expect(result.stdout).toContain("why tar not zip?");
373
+ });
374
+
375
+ test("3.2 thread index entry (head, status) is identical before and after ask", async () => {
376
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
377
+
378
+ // Before ask: snapshot the thread state
379
+ const { createUwfStore, getThread } = await import("../store.js");
380
+ const before = await createUwfStore(tmpDir);
381
+ const beforeEntry = getThread(before.varStore, THREAD_ID);
382
+ expect(beforeEntry).not.toBeNull();
383
+
384
+ const result = runUwf(
385
+ ["step", "ask", stepHash, "-p", "anything", "--agent", mockAgentPath],
386
+ casDir,
387
+ );
388
+ expect(result.status).toBe(0);
389
+
390
+ // After ask: thread state should be unchanged
391
+ const after = await createUwfStore(tmpDir);
392
+ const afterEntry = getThread(after.varStore, THREAD_ID);
393
+ expect(afterEntry).not.toBeNull();
394
+ expect(afterEntry?.head).toBe(beforeEntry?.head);
395
+ expect(afterEntry?.status).toBe(beforeEntry?.status);
396
+ });
397
+
398
+ test("3.3 no new StepNode is written to CAS (step count unchanged)", async () => {
399
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
400
+
401
+ // Count StepNodes before
402
+ const { createUwfStore } = await import("../store.js");
403
+ const before = await createUwfStore(tmpDir);
404
+ const stepSchemaHash = before.schemas.stepNode;
405
+
406
+ function countStepNodes(uwfStore: typeof before): number {
407
+ const candidates = [stepHash];
408
+ let count = 0;
409
+ for (const h of candidates) {
410
+ const node = uwfStore.store.cas.get(h);
411
+ if (node !== null && node.type === stepSchemaHash) count++;
412
+ }
413
+ return count;
414
+ }
415
+
416
+ const beforeCount = countStepNodes(before);
417
+ expect(beforeCount).toBe(1);
418
+
419
+ const result = runUwf(
420
+ ["step", "ask", stepHash, "-p", "anything", "--agent", mockAgentPath],
421
+ casDir,
422
+ );
423
+ expect(result.status).toBe(0);
424
+
425
+ // After ask: still only the seeded StepNode exists at head; no new step appended.
426
+ const after = await createUwfStore(tmpDir);
427
+ const headNode = after.store.cas.get(stepHash);
428
+ expect(headNode).not.toBeNull();
429
+ expect(headNode?.type).toBe(after.schemas.stepNode);
430
+
431
+ // Confirm thread head still points to the original step hash
432
+ const { getThread } = await import("../store.js");
433
+ const entry = getThread(after.varStore, THREAD_ID);
434
+ expect(entry?.head).toBe(stepHash);
435
+ });
436
+ });
437
+
438
+ // ── Group 4: Fork cache semantics ─────────────────────────────────────────
439
+
440
+ describe("uwf step ask - fork cache", { timeout: 15_000 }, () => {
441
+ test("4.1 first ask creates a fork session and caches it", async () => {
442
+ const { casDir, stepHash, mockAgentPath, forkSessionCapturePath } = await setupAskFixture();
443
+
444
+ const result = runUwf(
445
+ ["step", "ask", stepHash, "-p", "first ask", "--agent", mockAgentPath],
446
+ casDir,
447
+ );
448
+ expect(result.status).toBe(0);
449
+
450
+ // The mock agent in fork mode receives the source session id
451
+ const forkArg = await readFile(forkSessionCapturePath, "utf8");
452
+ expect(forkArg).toBe(STEP_SESSION_ID);
453
+
454
+ // Cache file should now contain the ask key
455
+ const cachePath = join(tmpDir, "cache", "mock-sessions.json");
456
+ const raw = await readFile(cachePath, "utf8");
457
+ const parsed = JSON.parse(raw) as Record<string, string>;
458
+ expect(parsed[`${stepHash}:ask`]).toBeDefined();
459
+ expect(parsed[`${stepHash}:ask`]).toBe(`forked-from-${STEP_SESSION_ID}`);
460
+ });
461
+
462
+ test("4.2 second ask on same step reuses the cached fork session", async () => {
463
+ const cachedFork = "ses-already-forked-once";
464
+ const { casDir, stepHash, mockAgentPath, modeCapturePath, askSessionCapturePath } =
465
+ await setupAskFixture({ preCachedForkSessionId: cachedFork });
466
+
467
+ const result = runUwf(
468
+ ["step", "ask", stepHash, "-p", "second ask", "--agent", mockAgentPath],
469
+ casDir,
470
+ );
471
+ expect(result.status).toBe(0);
472
+
473
+ // The mock agent must have been invoked in `ask` mode (no fork performed).
474
+ const mode = await readFile(modeCapturePath, "utf8");
475
+ expect(mode).toBe("ask");
476
+
477
+ // The ask invocation should have received the cached fork session id.
478
+ const askArg = await readFile(askSessionCapturePath, "utf8");
479
+ expect(askArg).toBe(cachedFork);
480
+ });
481
+
482
+ test("4.3 different step hash creates an independent fork", async () => {
483
+ // Run a first ask on the base step → caches forkA
484
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
485
+
486
+ const r1 = runUwf(
487
+ ["step", "ask", stepHash, "-p", "ask on step A", "--agent", mockAgentPath],
488
+ casDir,
489
+ );
490
+ expect(r1.status).toBe(0);
491
+
492
+ // Build a second StepNode (different hash) with a different sessionId so
493
+ // its detail-derived ask session is independent of the first.
494
+ const { createUwfStore } = await import("../store.js");
495
+ const uwf = await createUwfStore(tmpDir);
496
+ const detailSchemaHash = await putSchema(uwf.store, DETAIL_SCHEMA);
497
+ const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
498
+ const otherDetailHash = await uwf.store.cas.put(detailSchemaHash, {
499
+ sessionId: "ses-original-step-002",
500
+ model: "test-model",
501
+ duration: 1000,
502
+ turnCount: 0,
503
+ turns: [],
504
+ });
505
+ const otherOutputHash = await uwf.store.cas.put(outputSchemaHash, {
506
+ $status: "ok",
507
+ note: "alt",
508
+ });
509
+
510
+ // Reuse the same start ref the first step points to so the new step is a valid sibling.
511
+ const head = uwf.store.cas.get(stepHash);
512
+ const startRefFromHead = (head?.payload as { start: CasRef }).start;
513
+ const properOtherStep = await uwf.store.cas.put(uwf.schemas.stepNode, {
514
+ start: startRefFromHead,
515
+ prev: null,
516
+ role: "worker",
517
+ output: otherOutputHash,
518
+ detail: otherDetailHash,
519
+ agent: mockAgentPath,
520
+ edgePrompt: "Start work",
521
+ startedAtMs: 1716600002000,
522
+ completedAtMs: 1716600003000,
523
+ cwd: tmpDir,
524
+ assembledPrompt: null,
525
+ usage: null,
526
+ });
527
+
528
+ // sanity check we constructed a separate hash
529
+ expect(properOtherStep).not.toBe(stepHash);
530
+
531
+ const r2 = runUwf(
532
+ ["step", "ask", properOtherStep, "-p", "ask on step B", "--agent", mockAgentPath],
533
+ casDir,
534
+ );
535
+ expect(r2.status).toBe(0);
536
+
537
+ const cachePath = join(tmpDir, "cache", "mock-sessions.json");
538
+ const raw = await readFile(cachePath, "utf8");
539
+ const parsed = JSON.parse(raw) as Record<string, string>;
540
+ expect(parsed[`${stepHash}:ask`]).toBeDefined();
541
+ expect(parsed[`${properOtherStep}:ask`]).toBeDefined();
542
+ expect(parsed[`${stepHash}:ask`]).not.toBe(parsed[`${properOtherStep}:ask`]);
543
+ });
544
+ });
545
+
546
+ // ── Group 5: Fallback (agent has no fork support) ─────────────────────────
547
+
548
+ describe("uwf step ask - fallback path", () => {
549
+ test("5.1 fallback agent (no fork support) still answers via stdout", async () => {
550
+ // Use a fallback agent that ONLY supports `ask` mode without ever being asked
551
+ // to fork. The CLI should detect missing fork support and inject context instead.
552
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
553
+
554
+ // Create a fallback agent script that fails with non-zero exit on "fork" mode.
555
+ // Fallback path must NOT call mode=fork; it should call mode=ask directly.
556
+ const fallbackPath = join(tmpDir, "fallback-agent.sh");
557
+ const promptCapture = join(tmpDir, "fallback-prompt.txt");
558
+ const sessionCapture = join(tmpDir, "fallback-session.txt");
559
+ const modeCapture = join(tmpDir, "fallback-mode.txt");
560
+ await writeFile(
561
+ fallbackPath,
562
+ `#!/bin/sh
563
+ mode=""
564
+ prompt=""
565
+ session=""
566
+ detail=""
567
+ while [ $# -gt 0 ]; do
568
+ case "$1" in
569
+ --mode) mode="$2"; shift 2 ;;
570
+ --prompt) prompt="$2"; shift 2 ;;
571
+ --session) session="$2"; shift 2 ;;
572
+ --detail) detail="$2"; shift 2 ;;
573
+ *) shift ;;
574
+ esac
575
+ done
576
+ printf '%s' "$mode" > '${modeCapture}'
577
+ printf '%s' "$prompt" > '${promptCapture}'
578
+ printf '%s' "$session" > '${sessionCapture}'
579
+ case "$mode" in
580
+ fork) echo "fork not supported" >&2; exit 99 ;;
581
+ ask) printf 'FALLBACK_ANSWER for: %s (detail=%s)\\n' "$prompt" "$detail" ;;
582
+ *) echo "unknown" >&2; exit 1 ;;
583
+ esac
584
+ `,
585
+ { mode: 0o755 },
586
+ );
587
+
588
+ const result = runUwf(
589
+ ["step", "ask", stepHash, "-p", "explain context", "--agent", fallbackPath, "--no-fork"],
590
+ casDir,
591
+ );
592
+ expect(result.status).toBe(0);
593
+ expect(result.stdout).toContain("FALLBACK_ANSWER");
594
+ expect(result.stdout).toContain("explain context");
595
+
596
+ // The fallback agent should be invoked in `ask` mode, with NO session id
597
+ // (since no fork happened). The detail ref must be passed for context injection.
598
+ const mode = await readFile(modeCapture, "utf8");
599
+ expect(mode).toBe("ask");
600
+ const session = await readFile(sessionCapture, "utf8");
601
+ expect(session).toBe("");
602
+
603
+ // Make sure mockAgentPath's mock never ran.
604
+ void mockAgentPath;
605
+ });
606
+
607
+ test("5.2 fallback ask still does NOT mutate thread state", async () => {
608
+ const { casDir, stepHash } = await setupAskFixture();
609
+
610
+ const fallbackPath = join(tmpDir, "fallback-agent.sh");
611
+ await writeFile(
612
+ fallbackPath,
613
+ `#!/bin/sh
614
+ mode=""
615
+ prompt=""
616
+ while [ $# -gt 0 ]; do
617
+ case "$1" in
618
+ --mode) mode="$2"; shift 2 ;;
619
+ --prompt) prompt="$2"; shift 2 ;;
620
+ *) shift ;;
621
+ esac
622
+ done
623
+ case "$mode" in
624
+ fork) echo "fork not supported" >&2; exit 99 ;;
625
+ ask) printf 'OK %s\\n' "$prompt" ;;
626
+ *) exit 1 ;;
627
+ esac
628
+ `,
629
+ { mode: 0o755 },
630
+ );
631
+
632
+ const { createUwfStore, getThread } = await import("../store.js");
633
+ const before = await createUwfStore(tmpDir);
634
+ const beforeEntry = getThread(before.varStore, THREAD_ID);
635
+
636
+ const result = runUwf(
637
+ ["step", "ask", stepHash, "-p", "any", "--agent", fallbackPath, "--no-fork"],
638
+ casDir,
639
+ );
640
+ expect(result.status).toBe(0);
641
+
642
+ const after = await createUwfStore(tmpDir);
643
+ const afterEntry = getThread(after.varStore, THREAD_ID);
644
+ expect(afterEntry?.head).toBe(beforeEntry?.head);
645
+ expect(afterEntry?.status).toBe(beforeEntry?.status);
646
+ });
647
+ });
648
+
649
+ // ── Group 6: Agent resolution ─────────────────────────────────────────────
650
+
651
+ describe("uwf step ask - agent resolution", () => {
652
+ test("6.1 without --agent flag, agent is resolved from step's agent field", async () => {
653
+ // Step's agent field points at mockAgentPath by default.
654
+ const { casDir, stepHash, modeCapturePath, promptCapturePath } = await setupAskFixture();
655
+ const result = runUwf(["step", "ask", stepHash, "-p", "explain"], casDir);
656
+ expect(result.status).toBe(0);
657
+
658
+ // The mockAgentPath must have been invoked in ask mode with the user prompt.
659
+ const mode = await readFile(modeCapturePath, "utf8");
660
+ expect(mode).toBe("ask");
661
+ const captured = await readFile(promptCapturePath, "utf8");
662
+ expect(captured).toBe("explain");
663
+ });
664
+
665
+ test("6.2 --agent override beats step's recorded agent", async () => {
666
+ // Record a non-existent agent in step.agent. Provide a working one via --agent.
667
+ const { casDir, stepHash, mockAgentPath } = await setupAskFixture({
668
+ stepAgentNameOverride: "uwf-does-not-exist",
669
+ });
670
+ const result = runUwf(
671
+ ["step", "ask", stepHash, "-p", "explain", "--agent", mockAgentPath],
672
+ casDir,
673
+ );
674
+ expect(result.status).toBe(0);
675
+ expect(result.stdout).toContain("MOCK_ANSWER");
676
+ });
677
+ });
@@ -119,6 +119,7 @@ async function createTestStep(
119
119
  assembledPrompt: null,
120
120
  cwd: "/tmp",
121
121
  usage: null,
122
+ previousAttempts: null,
122
123
  };
123
124
  return store.cas.put(schemas.stepNode, stepPayload);
124
125
  }
@@ -97,6 +97,7 @@ describe("protocol types", () => {
97
97
  assembledPrompt: null,
98
98
  cwd: "/test/path",
99
99
  usage: null,
100
+ previousAttempts: null,
100
101
  };
101
102
  expect(record.startedAtMs).toBe(1000);
102
103
  expect(record.completedAtMs).toBe(2000);
@@ -112,6 +113,7 @@ describe("protocol types", () => {
112
113
  timestamp: 123,
113
114
  durationMs: 5000,
114
115
  usage: null,
116
+ previousAttempts: null,
115
117
  };
116
118
  expect(entry.durationMs).toBe(5000);
117
119
  });