@united-workforce/cli 0.3.0 → 0.4.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 (219) hide show
  1. package/README.md +15 -8
  2. package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
  3. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  4. package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
  5. package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
  6. package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
  7. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
  8. package/dist/__tests__/build-step-entry.test.d.ts +2 -0
  9. package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
  10. package/dist/__tests__/build-step-entry.test.js +173 -0
  11. package/dist/__tests__/build-step-entry.test.js.map +1 -0
  12. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
  13. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
  14. package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
  15. package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
  16. package/dist/__tests__/config.test.js +26 -302
  17. package/dist/__tests__/config.test.js.map +1 -1
  18. package/dist/__tests__/current-role.test.js +7 -6
  19. package/dist/__tests__/current-role.test.js.map +1 -1
  20. package/dist/__tests__/e2e-mock-agent.test.js +20 -23
  21. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  22. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
  23. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
  24. package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
  25. package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
  26. package/dist/__tests__/moderator-evaluate.test.js +9 -50
  27. package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
  28. package/dist/__tests__/pid-recycling.test.d.ts +2 -0
  29. package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
  30. package/dist/__tests__/pid-recycling.test.js +271 -0
  31. package/dist/__tests__/pid-recycling.test.js.map +1 -0
  32. package/dist/__tests__/prompt.test.js +321 -0
  33. package/dist/__tests__/prompt.test.js.map +1 -1
  34. package/dist/__tests__/resolve-head-hash.test.js +4 -4
  35. package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
  36. package/dist/__tests__/setup-agent-discovery.test.js +21 -30
  37. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  38. package/dist/__tests__/setup-complexity.test.js +2 -168
  39. package/dist/__tests__/setup-complexity.test.js.map +1 -1
  40. package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
  41. package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
  42. package/dist/__tests__/setup-no-llm.test.js +52 -0
  43. package/dist/__tests__/setup-no-llm.test.js.map +1 -0
  44. package/dist/__tests__/solve-issue-tea-worktree.test.js +24 -27
  45. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
  46. package/dist/__tests__/step-ask.test.d.ts +2 -0
  47. package/dist/__tests__/step-ask.test.d.ts.map +1 -0
  48. package/dist/__tests__/step-ask.test.js +499 -0
  49. package/dist/__tests__/step-ask.test.js.map +1 -0
  50. package/dist/__tests__/step-show-json.test.js +1 -0
  51. package/dist/__tests__/step-show-json.test.js.map +1 -1
  52. package/dist/__tests__/step-timing.test.js +2 -0
  53. package/dist/__tests__/step-timing.test.js.map +1 -1
  54. package/dist/__tests__/store-global-cas.test.js +2 -2
  55. package/dist/__tests__/store-global-cas.test.js.map +1 -1
  56. package/dist/__tests__/store-unified-threads.test.js +9 -9
  57. package/dist/__tests__/store-unified-threads.test.js.map +1 -1
  58. package/dist/__tests__/thread-cancel-status.test.js +6 -6
  59. package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
  60. package/dist/__tests__/thread-list-filters.test.js +344 -9
  61. package/dist/__tests__/thread-list-filters.test.js.map +1 -1
  62. package/dist/__tests__/thread-poke.test.d.ts +2 -0
  63. package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
  64. package/dist/__tests__/thread-poke.test.js +412 -0
  65. package/dist/__tests__/thread-poke.test.js.map +1 -0
  66. package/dist/__tests__/thread-resume.test.js +10 -14
  67. package/dist/__tests__/thread-resume.test.js.map +1 -1
  68. package/dist/__tests__/thread-show-status.test.js +17 -28
  69. package/dist/__tests__/thread-show-status.test.js.map +1 -1
  70. package/dist/__tests__/thread-suspend-step.test.js +8 -14
  71. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  72. package/dist/__tests__/thread-suspended-display.test.js +10 -22
  73. package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
  74. package/dist/__tests__/thread.test.js +4 -4
  75. package/dist/__tests__/thread.test.js.map +1 -1
  76. package/dist/__tests__/validate-semantic.test.js +49 -21
  77. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  78. package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
  79. package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
  80. package/dist/__tests__/workflow-list-recursive.test.js +283 -0
  81. package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
  82. package/dist/__tests__/workflow-resolution.test.js +36 -21
  83. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  84. package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
  85. package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
  86. package/dist/__tests__/workflow-show-resolution.test.js +210 -0
  87. package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
  88. package/dist/__tests__/workflow-validate.test.d.ts +2 -0
  89. package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
  90. package/dist/__tests__/workflow-validate.test.js +687 -0
  91. package/dist/__tests__/workflow-validate.test.js.map +1 -0
  92. package/dist/background/background.d.ts +22 -1
  93. package/dist/background/background.d.ts.map +1 -1
  94. package/dist/background/background.js +83 -6
  95. package/dist/background/background.js.map +1 -1
  96. package/dist/background/index.d.ts +1 -1
  97. package/dist/background/index.d.ts.map +1 -1
  98. package/dist/background/index.js +1 -1
  99. package/dist/background/index.js.map +1 -1
  100. package/dist/background/types.d.ts +1 -0
  101. package/dist/background/types.d.ts.map +1 -1
  102. package/dist/cli.js +66 -31
  103. package/dist/cli.js.map +1 -1
  104. package/dist/commands/config.d.ts +3 -1
  105. package/dist/commands/config.d.ts.map +1 -1
  106. package/dist/commands/config.js +7 -33
  107. package/dist/commands/config.js.map +1 -1
  108. package/dist/commands/prompt.d.ts.map +1 -1
  109. package/dist/commands/prompt.js +15 -2
  110. package/dist/commands/prompt.js.map +1 -1
  111. package/dist/commands/setup.d.ts +7 -39
  112. package/dist/commands/setup.d.ts.map +1 -1
  113. package/dist/commands/setup.js +27 -302
  114. package/dist/commands/setup.js.map +1 -1
  115. package/dist/commands/step.d.ts +44 -1
  116. package/dist/commands/step.d.ts.map +1 -1
  117. package/dist/commands/step.js +255 -11
  118. package/dist/commands/step.js.map +1 -1
  119. package/dist/commands/thread.d.ts +16 -3
  120. package/dist/commands/thread.d.ts.map +1 -1
  121. package/dist/commands/thread.js +379 -140
  122. package/dist/commands/thread.js.map +1 -1
  123. package/dist/commands/workflow.d.ts +9 -1
  124. package/dist/commands/workflow.d.ts.map +1 -1
  125. package/dist/commands/workflow.js +130 -6
  126. package/dist/commands/workflow.js.map +1 -1
  127. package/dist/moderator/__tests__/evaluate.test.js +31 -17
  128. package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
  129. package/dist/moderator/evaluate.d.ts.map +1 -1
  130. package/dist/moderator/evaluate.js +4 -16
  131. package/dist/moderator/evaluate.js.map +1 -1
  132. package/dist/moderator/index.d.ts +1 -2
  133. package/dist/moderator/index.d.ts.map +1 -1
  134. package/dist/moderator/index.js +0 -1
  135. package/dist/moderator/index.js.map +1 -1
  136. package/dist/moderator/types.d.ts +6 -10
  137. package/dist/moderator/types.d.ts.map +1 -1
  138. package/dist/moderator/types.js +1 -3
  139. package/dist/moderator/types.js.map +1 -1
  140. package/dist/schemas.d.ts +2 -0
  141. package/dist/schemas.d.ts.map +1 -1
  142. package/dist/schemas.js +5 -3
  143. package/dist/schemas.js.map +1 -1
  144. package/dist/store.d.ts +28 -9
  145. package/dist/store.d.ts.map +1 -1
  146. package/dist/store.js +75 -16
  147. package/dist/store.js.map +1 -1
  148. package/dist/validate-semantic.d.ts.map +1 -1
  149. package/dist/validate-semantic.js +83 -66
  150. package/dist/validate-semantic.js.map +1 -1
  151. package/dist/validate.d.ts +6 -0
  152. package/dist/validate.d.ts.map +1 -1
  153. package/dist/validate.js +24 -0
  154. package/dist/validate.js.map +1 -1
  155. package/package.json +8 -10
  156. package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
  157. package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
  158. package/src/__tests__/build-step-entry.test.ts +203 -0
  159. package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
  160. package/src/__tests__/config.test.ts +33 -321
  161. package/src/__tests__/current-role.test.ts +7 -6
  162. package/src/__tests__/e2e-mock-agent.test.ts +20 -23
  163. package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
  164. package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
  165. package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
  166. package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
  167. package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
  168. package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
  169. package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
  170. package/src/__tests__/moderator-evaluate.test.ts +9 -52
  171. package/src/__tests__/pid-recycling.test.ts +328 -0
  172. package/src/__tests__/prompt.test.ts +397 -0
  173. package/src/__tests__/resolve-head-hash.test.ts +4 -4
  174. package/src/__tests__/setup-agent-discovery.test.ts +26 -51
  175. package/src/__tests__/setup-complexity.test.ts +1 -203
  176. package/src/__tests__/setup-no-llm.test.ts +68 -0
  177. package/src/__tests__/solve-issue-tea-worktree.test.ts +24 -30
  178. package/src/__tests__/step-ask.test.ts +670 -0
  179. package/src/__tests__/step-show-json.test.ts +1 -0
  180. package/src/__tests__/step-timing.test.ts +2 -0
  181. package/src/__tests__/store-global-cas.test.ts +2 -2
  182. package/src/__tests__/store-unified-threads.test.ts +9 -9
  183. package/src/__tests__/thread-cancel-status.test.ts +6 -6
  184. package/src/__tests__/thread-list-filters.test.ts +434 -8
  185. package/src/__tests__/thread-poke.test.ts +545 -0
  186. package/src/__tests__/thread-resume.test.ts +10 -14
  187. package/src/__tests__/thread-show-status.test.ts +17 -29
  188. package/src/__tests__/thread-suspend-step.test.ts +8 -14
  189. package/src/__tests__/thread-suspended-display.test.ts +10 -22
  190. package/src/__tests__/thread.test.ts +4 -4
  191. package/src/__tests__/validate-semantic.test.ts +59 -31
  192. package/src/__tests__/workflow-list-recursive.test.ts +370 -0
  193. package/src/__tests__/workflow-resolution.test.ts +39 -21
  194. package/src/__tests__/workflow-show-resolution.test.ts +285 -0
  195. package/src/__tests__/workflow-validate.test.ts +806 -0
  196. package/src/background/background.ts +88 -6
  197. package/src/background/index.ts +2 -0
  198. package/src/background/types.ts +1 -0
  199. package/src/cli.ts +97 -47
  200. package/src/commands/config.ts +7 -35
  201. package/src/commands/prompt.ts +15 -2
  202. package/src/commands/setup.ts +29 -357
  203. package/src/commands/step.ts +339 -12
  204. package/src/commands/thread.ts +463 -169
  205. package/src/commands/workflow.ts +159 -4
  206. package/src/moderator/__tests__/evaluate.test.ts +34 -17
  207. package/src/moderator/evaluate.ts +5 -17
  208. package/src/moderator/index.ts +1 -6
  209. package/src/moderator/types.ts +6 -14
  210. package/src/schemas.ts +13 -3
  211. package/src/store.ts +86 -20
  212. package/src/validate-semantic.ts +109 -78
  213. package/src/validate.ts +27 -0
  214. package/dist/__tests__/setup-validate.test.d.ts +0 -2
  215. package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
  216. package/dist/__tests__/setup-validate.test.js +0 -108
  217. package/dist/__tests__/setup-validate.test.js.map +0 -1
  218. package/src/__tests__/setup-validate.test.ts +0 -148
  219. /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
@@ -230,11 +230,11 @@ describe("Global CAS directory", () => {
230
230
  const { createThreadIndexEntry } = await import("@united-workforce/protocol");
231
231
 
232
232
  setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
233
- completeThread(uwf.varStore, threadId, "completed");
233
+ completeThread(uwf.varStore, threadId, "end");
234
234
 
235
235
  const entry = getThread(uwf.varStore, threadId);
236
236
  expect(entry?.head).toBe(headHash);
237
- expect(entry?.status).toBe("completed");
237
+ expect(entry?.status).toBe("end");
238
238
 
239
239
  const { access } = await import("node:fs/promises");
240
240
  await access(join(globalCasDir, "vars"));
@@ -46,7 +46,7 @@ describe("unified thread storage", () => {
46
46
 
47
47
  setThread(uwf.varStore, threadId2, {
48
48
  head: head2,
49
- status: "completed",
49
+ status: "end",
50
50
  suspendedRole: null,
51
51
  suspendMessage: null,
52
52
  completedAt: Date.now(),
@@ -110,7 +110,7 @@ describe("unified thread storage", () => {
110
110
 
111
111
  setThread(uwf.varStore, threadId2, {
112
112
  head: head2,
113
- status: "completed",
113
+ status: "end",
114
114
  suspendedRole: null,
115
115
  suspendMessage: null,
116
116
  completedAt: Date.now(),
@@ -145,11 +145,11 @@ describe("unified thread storage", () => {
145
145
  completedAt: null,
146
146
  });
147
147
 
148
- completeThread(uwf.varStore, threadId, "completed");
148
+ completeThread(uwf.varStore, threadId, "end");
149
149
 
150
150
  const entry = getThread(uwf.varStore, threadId);
151
151
  expect(entry).not.toBeNull();
152
- expect(entry?.status).toBe("completed");
152
+ expect(entry?.status).toBe("end");
153
153
  expect(entry?.completedAt).toBeDefined();
154
154
  expect(entry?.completedAt).toBeGreaterThan(0);
155
155
  });
@@ -191,11 +191,11 @@ describe("unified thread storage", () => {
191
191
  completedAt: null,
192
192
  });
193
193
 
194
- completeThread(uwf.varStore, threadId, "completed");
194
+ completeThread(uwf.varStore, threadId, "end");
195
195
 
196
196
  const entry = getThread(uwf.varStore, threadId);
197
197
  expect(entry).not.toBeNull();
198
- expect(entry?.status).toBe("completed");
198
+ expect(entry?.status).toBe("end");
199
199
  expect(entry?.suspendedRole).toBeNull();
200
200
  expect(entry?.suspendMessage).toBeNull();
201
201
  });
@@ -206,7 +206,7 @@ describe("unified thread storage", () => {
206
206
  const threadId = "01JTEST000000000000NOEXIST" as ThreadId;
207
207
 
208
208
  // Should not throw
209
- completeThread(uwf.varStore, threadId, "completed");
209
+ completeThread(uwf.varStore, threadId, "end");
210
210
 
211
211
  const entry = getThread(uwf.varStore, threadId);
212
212
  expect(entry).toBeNull();
@@ -221,7 +221,7 @@ describe("unified thread storage", () => {
221
221
 
222
222
  setThread(uwf.varStore, threadId, {
223
223
  head,
224
- status: "completed",
224
+ status: "end",
225
225
  suspendedRole: null,
226
226
  suspendMessage: null,
227
227
  completedAt: now,
@@ -229,7 +229,7 @@ describe("unified thread storage", () => {
229
229
 
230
230
  const entry = getThread(uwf.varStore, threadId);
231
231
  expect(entry).not.toBeNull();
232
- expect(entry?.status).toBe("completed");
232
+ expect(entry?.status).toBe("end");
233
233
  expect(entry?.completedAt).toBe(now);
234
234
  });
235
235
  });
@@ -61,11 +61,11 @@ describe("thread cancel status", () => {
61
61
  completedAt: null,
62
62
  });
63
63
 
64
- completeThread(uwf.varStore, threadId, "completed");
64
+ completeThread(uwf.varStore, threadId, "end");
65
65
 
66
66
  const entry = getThread(uwf.varStore, threadId);
67
67
  expect(entry).not.toBeNull();
68
- expect(entry?.status).toBe("completed");
68
+ expect(entry?.status).toBe("end");
69
69
  });
70
70
 
71
71
  test("loadHistoryThreads returns completed and cancelled", async () => {
@@ -82,7 +82,7 @@ describe("thread cancel status", () => {
82
82
  suspendMessage: null,
83
83
  completedAt: null,
84
84
  });
85
- completeThread(uwf.varStore, threadId1, "completed");
85
+ completeThread(uwf.varStore, threadId1, "end");
86
86
 
87
87
  const threadId2 = "01JTEST000000000000CANCEL5" as ThreadId;
88
88
  setThread(uwf.varStore, threadId2, {
@@ -99,7 +99,7 @@ describe("thread cancel status", () => {
99
99
  const statuses = Object.values(history)
100
100
  .map((entry) => entry.status)
101
101
  .sort();
102
- expect(statuses).toEqual(["cancelled", "completed"]);
102
+ expect(statuses).toEqual(["cancelled", "end"]);
103
103
  });
104
104
 
105
105
  test("mixed completed and cancelled entries preserve distinct statuses", async () => {
@@ -116,7 +116,7 @@ describe("thread cancel status", () => {
116
116
  suspendMessage: null,
117
117
  completedAt: null,
118
118
  });
119
- completeThread(uwf.varStore, threadId1, "completed");
119
+ completeThread(uwf.varStore, threadId1, "end");
120
120
 
121
121
  const threadId2 = "01JTEST000000000000CANCEL7" as ThreadId;
122
122
  setThread(uwf.varStore, threadId2, {
@@ -133,6 +133,6 @@ describe("thread cancel status", () => {
133
133
  const statuses = Object.values(history)
134
134
  .map((entry) => entry.status)
135
135
  .sort();
136
- expect(statuses).toEqual(["cancelled", "completed"]);
136
+ expect(statuses).toEqual(["cancelled", "end"]);
137
137
  });
138
138
  });
@@ -5,7 +5,7 @@ import type { CasRef, ThreadId } from "@united-workforce/protocol";
5
5
  import { createThreadIndexEntry } from "@united-workforce/protocol";
6
6
  import { extractUlidTimestamp, generateUlid } from "@united-workforce/util";
7
7
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
8
- import { createMarker, deleteMarker } from "../background/index.js";
8
+ import { createMarker, deleteMarker, getProcessStartTime } from "../background/index.js";
9
9
  import { cmdThreadList } from "../commands/thread.js";
10
10
  import { parseTimeInput } from "../commands/thread-time-parser.js";
11
11
  import type { UwfStore } from "../store.js";
@@ -13,6 +13,7 @@ import {
13
13
  completeThread as completeThreadInStore,
14
14
  createUwfStore,
15
15
  loadAllThreads,
16
+ saveWorkflowRegistry,
16
17
  setThread,
17
18
  } from "../store.js";
18
19
 
@@ -66,6 +67,7 @@ async function markThreadRunning(storageRoot: string, threadId: ThreadId, workfl
66
67
  workflow,
67
68
  pid: process.pid, // Use current process PID so isPidAlive returns true
68
69
  startedAt: Date.now(),
70
+ processStartTime: getProcessStartTime(process.pid),
69
71
  });
70
72
  }
71
73
 
@@ -76,7 +78,7 @@ async function completeThread(
76
78
  _headHash: CasRef,
77
79
  ) {
78
80
  const uwfIdx = await createUwfStore(storageRoot);
79
- completeThreadInStore(uwfIdx.varStore, threadId, "completed");
81
+ completeThreadInStore(uwfIdx.varStore, threadId, "end");
80
82
  }
81
83
 
82
84
  // ── test setup ────────────────────────────────────────────────────────────────
@@ -135,7 +137,7 @@ describe("cmdThreadList status filter", () => {
135
137
  if (thread3Head === undefined) throw new Error("thread3 head not found");
136
138
  await completeThread(tmpDir, thread3, workflowHash, thread3Head);
137
139
 
138
- const result = await cmdThreadList(tmpDir, ["idle", "completed"], null, null, null, null);
140
+ const result = await cmdThreadList(tmpDir, ["idle", "end"], null, null, null, null);
139
141
 
140
142
  // Clean up marker
141
143
  await deleteMarker(tmpDir, thread2);
@@ -160,14 +162,14 @@ describe("cmdThreadList status filter", () => {
160
162
  if (thread3Head === undefined) throw new Error("thread3 head not found");
161
163
  await completeThread(tmpDir, thread3, workflowHash, thread3Head);
162
164
 
163
- const result = await cmdThreadList(tmpDir, ["completed"], null, null, null, null);
165
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
164
166
 
165
167
  expect(result).toHaveLength(1);
166
168
  expect(result[0]?.thread).toBe(thread3);
167
- expect(result[0]?.status).toBe("completed");
169
+ expect(result[0]?.status).toBe("end");
168
170
  });
169
171
 
170
- test("should return all threads when no status filter provided", async () => {
172
+ test("should return only active threads when no filter and no --all", async () => {
171
173
  const uwf = await makeUwfStore(tmpDir);
172
174
  const workflowHash = await createTestWorkflow(uwf);
173
175
 
@@ -185,8 +187,290 @@ describe("cmdThreadList status filter", () => {
185
187
 
186
188
  const result = await cmdThreadList(tmpDir, null, null, null, null, null);
187
189
 
190
+ // Default behavior (issue #147): only active threads (idle + running)
191
+ expect(result).toHaveLength(2);
192
+ expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2].sort());
193
+
194
+ // Clean up marker
195
+ await deleteMarker(tmpDir, thread2);
196
+ });
197
+
198
+ test("should return all threads when --all (showAll=true)", async () => {
199
+ const uwf = await makeUwfStore(tmpDir);
200
+ const workflowHash = await createTestWorkflow(uwf);
201
+
202
+ const thread1 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
203
+ const thread2 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
204
+ const thread3 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
205
+
206
+ await markThreadRunning(tmpDir, thread2, workflowHash);
207
+
208
+ const uwfIdx = await createUwfStore(tmpDir);
209
+ const index = loadAllThreads(uwfIdx.varStore);
210
+ const thread3Head = index[thread3]!.head;
211
+ if (thread3Head === undefined) throw new Error("thread3 head not found");
212
+ await completeThread(tmpDir, thread3, workflowHash, thread3Head);
213
+
214
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
215
+
188
216
  expect(result).toHaveLength(3);
189
217
  expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2, thread3].sort());
218
+
219
+ // Clean up marker
220
+ await deleteMarker(tmpDir, thread2);
221
+ });
222
+ });
223
+
224
+ // ── default behavior tests (issue #147) ───────────────────────────────────────
225
+
226
+ describe("cmdThreadList default behavior (issue #147)", () => {
227
+ test("default returns only idle + running threads", async () => {
228
+ const uwf = await makeUwfStore(tmpDir);
229
+ const workflowHash = await createTestWorkflow(uwf);
230
+
231
+ const threadA = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
232
+ const threadB = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
233
+ const threadC = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
234
+ const threadD = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
235
+
236
+ await markThreadRunning(tmpDir, threadB, workflowHash);
237
+
238
+ const uwfIdx = await createUwfStore(tmpDir);
239
+ const index = loadAllThreads(uwfIdx.varStore);
240
+ const threadCHead = index[threadC]!.head;
241
+ if (threadCHead === undefined) throw new Error("threadC head not found");
242
+ await completeThread(tmpDir, threadC, workflowHash, threadCHead);
243
+
244
+ // Cancel threadD
245
+ const threadDHead = index[threadD]!.head;
246
+ if (threadDHead === undefined) throw new Error("threadD head not found");
247
+ const uwfCancel = await createUwfStore(tmpDir);
248
+ completeThreadInStore(uwfCancel.varStore, threadD, "cancelled");
249
+
250
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
251
+
252
+ expect(result).toHaveLength(2);
253
+ expect(result.map((r) => r.thread).sort()).toEqual([threadA, threadB].sort());
254
+
255
+ await deleteMarker(tmpDir, threadB);
256
+ });
257
+
258
+ test("default excludes completed threads", async () => {
259
+ const uwf = await makeUwfStore(tmpDir);
260
+ const workflowHash = await createTestWorkflow(uwf);
261
+
262
+ const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 6000);
263
+ const completedThreads: ThreadId[] = [];
264
+ for (let i = 0; i < 5; i++) {
265
+ const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
266
+ completedThreads.push(t);
267
+ const uwfIdx = await createUwfStore(tmpDir);
268
+ const index = loadAllThreads(uwfIdx.varStore);
269
+ const head = index[t]!.head;
270
+ if (head === undefined) throw new Error("head not found");
271
+ await completeThread(tmpDir, t, workflowHash, head);
272
+ }
273
+
274
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
275
+
276
+ expect(result).toHaveLength(1);
277
+ expect(result[0]?.thread).toBe(idleThread);
278
+ });
279
+
280
+ test("default excludes cancelled threads", async () => {
281
+ const uwf = await makeUwfStore(tmpDir);
282
+ const workflowHash = await createTestWorkflow(uwf);
283
+
284
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
285
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
286
+
287
+ const cancelled: ThreadId[] = [];
288
+ for (let i = 0; i < 3; i++) {
289
+ const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (3 - i) * 1000);
290
+ cancelled.push(t);
291
+ const uwfIdx = await createUwfStore(tmpDir);
292
+ completeThreadInStore(uwfIdx.varStore, t, "cancelled");
293
+ }
294
+
295
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
296
+
297
+ expect(result).toHaveLength(1);
298
+ expect(result[0]?.thread).toBe(runningThread);
299
+
300
+ await deleteMarker(tmpDir, runningThread);
301
+ });
302
+
303
+ test("--all (showAll=true) returns every status", async () => {
304
+ const uwf = await makeUwfStore(tmpDir);
305
+ const workflowHash = await createTestWorkflow(uwf);
306
+
307
+ const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
308
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
309
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
310
+
311
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
312
+ const uwfIdx = await createUwfStore(tmpDir);
313
+ const idx = loadAllThreads(uwfIdx.varStore);
314
+ const ch = idx[completedThread]!.head;
315
+ if (ch === undefined) throw new Error("completedThread head not found");
316
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
317
+
318
+ const cancelledThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
319
+ completeThreadInStore(uwfIdx.varStore, cancelledThread, "cancelled");
320
+
321
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
322
+
323
+ expect(result).toHaveLength(4);
324
+ expect(result.map((r) => r.thread).sort()).toEqual(
325
+ [idleThread, runningThread, completedThread, cancelledThread].sort(),
326
+ );
327
+
328
+ await deleteMarker(tmpDir, runningThread);
329
+ });
330
+
331
+ test("explicit --status overrides default (still returns just the filtered statuses)", async () => {
332
+ const uwf = await makeUwfStore(tmpDir);
333
+ const workflowHash = await createTestWorkflow(uwf);
334
+
335
+ const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
336
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
337
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
338
+
339
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
340
+ const uwfIdx = await createUwfStore(tmpDir);
341
+ const idx = loadAllThreads(uwfIdx.varStore);
342
+ const ch = idx[completedThread]!.head;
343
+ if (ch === undefined) throw new Error("completedThread head not found");
344
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
345
+
346
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
347
+
348
+ expect(result).toHaveLength(1);
349
+ expect(result[0]?.thread).toBe(completedThread);
350
+ expect(result[0]?.status).toBe("end");
351
+
352
+ await deleteMarker(tmpDir, runningThread);
353
+ });
354
+
355
+ test("--status active keeps working", async () => {
356
+ const uwf = await makeUwfStore(tmpDir);
357
+ const workflowHash = await createTestWorkflow(uwf);
358
+
359
+ const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
360
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
361
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
362
+
363
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
364
+ const uwfIdx = await createUwfStore(tmpDir);
365
+ const idx = loadAllThreads(uwfIdx.varStore);
366
+ const ch = idx[completedThread]!.head;
367
+ if (ch === undefined) throw new Error("completedThread head not found");
368
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
369
+
370
+ const result = await cmdThreadList(tmpDir, ["idle", "running"], null, null, null, null);
371
+
372
+ expect(result).toHaveLength(2);
373
+ expect(result.map((r) => r.thread).sort()).toEqual([idleThread, runningThread].sort());
374
+
375
+ await deleteMarker(tmpDir, runningThread);
376
+ });
377
+
378
+ test("--status + --all — explicit status wins", async () => {
379
+ const uwf = await makeUwfStore(tmpDir);
380
+ const workflowHash = await createTestWorkflow(uwf);
381
+
382
+ const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
383
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
384
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
385
+
386
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
387
+ const uwfIdx = await createUwfStore(tmpDir);
388
+ const idx = loadAllThreads(uwfIdx.varStore);
389
+ const ch = idx[completedThread]!.head;
390
+ if (ch === undefined) throw new Error("completedThread head not found");
391
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
392
+
393
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null, true);
394
+
395
+ expect(result).toHaveLength(1);
396
+ expect(result[0]?.thread).toBe(completedThread);
397
+
398
+ await deleteMarker(tmpDir, runningThread);
399
+ });
400
+
401
+ test("default returns empty when no threads", async () => {
402
+ await makeUwfStore(tmpDir);
403
+
404
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
405
+
406
+ expect(result).toHaveLength(0);
407
+ });
408
+
409
+ test("default + time range filter composes correctly", async () => {
410
+ const uwf = await makeUwfStore(tmpDir);
411
+ const workflowHash = await createTestWorkflow(uwf);
412
+
413
+ const ts1 = Date.UTC(2026, 4, 20, 0, 0, 0);
414
+ const ts2 = Date.UTC(2026, 4, 21, 0, 0, 0);
415
+ const ts3 = Date.UTC(2026, 4, 22, 0, 0, 0);
416
+ const ts4 = Date.UTC(2026, 4, 23, 0, 0, 0);
417
+ const ts5 = Date.UTC(2026, 4, 24, 0, 0, 0);
418
+
419
+ const _t1 = await createTestThread(uwf, tmpDir, workflowHash, ts1);
420
+ const t2 = await createTestThread(uwf, tmpDir, workflowHash, ts2);
421
+ const t3 = await createTestThread(uwf, tmpDir, workflowHash, ts3);
422
+ const t4 = await createTestThread(uwf, tmpDir, workflowHash, ts4);
423
+ const _t5 = await createTestThread(uwf, tmpDir, workflowHash, ts5);
424
+
425
+ // Mark t3 running
426
+ await markThreadRunning(tmpDir, t3, workflowHash);
427
+
428
+ // Complete t4 (should be excluded by default)
429
+ const uwfIdx = await createUwfStore(tmpDir);
430
+ const idx = loadAllThreads(uwfIdx.varStore);
431
+ const t4head = idx[t4]!.head;
432
+ if (t4head === undefined) throw new Error("t4 head not found");
433
+ await completeThread(tmpDir, t4, workflowHash, t4head);
434
+
435
+ // afterMs in middle of range to exclude _t1
436
+ const afterMs = Date.UTC(2026, 4, 20, 12, 0, 0);
437
+ const result = await cmdThreadList(tmpDir, null, afterMs, null, null, null);
438
+
439
+ // Expected: t2 (idle), t3 (running), _t5 (idle); excludes t4 (completed) and _t1 (filtered by time)
440
+ expect(result).toHaveLength(3);
441
+ const ids = result.map((r) => r.thread).sort();
442
+ expect(ids).toEqual([t2, t3, _t5].sort());
443
+
444
+ await deleteMarker(tmpDir, t3);
445
+ });
446
+
447
+ test("default + pagination composes correctly", async () => {
448
+ const uwf = await makeUwfStore(tmpDir);
449
+ const workflowHash = await createTestWorkflow(uwf);
450
+
451
+ // Create 10 idle threads + 5 completed threads
452
+ const idleThreads: ThreadId[] = [];
453
+ for (let i = 0; i < 10; i++) {
454
+ idleThreads.push(
455
+ await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (15 - i) * 1000),
456
+ );
457
+ }
458
+ for (let i = 0; i < 5; i++) {
459
+ const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
460
+ const uwfIdx = await createUwfStore(tmpDir);
461
+ const idx = loadAllThreads(uwfIdx.varStore);
462
+ const head = idx[t]!.head;
463
+ if (head === undefined) throw new Error("head not found");
464
+ await completeThread(tmpDir, t, workflowHash, head);
465
+ }
466
+
467
+ const result = await cmdThreadList(tmpDir, null, null, null, 2, 3);
468
+
469
+ expect(result).toHaveLength(3);
470
+ // All results should be idle (default excludes completed)
471
+ for (const r of result) {
472
+ expect(r.status).toBe("idle");
473
+ }
190
474
  });
191
475
  });
192
476
 
@@ -382,11 +666,11 @@ describe("combined filters", () => {
382
666
  await completeThread(tmpDir, thread, workflowHash, headHash);
383
667
  }
384
668
 
385
- const result = await cmdThreadList(tmpDir, ["completed"], null, null, 3, 5);
669
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, 3, 5);
386
670
 
387
671
  expect(result).toHaveLength(5);
388
672
  for (const r of result) {
389
- expect(r.status).toBe("completed");
673
+ expect(r.status).toBe("end");
390
674
  }
391
675
  });
392
676
 
@@ -570,3 +854,145 @@ describe("ISO date parsing", () => {
570
854
  expect(() => parseTimeInput("invalid", nowMs)).toThrow();
571
855
  });
572
856
  });
857
+
858
+ // ── corrupt thread resilience (#250) ──────────────────────────────────────────
859
+
860
+ describe("corrupt thread resilience (#250)", () => {
861
+ test("thread list returns corrupt entry when CAS node is missing", async () => {
862
+ const uwf = await makeUwfStore(tmpDir);
863
+
864
+ // Create a valid thread
865
+ const workflowHash = await createTestWorkflow(uwf);
866
+ const now = Date.now();
867
+ const _validId = await createTestThread(uwf, tmpDir, workflowHash, now);
868
+
869
+ // Create another thread with a unique start node, then delete its workflow CAS to corrupt it
870
+ const corruptThreadId = generateUlid(now + 1000) as ThreadId;
871
+ const startPayload = {
872
+ workflow: workflowHash,
873
+ prompt: "corrupt thread prompt — unique to avoid CAS hash collision",
874
+ cwd: tmpDir,
875
+ };
876
+ const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
877
+ setThread(uwf.varStore, corruptThreadId, createThreadIndexEntry(headHash));
878
+
879
+ // Delete the workflow CAS node — start node still exists but workflow ref dangles
880
+ uwf.store.cas.delete(workflowHash);
881
+
882
+ // thread list should NOT throw — it should return both threads
883
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
884
+
885
+ // Both threads should appear (the valid one is now also corrupt since workflow is shared)
886
+ // In practice: both become corrupt because they share the same workflow CAS node
887
+ // This matches the real scenario from issue #250 — gc deleted a shared node
888
+ expect(result.length).toBeGreaterThanOrEqual(2);
889
+ const corruptItems = result.filter((r) => r.status === "corrupt");
890
+ expect(corruptItems.length).toBeGreaterThanOrEqual(1);
891
+ for (const item of corruptItems) {
892
+ expect(item.statusDisplay).toBe("corrupt");
893
+ }
894
+ });
895
+
896
+ test("corrupt threads appear in default filter (without --all)", async () => {
897
+ const uwf = await makeUwfStore(tmpDir);
898
+ const workflowHash = await createTestWorkflow(uwf);
899
+
900
+ // Create a thread then corrupt it
901
+ const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
902
+ const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
903
+ uwf.store.cas.delete(corruptEntry.head);
904
+
905
+ // Default filter (no --all, no --status) should include corrupt
906
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
907
+ expect(result).toHaveLength(1);
908
+ expect(result[0].status).toBe("corrupt");
909
+ });
910
+ });
911
+
912
+ // ── orphan thread detection (#286) ────────────────────────────────────────────
913
+
914
+ describe("orphan thread detection (#286)", () => {
915
+ test("thread list includes workflowName when workflow is in registry", async () => {
916
+ const uwf = await makeUwfStore(tmpDir);
917
+ const workflowHash = await createTestWorkflow(uwf);
918
+
919
+ // Register the workflow in registry
920
+ saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
921
+
922
+ const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
923
+
924
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
925
+ expect(result).toHaveLength(1);
926
+ expect(result[0].thread).toBe(threadId);
927
+ expect(result[0].workflowName).toBe("test-workflow");
928
+ });
929
+
930
+ test("thread list returns workflowName: null for orphaned threads", async () => {
931
+ const uwf = await makeUwfStore(tmpDir);
932
+ const workflowHash = await createTestWorkflow(uwf);
933
+
934
+ // Do NOT register the workflow — thread is orphaned
935
+ const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
936
+
937
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
938
+ expect(result).toHaveLength(1);
939
+ expect(result[0].thread).toBe(threadId);
940
+ expect(result[0].workflowName).toBeNull();
941
+ });
942
+
943
+ test("mixed registered and orphaned threads in the same list", async () => {
944
+ const uwf = await makeUwfStore(tmpDir);
945
+ const workflowHash = await createTestWorkflow(uwf);
946
+
947
+ // Register the workflow
948
+ saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
949
+
950
+ // Create a thread using the registered workflow
951
+ const now = Date.now();
952
+ const registeredId = await createTestThread(uwf, tmpDir, workflowHash, now);
953
+
954
+ // Create a second workflow (different hash), not registered
955
+ const orphanWorkflowPayload = {
956
+ name: "orphan-workflow",
957
+ roles: {
958
+ role1: {
959
+ goal: "orphan goal",
960
+ outputSchema: { type: "object" as const, properties: {} },
961
+ },
962
+ },
963
+ graph: { start: "role1" },
964
+ conditions: {},
965
+ };
966
+ const orphanHash = await uwf.store.cas.put(uwf.schemas.workflow, orphanWorkflowPayload);
967
+ const orphanId = await createTestThread(uwf, tmpDir, orphanHash, now + 1000);
968
+
969
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
970
+ expect(result).toHaveLength(2);
971
+
972
+ // Sorted newest first, so orphan (later timestamp) comes first
973
+ const orphanItem = result.find((r) => r.thread === orphanId);
974
+ const registeredItem = result.find((r) => r.thread === registeredId);
975
+
976
+ expect(orphanItem).toBeDefined();
977
+ expect(orphanItem!.workflowName).toBeNull();
978
+
979
+ expect(registeredItem).toBeDefined();
980
+ expect(registeredItem!.workflowName).toBe("test-workflow");
981
+ });
982
+
983
+ test("corrupt threads have workflowName: null", async () => {
984
+ const uwf = await makeUwfStore(tmpDir);
985
+ const workflowHash = await createTestWorkflow(uwf);
986
+ saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
987
+
988
+ // Create a thread then corrupt it by deleting its head CAS node
989
+ const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
990
+ const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
991
+ uwf.store.cas.delete(corruptEntry.head);
992
+
993
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
994
+ expect(result).toHaveLength(1);
995
+ expect(result[0].status).toBe("corrupt");
996
+ expect(result[0].workflowName).toBeNull();
997
+ });
998
+ });