@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,1025 @@
1
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { bootstrap, putSchema, type Store } from "@ocas/core";
5
+ import type { CasRef, ThreadId } from "@united-workforce/protocol";
6
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
7
+ import { cmdStepList, cmdStepShow } from "../commands/step.js";
8
+ import {
9
+ cmdThreadRead,
10
+ extractLastAssistantContent,
11
+ THREAD_READ_DEFAULT_QUOTA,
12
+ } from "../commands/thread.js";
13
+ import type { UwfStore } from "../store.js";
14
+ import { completeThread, createUwfStore, setThread } from "../store.js";
15
+ import { seedThreads } from "./thread-test-helpers.js";
16
+
17
+ // ── schemas used in tests ────────────────────────────────────────────────────
18
+
19
+ const TURN_SCHEMA = {
20
+ title: "hermes-turn",
21
+ type: "object" as const,
22
+ required: ["index", "role", "content"],
23
+ properties: {
24
+ index: { type: "integer" as const },
25
+ role: { type: "string" as const },
26
+ content: { type: "string" as const },
27
+ toolCalls: {
28
+ anyOf: [
29
+ { type: "array" as const, items: { type: "object" as const } },
30
+ { type: "null" as const },
31
+ ],
32
+ },
33
+ reasoning: { anyOf: [{ type: "string" as const }, { type: "null" as const }] },
34
+ },
35
+ additionalProperties: false,
36
+ };
37
+
38
+ const DETAIL_SCHEMA = {
39
+ title: "hermes-detail",
40
+ type: "object" as const,
41
+ required: ["sessionId", "model", "duration", "turnCount", "turns"],
42
+ properties: {
43
+ sessionId: { type: "string" as const },
44
+ model: { type: "string" as const },
45
+ duration: { type: "integer" as const },
46
+ turnCount: { type: "integer" as const },
47
+ turns: {
48
+ type: "array" as const,
49
+ items: { type: "string" as const, format: "ocas_ref" },
50
+ },
51
+ },
52
+ additionalProperties: false,
53
+ };
54
+
55
+ // ── helpers ───────────────────────────────────────────────────────────────────
56
+
57
+ async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
58
+ const casDir = join(storageRoot, "cas");
59
+ await mkdir(casDir, { recursive: true });
60
+ process.env.OCAS_HOME = casDir;
61
+ return createUwfStore(storageRoot);
62
+ }
63
+
64
+ async function registerDetailSchemas(store: Store) {
65
+ await bootstrap(store);
66
+ const [turn, detail] = await Promise.all([
67
+ putSchema(store, TURN_SCHEMA),
68
+ putSchema(store, DETAIL_SCHEMA),
69
+ ]);
70
+ return { turn, detail };
71
+ }
72
+
73
+ // ── fixture ───────────────────────────────────────────────────────────────────
74
+
75
+ let tmpDir: string;
76
+
77
+ beforeEach(async () => {
78
+ tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-test-"));
79
+ });
80
+
81
+ afterEach(async () => {
82
+ await rm(tmpDir, { recursive: true, force: true });
83
+ });
84
+
85
+ // ── extractLastAssistantContent ───────────────────────────────────────────────
86
+
87
+ describe("extractLastAssistantContent", () => {
88
+ test("returns last non-empty assistant content from turns", async () => {
89
+ const uwf = await makeUwfStore(tmpDir);
90
+ const schemas = await registerDetailSchemas(uwf.store);
91
+
92
+ const turn1 = await uwf.store.cas.put(schemas.turn, {
93
+ index: 0,
94
+ role: "assistant",
95
+ content: "intermediate",
96
+ toolCalls: null,
97
+ reasoning: null,
98
+ });
99
+ const turn2 = await uwf.store.cas.put(schemas.turn, {
100
+ index: 1,
101
+ role: "tool",
102
+ content: "ok",
103
+ toolCalls: null,
104
+ reasoning: null,
105
+ });
106
+ const turn3 = await uwf.store.cas.put(schemas.turn, {
107
+ index: 2,
108
+ role: "assistant",
109
+ content: "final answer",
110
+ toolCalls: null,
111
+ reasoning: null,
112
+ });
113
+
114
+ const detailHash = await uwf.store.cas.put(schemas.detail, {
115
+ sessionId: "s1",
116
+ model: "m1",
117
+ duration: 1000,
118
+ turnCount: 3,
119
+ turns: [turn1, turn2, turn3],
120
+ });
121
+
122
+ expect(extractLastAssistantContent(uwf, detailHash)).toBe("final answer");
123
+ });
124
+
125
+ test("returns null when detail node does not exist in store", async () => {
126
+ const uwf = await makeUwfStore(tmpDir);
127
+ expect(extractLastAssistantContent(uwf, "nonexistent00" as CasRef)).toBeNull();
128
+ });
129
+
130
+ test("returns null when turns array is empty", async () => {
131
+ const uwf = await makeUwfStore(tmpDir);
132
+ const schemas = await registerDetailSchemas(uwf.store);
133
+
134
+ const detailHash = await uwf.store.cas.put(schemas.detail, {
135
+ sessionId: "s2",
136
+ model: "m2",
137
+ duration: 0,
138
+ turnCount: 0,
139
+ turns: [],
140
+ });
141
+
142
+ expect(extractLastAssistantContent(uwf, detailHash)).toBeNull();
143
+ });
144
+
145
+ test("returns null when all assistant turns have empty content", async () => {
146
+ const uwf = await makeUwfStore(tmpDir);
147
+ const schemas = await registerDetailSchemas(uwf.store);
148
+
149
+ const turn1 = await uwf.store.cas.put(schemas.turn, {
150
+ index: 0,
151
+ role: "assistant",
152
+ content: "",
153
+ toolCalls: null,
154
+ reasoning: null,
155
+ });
156
+
157
+ const detailHash = await uwf.store.cas.put(schemas.detail, {
158
+ sessionId: "s3",
159
+ model: "m3",
160
+ duration: 0,
161
+ turnCount: 1,
162
+ turns: [turn1],
163
+ });
164
+
165
+ expect(extractLastAssistantContent(uwf, detailHash)).toBeNull();
166
+ });
167
+
168
+ test("skips whitespace-only assistant content and returns earlier match", async () => {
169
+ const uwf = await makeUwfStore(tmpDir);
170
+ const schemas = await registerDetailSchemas(uwf.store);
171
+
172
+ const turn1 = await uwf.store.cas.put(schemas.turn, {
173
+ index: 0,
174
+ role: "assistant",
175
+ content: "real content",
176
+ toolCalls: null,
177
+ reasoning: null,
178
+ });
179
+ const turn2 = await uwf.store.cas.put(schemas.turn, {
180
+ index: 1,
181
+ role: "assistant",
182
+ content: " ",
183
+ toolCalls: null,
184
+ reasoning: null,
185
+ });
186
+
187
+ const detailHash = await uwf.store.cas.put(schemas.detail, {
188
+ sessionId: "s4",
189
+ model: "m4",
190
+ duration: 0,
191
+ turnCount: 2,
192
+ turns: [turn1, turn2],
193
+ });
194
+
195
+ expect(extractLastAssistantContent(uwf, detailHash)).toBe("real content");
196
+ });
197
+ });
198
+
199
+ // ── cmdThreadRead: <output> section ──────────────────────────────────────────
200
+
201
+ describe("cmdThreadRead <output> section", () => {
202
+ test("includes <output> tags when detail has assistant turns", async () => {
203
+ const uwf = await makeUwfStore(tmpDir);
204
+ const detailSchemas = await registerDetailSchemas(uwf.store);
205
+
206
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
207
+ name: "test-wf",
208
+ description: "desc",
209
+ roles: {
210
+ writer: {
211
+ description: "Write",
212
+ goal: "You are a writer.",
213
+ capabilities: [],
214
+ procedure: "Write content as requested.",
215
+ output: "Summarize what was written.",
216
+ meta: "placeholder00" as CasRef,
217
+ },
218
+ },
219
+ conditions: {},
220
+ graph: {},
221
+ });
222
+
223
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
224
+ workflow: workflowHash,
225
+ prompt: "Write something",
226
+ });
227
+
228
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
229
+ name: "out",
230
+ description: "",
231
+ roles: {},
232
+ conditions: {},
233
+ graph: {},
234
+ });
235
+
236
+ const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
237
+ index: 0,
238
+ role: "assistant",
239
+ content: "The assistant response text",
240
+ toolCalls: null,
241
+ reasoning: null,
242
+ });
243
+ const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
244
+ sessionId: "sx",
245
+ model: "mx",
246
+ duration: 500,
247
+ turnCount: 1,
248
+ turns: [turnHash],
249
+ });
250
+
251
+ const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
252
+ start: startHash,
253
+ prev: null,
254
+ role: "writer",
255
+ output: outputHash,
256
+ detail: detailHash,
257
+ agent: "uwf-hermes",
258
+ });
259
+
260
+ const threadId = "01JTEST0000000000000000001" as ThreadId;
261
+ await seedThreads(tmpDir, { [threadId]: stepHash });
262
+
263
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
264
+
265
+ expect(markdown).toContain("<output>");
266
+ expect(markdown).toContain("</output>");
267
+ expect(markdown).toContain("The assistant response text");
268
+ expect(markdown).not.toContain("### Content");
269
+ });
270
+
271
+ test("omits <output> tags when detail has no matching assistant turns", async () => {
272
+ const uwf = await makeUwfStore(tmpDir);
273
+
274
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
275
+ name: "test-wf2",
276
+ description: "desc",
277
+ roles: {},
278
+ conditions: {},
279
+ graph: {},
280
+ });
281
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
282
+ workflow: workflowHash,
283
+ prompt: "Do stuff",
284
+ });
285
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
286
+ name: "out",
287
+ description: "",
288
+ roles: {},
289
+ conditions: {},
290
+ graph: {},
291
+ });
292
+
293
+ // A detail ref that doesn't exist in the store → extractLastAssistantContent returns null
294
+ const missingDetailRef = "missingdetail0" as CasRef;
295
+
296
+ const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
297
+ start: startHash,
298
+ prev: null,
299
+ role: "worker",
300
+ output: outputHash,
301
+ detail: missingDetailRef,
302
+ agent: "uwf-hermes",
303
+ });
304
+
305
+ const threadId = "01JTEST0000000000000000002" as ThreadId;
306
+ await seedThreads(tmpDir, { [threadId]: stepHash });
307
+
308
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
309
+
310
+ expect(markdown).not.toContain("<output>");
311
+ expect(markdown).not.toContain("</output>");
312
+ expect(markdown).not.toContain("### Content");
313
+ });
314
+ });
315
+
316
+ // ── cmdStepShow ───────────────────────────────────────────────────────────────
317
+
318
+ describe("cmdStepShow", () => {
319
+ test("returns expanded detail node with turns inlined", async () => {
320
+ const uwf = await makeUwfStore(tmpDir);
321
+ const detailSchemas = await registerDetailSchemas(uwf.store);
322
+
323
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
324
+ name: "wf",
325
+ description: "",
326
+ roles: {},
327
+ conditions: {},
328
+ graph: {},
329
+ });
330
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
331
+ workflow: workflowHash,
332
+ prompt: "p",
333
+ });
334
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
335
+ name: "out",
336
+ description: "",
337
+ roles: {},
338
+ conditions: {},
339
+ graph: {},
340
+ });
341
+
342
+ const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
343
+ index: 0,
344
+ role: "assistant",
345
+ content: "done",
346
+ toolCalls: null,
347
+ reasoning: null,
348
+ });
349
+ const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
350
+ sessionId: "sess42",
351
+ model: "gpt-4o",
352
+ duration: 3000,
353
+ turnCount: 1,
354
+ turns: [turnHash],
355
+ });
356
+
357
+ const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
358
+ start: startHash,
359
+ prev: null,
360
+ role: "coder",
361
+ output: outputHash,
362
+ detail: detailHash,
363
+ agent: "uwf-hermes",
364
+ });
365
+
366
+ const result = await cmdStepShow(tmpDir, stepHash);
367
+
368
+ expect(result).toMatchObject({
369
+ sessionId: "sess42",
370
+ model: "gpt-4o",
371
+ duration: 3000,
372
+ turnCount: 1,
373
+ });
374
+
375
+ const expanded = result as Record<string, unknown>;
376
+ expect(Array.isArray(expanded.turns)).toBe(true);
377
+ const turns = expanded.turns as unknown[];
378
+ expect(turns).toHaveLength(1);
379
+ expect(turns[0]).toMatchObject({
380
+ index: 0,
381
+ role: "assistant",
382
+ content: "done",
383
+ });
384
+ });
385
+ });
386
+
387
+ // ── cmdThreadRead: <prompt> deduplication ────────────────────────────────────
388
+
389
+ describe("cmdThreadRead <prompt> deduplication", () => {
390
+ async function makeThreadWithRoles(uwf: UwfStore, roles: string[]): Promise<string> {
391
+ const roleMap: Record<string, unknown> = {};
392
+ for (const r of [...new Set(roles)]) {
393
+ roleMap[r] = {
394
+ description: r,
395
+ goal: `Goal for ${r}`,
396
+ capabilities: [],
397
+ procedure: "Do stuff.",
398
+ output: "Output.",
399
+ meta: "placeholder00" as CasRef,
400
+ };
401
+ }
402
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
403
+ name: "dedup-wf",
404
+ description: "desc",
405
+ roles: roleMap,
406
+ conditions: {},
407
+ graph: {},
408
+ });
409
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
410
+ workflow: workflowHash,
411
+ prompt: "Start",
412
+ });
413
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
414
+ name: "out",
415
+ description: "",
416
+ roles: {},
417
+ conditions: {},
418
+ graph: {},
419
+ });
420
+
421
+ let prev: string | null = null;
422
+ let stepHash = "";
423
+ for (const role of roles) {
424
+ stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
425
+ start: startHash,
426
+ prev: prev as CasRef | null,
427
+ role,
428
+ output: outputHash,
429
+ detail: null,
430
+ agent: "uwf-test",
431
+ });
432
+ prev = stepHash;
433
+ }
434
+ return stepHash;
435
+ }
436
+
437
+ test("same consecutive role shows <prompt> once", async () => {
438
+ const uwf = await makeUwfStore(tmpDir);
439
+ const headHash = await makeThreadWithRoles(uwf, ["writer", "writer"]);
440
+ const threadId = "01JTEST0000000000000003" as ThreadId;
441
+ await seedThreads(tmpDir, { [threadId]: headHash });
442
+
443
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
444
+ const count = (markdown.match(/<prompt>/g) ?? []).length;
445
+ expect(count).toBe(1);
446
+ });
447
+
448
+ test("different consecutive roles each show <prompt>", async () => {
449
+ const uwf = await makeUwfStore(tmpDir);
450
+ const headHash = await makeThreadWithRoles(uwf, ["planner", "coder"]);
451
+ const threadId = "01JTEST0000000000000004" as ThreadId;
452
+ await seedThreads(tmpDir, { [threadId]: headHash });
453
+
454
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
455
+ const count = (markdown.match(/<prompt>/g) ?? []).length;
456
+ expect(count).toBe(2);
457
+ });
458
+
459
+ test("non-consecutive same role shows <prompt> twice", async () => {
460
+ const uwf = await makeUwfStore(tmpDir);
461
+ const headHash = await makeThreadWithRoles(uwf, ["roleA", "roleB", "roleA"]);
462
+ const threadId = "01JTEST0000000000000005" as ThreadId;
463
+ await seedThreads(tmpDir, { [threadId]: headHash });
464
+
465
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
466
+ const count = (markdown.match(/<prompt>/g) ?? []).length;
467
+ expect(count).toBe(2);
468
+ });
469
+ });
470
+
471
+ // ── cmdThreadRead: showStart / before / quota ─────────────────────────────────
472
+
473
+ describe("cmdThreadRead start section / before / quota", () => {
474
+ async function makeSimpleThread(
475
+ uwf: UwfStore,
476
+ roles: string[],
477
+ ): Promise<{ startHash: CasRef; stepHashes: CasRef[] }> {
478
+ const uniqueRoles = [...new Set(roles)];
479
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
480
+ name: "simple-wf",
481
+ description: "desc",
482
+ roles: Object.fromEntries(
483
+ uniqueRoles.map((r) => [
484
+ r,
485
+ {
486
+ description: r,
487
+ goal: `Goal for ${r}`,
488
+ capabilities: [],
489
+ procedure: "Do stuff.",
490
+ output: "Output.",
491
+ meta: "placeholder00" as CasRef,
492
+ },
493
+ ]),
494
+ ),
495
+ conditions: {},
496
+ graph: {},
497
+ });
498
+ const startHash = (await uwf.store.cas.put(uwf.schemas.startNode, {
499
+ workflow: workflowHash,
500
+ prompt: "Initial prompt",
501
+ })) as CasRef;
502
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
503
+ name: "out",
504
+ description: "",
505
+ roles: {},
506
+ conditions: {},
507
+ graph: {},
508
+ });
509
+
510
+ const stepHashes: CasRef[] = [];
511
+ let prev: CasRef | null = null;
512
+ for (const role of roles) {
513
+ const stepHash = (await uwf.store.cas.put(uwf.schemas.stepNode, {
514
+ start: startHash,
515
+ prev,
516
+ role,
517
+ output: outputHash,
518
+ detail: null,
519
+ agent: "uwf-test",
520
+ })) as CasRef;
521
+ stepHashes.push(stepHash);
522
+ prev = stepHash;
523
+ }
524
+ return { startHash, stepHashes };
525
+ }
526
+
527
+ test("showStart=true includes # Thread header and ## Task section", async () => {
528
+ const uwf = await makeUwfStore(tmpDir);
529
+ const { stepHashes } = await makeSimpleThread(uwf, ["roleA"]);
530
+ const threadId = "01JTEST0000000000000006" as ThreadId;
531
+ await seedThreads(tmpDir, { [threadId]: stepHashes[stepHashes.length - 1]! });
532
+
533
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, true);
534
+ expect(markdown).toContain("# Thread");
535
+ expect(markdown).toContain("## Task");
536
+ expect(markdown).toContain("Initial prompt");
537
+ });
538
+
539
+ test("showStart=false with before=null still shows # Thread header (default behavior)", async () => {
540
+ const uwf = await makeUwfStore(tmpDir);
541
+ const { stepHashes } = await makeSimpleThread(uwf, ["roleA"]);
542
+ const threadId = "01JTEST0000000000000007" as ThreadId;
543
+ await seedThreads(tmpDir, { [threadId]: stepHashes[stepHashes.length - 1]! });
544
+
545
+ // When before=null, the start section is always shown regardless of showStart
546
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
547
+ expect(markdown).toContain("# Thread");
548
+ expect(markdown).toContain("## Task");
549
+ });
550
+
551
+ test("before filter: only steps before the given hash appear", async () => {
552
+ const uwf = await makeUwfStore(tmpDir);
553
+ const { stepHashes } = await makeSimpleThread(uwf, ["roleA", "roleB", "roleC"]);
554
+ const [_hashA, hashB, hashC] = stepHashes as [CasRef, CasRef, CasRef];
555
+ const threadId = "01JTEST0000000000000008" as ThreadId;
556
+ await seedThreads(tmpDir, { [threadId]: hashC });
557
+
558
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, hashB, false);
559
+ expect(markdown).toContain("roleA");
560
+ expect(markdown).not.toContain("roleB");
561
+ expect(markdown).not.toContain("roleC");
562
+ });
563
+
564
+ test("quota=1 limits output and includes skip hint", async () => {
565
+ const uwf = await makeUwfStore(tmpDir);
566
+ const { stepHashes } = await makeSimpleThread(uwf, ["roleA", "roleB", "roleC"]);
567
+ const threadId = "01JTEST000000000000000A" as ThreadId;
568
+ await seedThreads(tmpDir, { [threadId]: stepHashes[stepHashes.length - 1]! });
569
+
570
+ const markdown = await cmdThreadRead(tmpDir, threadId, 1, null, false);
571
+ expect(markdown).toContain("earlier step");
572
+ });
573
+
574
+ test("all steps fit in quota: no skip hint", async () => {
575
+ const uwf = await makeUwfStore(tmpDir);
576
+ const { stepHashes } = await makeSimpleThread(uwf, ["roleA"]);
577
+ const threadId = "01JTEST000000000000000B" as ThreadId;
578
+ await seedThreads(tmpDir, { [threadId]: stepHashes[0]! });
579
+
580
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
581
+ expect(markdown).not.toContain("earlier step");
582
+ });
583
+ });
584
+
585
+ // ── Tests that call process.exit must be last ─────────────────────────────────
586
+
587
+ describe("cmdStepShow (process.exit tests - must be last)", () => {
588
+ test("throws when step hash does not exist", async () => {
589
+ await expect(cmdStepShow(tmpDir, "nonexistenth0" as CasRef)).rejects.toThrow();
590
+ });
591
+
592
+ test("before with unknown hash rejects", async () => {
593
+ const uwfStore = await makeUwfStore(tmpDir);
594
+
595
+ const workflowHash = await uwfStore.store.cas.put(uwfStore.schemas.workflow, {
596
+ name: "wf2",
597
+ description: "",
598
+ roles: {
599
+ roleA: {
600
+ description: "r",
601
+ goal: "g",
602
+ capabilities: [],
603
+ procedure: "p",
604
+ output: "o",
605
+ meta: "placeholder00" as CasRef,
606
+ },
607
+ },
608
+ conditions: {},
609
+ graph: {},
610
+ });
611
+ const startHash = await uwfStore.store.cas.put(uwfStore.schemas.startNode, {
612
+ workflow: workflowHash,
613
+ prompt: "p",
614
+ });
615
+ const outputHash = await uwfStore.store.cas.put(uwfStore.schemas.workflow, {
616
+ name: "out",
617
+ description: "",
618
+ roles: {},
619
+ conditions: {},
620
+ graph: {},
621
+ });
622
+ const stepHash = await uwfStore.store.cas.put(uwfStore.schemas.stepNode, {
623
+ start: startHash,
624
+ prev: null,
625
+ role: "roleA",
626
+ output: outputHash,
627
+ detail: null,
628
+ agent: "uwf-test",
629
+ });
630
+ await seedThreads(tmpDir, { ["01JTEST000000000000000C" as ThreadId]: stepHash as CasRef });
631
+
632
+ await expect(
633
+ cmdThreadRead(
634
+ tmpDir,
635
+ "01JTEST000000000000000C" as ThreadId,
636
+ THREAD_READ_DEFAULT_QUOTA,
637
+ "unknownhash0" as CasRef,
638
+ false,
639
+ ),
640
+ ).rejects.toThrow();
641
+ });
642
+ });
643
+
644
+ // ── cmdStepList / cmdStepShow: completed threads ──────────────────────────────
645
+
646
+ describe("cmdStepList with completed threads", () => {
647
+ test("lists steps from active thread", async () => {
648
+ const uwf = await makeUwfStore(tmpDir);
649
+
650
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
651
+ name: "test-wf-active",
652
+ description: "desc",
653
+ roles: {},
654
+ conditions: {},
655
+ graph: {},
656
+ });
657
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
658
+ workflow: workflowHash,
659
+ prompt: "Start prompt",
660
+ });
661
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
662
+ name: "out",
663
+ description: "",
664
+ roles: {},
665
+ conditions: {},
666
+ graph: {},
667
+ });
668
+
669
+ const step1Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
670
+ start: startHash,
671
+ prev: null,
672
+ role: "role1",
673
+ output: outputHash,
674
+ detail: null,
675
+ agent: "uwf-test",
676
+ });
677
+ const step2Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
678
+ start: startHash,
679
+ prev: step1Hash,
680
+ role: "role2",
681
+ output: outputHash,
682
+ detail: null,
683
+ agent: "uwf-test",
684
+ });
685
+ const step3Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
686
+ start: startHash,
687
+ prev: step2Hash,
688
+ role: "role3",
689
+ output: outputHash,
690
+ detail: null,
691
+ agent: "uwf-test",
692
+ });
693
+
694
+ const threadId = "01JTEST0000000000000000A1" as ThreadId;
695
+ await seedThreads(tmpDir, { [threadId]: step3Hash });
696
+
697
+ const result = await cmdStepList(tmpDir, threadId);
698
+
699
+ expect(result.thread).toBe(threadId);
700
+ expect(result.steps).toHaveLength(4); // start + 3 steps
701
+ expect(result.steps[1].role).toBe("role1");
702
+ expect(result.steps[2].role).toBe("role2");
703
+ expect(result.steps[3].role).toBe("role3");
704
+ });
705
+
706
+ test("lists steps from completed thread", async () => {
707
+ const uwf = await makeUwfStore(tmpDir);
708
+
709
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
710
+ name: "test-wf-completed",
711
+ description: "desc",
712
+ roles: {},
713
+ conditions: {},
714
+ graph: {},
715
+ });
716
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
717
+ workflow: workflowHash,
718
+ prompt: "Start prompt",
719
+ });
720
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
721
+ name: "out",
722
+ description: "",
723
+ roles: {},
724
+ conditions: {},
725
+ graph: {},
726
+ });
727
+
728
+ const step1Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
729
+ start: startHash,
730
+ prev: null,
731
+ role: "roleA",
732
+ output: outputHash,
733
+ detail: null,
734
+ agent: "uwf-test",
735
+ });
736
+ const step2Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
737
+ start: startHash,
738
+ prev: step1Hash,
739
+ role: "roleB",
740
+ output: outputHash,
741
+ detail: null,
742
+ agent: "uwf-test",
743
+ });
744
+
745
+ const threadId = "01JTEST0000000000000000A2" as ThreadId;
746
+ // Thread is NOT in active index (simulating completed thread)
747
+ // But it IS in history variable store
748
+ setThread(uwf.varStore, threadId, {
749
+ head: step2Hash,
750
+ status: "idle",
751
+ suspendedRole: null,
752
+ suspendMessage: null,
753
+ completedAt: null,
754
+ });
755
+ completeThread(uwf.varStore, threadId, "completed");
756
+
757
+ const result = await cmdStepList(tmpDir, threadId);
758
+
759
+ expect(result.thread).toBe(threadId);
760
+ expect(result.steps).toHaveLength(3); // start + 2 steps
761
+ expect(result.steps[1].role).toBe("roleA");
762
+ expect(result.steps[2].role).toBe("roleB");
763
+ });
764
+ });
765
+
766
+ describe("cmdStepShow with completed threads", () => {
767
+ test("shows step detail from active thread", async () => {
768
+ const uwf = await makeUwfStore(tmpDir);
769
+ const detailSchemas = await registerDetailSchemas(uwf.store);
770
+
771
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
772
+ name: "test-wf-step-active",
773
+ description: "desc",
774
+ roles: {},
775
+ conditions: {},
776
+ graph: {},
777
+ });
778
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
779
+ workflow: workflowHash,
780
+ prompt: "p",
781
+ });
782
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
783
+ name: "out",
784
+ description: "",
785
+ roles: {},
786
+ conditions: {},
787
+ graph: {},
788
+ });
789
+
790
+ const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
791
+ index: 0,
792
+ role: "assistant",
793
+ content: "Active thread response",
794
+ toolCalls: null,
795
+ reasoning: null,
796
+ });
797
+ const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
798
+ sessionId: "sess-active",
799
+ model: "model-x",
800
+ duration: 1234,
801
+ turnCount: 1,
802
+ turns: [turnHash],
803
+ });
804
+
805
+ const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
806
+ start: startHash,
807
+ prev: null,
808
+ role: "coder",
809
+ output: outputHash,
810
+ detail: detailHash,
811
+ agent: "uwf-hermes",
812
+ });
813
+
814
+ const threadId = "01JTEST0000000000000000B1" as ThreadId;
815
+ await seedThreads(tmpDir, { [threadId]: stepHash });
816
+
817
+ const result = await cmdStepShow(tmpDir, stepHash);
818
+
819
+ expect(result).toMatchObject({
820
+ sessionId: "sess-active",
821
+ model: "model-x",
822
+ duration: 1234,
823
+ turnCount: 1,
824
+ });
825
+ });
826
+
827
+ test("shows step detail from completed thread", async () => {
828
+ const uwf = await makeUwfStore(tmpDir);
829
+ const detailSchemas = await registerDetailSchemas(uwf.store);
830
+
831
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
832
+ name: "test-wf-step-completed",
833
+ description: "desc",
834
+ roles: {},
835
+ conditions: {},
836
+ graph: {},
837
+ });
838
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
839
+ workflow: workflowHash,
840
+ prompt: "p",
841
+ });
842
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
843
+ name: "out",
844
+ description: "",
845
+ roles: {},
846
+ conditions: {},
847
+ graph: {},
848
+ });
849
+
850
+ const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
851
+ index: 0,
852
+ role: "assistant",
853
+ content: "Completed thread response",
854
+ toolCalls: null,
855
+ reasoning: null,
856
+ });
857
+ const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
858
+ sessionId: "sess-completed",
859
+ model: "model-y",
860
+ duration: 5678,
861
+ turnCount: 1,
862
+ turns: [turnHash],
863
+ });
864
+
865
+ const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
866
+ start: startHash,
867
+ prev: null,
868
+ role: "reviewer",
869
+ output: outputHash,
870
+ detail: detailHash,
871
+ agent: "uwf-hermes",
872
+ });
873
+
874
+ const threadId = "01JTEST0000000000000000B2" as ThreadId;
875
+ // Thread is NOT in active index
876
+ // But it IS in the unified store with completed status
877
+ setThread(uwf.varStore, threadId, {
878
+ head: stepHash,
879
+ status: "idle",
880
+ suspendedRole: null,
881
+ suspendMessage: null,
882
+ completedAt: null,
883
+ });
884
+ completeThread(uwf.varStore, threadId, "completed");
885
+
886
+ const result = await cmdStepShow(tmpDir, stepHash);
887
+
888
+ expect(result).toMatchObject({
889
+ sessionId: "sess-completed",
890
+ model: "model-y",
891
+ duration: 5678,
892
+ turnCount: 1,
893
+ });
894
+ });
895
+ });
896
+
897
+ describe("cmdThreadRead with completed threads", () => {
898
+ test("reads completed thread context", async () => {
899
+ const uwf = await makeUwfStore(tmpDir);
900
+
901
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
902
+ name: "test-wf-read-completed",
903
+ description: "desc",
904
+ roles: {
905
+ writer: {
906
+ description: "Write",
907
+ goal: "You are a writer.",
908
+ capabilities: [],
909
+ procedure: "Write content.",
910
+ output: "Summary.",
911
+ meta: "placeholder00" as CasRef,
912
+ },
913
+ },
914
+ conditions: {},
915
+ graph: {},
916
+ });
917
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
918
+ workflow: workflowHash,
919
+ prompt: "Write something",
920
+ });
921
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
922
+ name: "out",
923
+ description: "",
924
+ roles: {},
925
+ conditions: {},
926
+ graph: {},
927
+ });
928
+
929
+ const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
930
+ start: startHash,
931
+ prev: null,
932
+ role: "writer",
933
+ output: outputHash,
934
+ detail: null,
935
+ agent: "uwf-hermes",
936
+ });
937
+
938
+ const threadId = "01JTEST0000000000000000C1" as ThreadId;
939
+ // Thread is in store with completed status
940
+ setThread(uwf.varStore, threadId, {
941
+ head: stepHash,
942
+ status: "idle",
943
+ suspendedRole: null,
944
+ suspendMessage: null,
945
+ completedAt: null,
946
+ });
947
+ completeThread(uwf.varStore, threadId, "completed");
948
+
949
+ const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
950
+
951
+ expect(markdown).toContain("writer");
952
+ expect(markdown).toContain("Write something");
953
+ });
954
+
955
+ test("reads completed thread with before filter", async () => {
956
+ const uwf = await makeUwfStore(tmpDir);
957
+
958
+ const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
959
+ name: "test-wf-read-before",
960
+ description: "desc",
961
+ roles: {},
962
+ conditions: {},
963
+ graph: {},
964
+ });
965
+ const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
966
+ workflow: workflowHash,
967
+ prompt: "Do task",
968
+ });
969
+ const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
970
+ name: "out",
971
+ description: "",
972
+ roles: {},
973
+ conditions: {},
974
+ graph: {},
975
+ });
976
+
977
+ const step1Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
978
+ start: startHash,
979
+ prev: null,
980
+ role: "roleX",
981
+ output: outputHash,
982
+ detail: null,
983
+ agent: "uwf-test",
984
+ });
985
+ const step2Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
986
+ start: startHash,
987
+ prev: step1Hash,
988
+ role: "roleY",
989
+ output: outputHash,
990
+ detail: null,
991
+ agent: "uwf-test",
992
+ });
993
+ const step3Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
994
+ start: startHash,
995
+ prev: step2Hash,
996
+ role: "roleZ",
997
+ output: outputHash,
998
+ detail: null,
999
+ agent: "uwf-test",
1000
+ });
1001
+
1002
+ const threadId = "01JTEST0000000000000000C2" as ThreadId;
1003
+ setThread(uwf.varStore, threadId, {
1004
+ head: step3Hash,
1005
+ status: "idle",
1006
+ suspendedRole: null,
1007
+ suspendMessage: null,
1008
+ completedAt: null,
1009
+ });
1010
+ completeThread(uwf.varStore, threadId, "completed");
1011
+
1012
+ const markdown = await cmdThreadRead(
1013
+ tmpDir,
1014
+ threadId,
1015
+ THREAD_READ_DEFAULT_QUOTA,
1016
+ step2Hash,
1017
+ false,
1018
+ );
1019
+
1020
+ // Should contain step1 (roleX) but not step2 (roleY) or step3 (roleZ)
1021
+ expect(markdown).toContain("roleX");
1022
+ expect(markdown).not.toContain("roleY");
1023
+ expect(markdown).not.toContain("roleZ");
1024
+ });
1025
+ });