@united-workforce/cli 0.6.1 → 0.8.1

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 (167) hide show
  1. package/README.md +120 -5
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
  4. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
  5. package/dist/__tests__/broker-prompt.test.d.ts +10 -0
  6. package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
  7. package/dist/__tests__/broker-prompt.test.js +129 -0
  8. package/dist/__tests__/broker-prompt.test.js.map +1 -0
  9. package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
  10. package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
  11. package/dist/__tests__/broker-step-active-turns.test.js +428 -0
  12. package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
  13. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
  14. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
  15. package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
  16. package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
  17. package/dist/__tests__/config.test.js +33 -37
  18. package/dist/__tests__/config.test.js.map +1 -1
  19. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
  20. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
  21. package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
  22. package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
  23. package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
  24. package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
  25. package/dist/__tests__/e2e-broker-step.test.js +278 -0
  26. package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
  27. package/dist/__tests__/e2e-mock-agent.test.js +1 -1
  28. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  29. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
  30. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
  31. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
  32. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
  33. package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
  34. package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
  35. package/dist/__tests__/log-tag-validity.test.js +110 -0
  36. package/dist/__tests__/log-tag-validity.test.js.map +1 -0
  37. package/dist/__tests__/setup-agent-discovery.test.js +35 -23
  38. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  39. package/dist/__tests__/setup-no-llm.test.js +5 -2
  40. package/dist/__tests__/setup-no-llm.test.js.map +1 -1
  41. package/dist/__tests__/step-ask.test.js +9 -6
  42. package/dist/__tests__/step-ask.test.js.map +1 -1
  43. package/dist/__tests__/step-show-json.test.js +5 -5
  44. package/dist/__tests__/step-show-json.test.js.map +1 -1
  45. package/dist/__tests__/step-show-text.test.d.ts +2 -0
  46. package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
  47. package/dist/__tests__/step-show-text.test.js +192 -0
  48. package/dist/__tests__/step-show-text.test.js.map +1 -0
  49. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
  50. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
  51. package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
  52. package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
  53. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
  54. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
  55. package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
  56. package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
  57. package/dist/__tests__/step-turns.test.d.ts +24 -0
  58. package/dist/__tests__/step-turns.test.d.ts.map +1 -0
  59. package/dist/__tests__/step-turns.test.js +646 -0
  60. package/dist/__tests__/step-turns.test.js.map +1 -0
  61. package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
  62. package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
  63. package/dist/__tests__/store-turn-chain.test.js +341 -0
  64. package/dist/__tests__/store-turn-chain.test.js.map +1 -0
  65. package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
  66. package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
  67. package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
  68. package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
  69. package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
  70. package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
  71. package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
  72. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
  73. package/dist/__tests__/thread-poke.test.js +6 -6
  74. package/dist/__tests__/thread-poke.test.js.map +1 -1
  75. package/dist/__tests__/thread-resume.test.js +2 -2
  76. package/dist/__tests__/thread-resume.test.js.map +1 -1
  77. package/dist/__tests__/thread-suspend-step.test.js +1 -1
  78. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  79. package/dist/__tests__/thread.test.js +28 -14
  80. package/dist/__tests__/thread.test.js.map +1 -1
  81. package/dist/cli.js +910 -344
  82. package/dist/cli.js.map +1 -1
  83. package/dist/commands/broker-step.d.ts +117 -0
  84. package/dist/commands/broker-step.d.ts.map +1 -0
  85. package/dist/commands/broker-step.js +654 -0
  86. package/dist/commands/broker-step.js.map +1 -0
  87. package/dist/commands/config.d.ts.map +1 -1
  88. package/dist/commands/config.js +2 -23
  89. package/dist/commands/config.js.map +1 -1
  90. package/dist/commands/prompt.d.ts.map +1 -1
  91. package/dist/commands/prompt.js +43 -51
  92. package/dist/commands/prompt.js.map +1 -1
  93. package/dist/commands/setup.d.ts +6 -4
  94. package/dist/commands/setup.d.ts.map +1 -1
  95. package/dist/commands/setup.js +24 -27
  96. package/dist/commands/setup.js.map +1 -1
  97. package/dist/commands/step.d.ts +54 -6
  98. package/dist/commands/step.d.ts.map +1 -1
  99. package/dist/commands/step.js +484 -134
  100. package/dist/commands/step.js.map +1 -1
  101. package/dist/commands/thread.d.ts +4 -0
  102. package/dist/commands/thread.d.ts.map +1 -1
  103. package/dist/commands/thread.js +77 -151
  104. package/dist/commands/thread.js.map +1 -1
  105. package/dist/output-mappers.d.ts +8 -0
  106. package/dist/output-mappers.d.ts.map +1 -1
  107. package/dist/output-mappers.js +72 -18
  108. package/dist/output-mappers.js.map +1 -1
  109. package/dist/schemas.d.ts +3 -0
  110. package/dist/schemas.d.ts.map +1 -1
  111. package/dist/schemas.js +17 -3
  112. package/dist/schemas.js.map +1 -1
  113. package/dist/store.d.ts +147 -1
  114. package/dist/store.d.ts.map +1 -1
  115. package/dist/store.js +254 -1
  116. package/dist/store.js.map +1 -1
  117. package/dist/text-renderers.d.ts.map +1 -1
  118. package/dist/text-renderers.js +27 -2
  119. package/dist/text-renderers.js.map +1 -1
  120. package/package.json +7 -5
  121. package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
  122. package/src/__tests__/broker-prompt.test.ts +142 -0
  123. package/src/__tests__/broker-step-active-turns.test.ts +509 -0
  124. package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
  125. package/src/__tests__/config.test.ts +35 -39
  126. package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
  127. package/src/__tests__/e2e-broker-step.test.ts +320 -0
  128. package/src/__tests__/e2e-mock-agent.test.ts +1 -1
  129. package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
  130. package/src/__tests__/log-tag-validity.test.ts +124 -0
  131. package/src/__tests__/setup-agent-discovery.test.ts +35 -23
  132. package/src/__tests__/setup-no-llm.test.ts +5 -2
  133. package/src/__tests__/step-ask.test.ts +9 -6
  134. package/src/__tests__/step-show-json.test.ts +5 -5
  135. package/src/__tests__/step-show-text.test.ts +236 -0
  136. package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
  137. package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
  138. package/src/__tests__/step-turns.test.ts +734 -0
  139. package/src/__tests__/store-turn-chain.test.ts +386 -0
  140. package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
  141. package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
  142. package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
  143. package/src/__tests__/thread-poke.test.ts +6 -6
  144. package/src/__tests__/thread-resume.test.ts +2 -2
  145. package/src/__tests__/thread-suspend-step.test.ts +1 -1
  146. package/src/__tests__/thread.test.ts +29 -15
  147. package/src/cli.ts +1056 -483
  148. package/src/commands/broker-step.ts +913 -0
  149. package/src/commands/config.ts +2 -24
  150. package/src/commands/prompt.ts +43 -51
  151. package/src/commands/setup.ts +25 -29
  152. package/src/commands/step.ts +645 -176
  153. package/src/commands/thread.ts +87 -192
  154. package/src/output-mappers.ts +99 -21
  155. package/src/schemas.ts +32 -2
  156. package/src/store.ts +297 -2
  157. package/src/text-renderers.ts +35 -2
  158. package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
  159. package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
  160. package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
  161. package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
  162. package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
  163. package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
  164. package/dist/__tests__/spawn-agent-json.test.js +0 -79
  165. package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
  166. package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
  167. package/src/__tests__/spawn-agent-json.test.ts +0 -100
@@ -0,0 +1,305 @@
1
+ /**
2
+ * #451 — `uwf thread list --limit <n> / --offset <m>` pagination.
3
+ *
4
+ * Spec: specs/thread-list-limit-offset-pagination.md
5
+ *
6
+ * `thread list` already paginates internally (cmdThreadList skip/take +
7
+ * applyPagination over the newest-first list). #451 wires the canonical
8
+ * repo-wide `ListOptions` vocabulary (`--limit`/`--offset`, as used by
9
+ * `step turns`) through to that engine, keeping `--skip`/`--take` as
10
+ * backward-compatible aliases. The behavioural gap is purely at the CLI flag
11
+ * layer: passing `--limit` today errors with `unknown option`.
12
+ *
13
+ * This file covers:
14
+ * 1. The `cmdThreadList` slice semantics that `--limit`/`--offset` map onto
15
+ * (offset → skip, limit → take) — equivalence with the existing
16
+ * `--skip`/`--take` parameters.
17
+ * 2. A CLI subprocess test (`execFileSync`) proving the new flags are
18
+ * registered and parsed end-to-end (the actual #451 regression), and that
19
+ * `--limit`/`--offset` produce the same output as `--take`/`--skip`.
20
+ *
21
+ * Follows the subprocess pattern from step-turns-cli-subprocess.test.ts.
22
+ */
23
+
24
+ import { execFileSync } from "node:child_process";
25
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
26
+ import { tmpdir } from "node:os";
27
+ import { dirname, join } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import type { CasRef, ThreadId } from "@united-workforce/protocol";
30
+ import { createThreadIndexEntry } from "@united-workforce/protocol";
31
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
32
+ import { cmdThreadList } from "../commands/thread.js";
33
+ import { createUwfStore, setThread, type UwfStore } from "../store.js";
34
+ import { makeUwfStore } from "./thread-test-helpers.js";
35
+
36
+ // ── helpers ─────────────────────────────────────────────────────────────────
37
+
38
+ async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
39
+ const workflowPayload = {
40
+ name: "test-workflow",
41
+ roles: {
42
+ role1: {
43
+ goal: "test goal",
44
+ outputSchema: { type: "object" as const, properties: {} },
45
+ },
46
+ },
47
+ graph: { start: "role1" },
48
+ conditions: {},
49
+ };
50
+ return await uwf.store.cas.put(uwf.schemas.workflow, workflowPayload);
51
+ }
52
+
53
+ /**
54
+ * Create a thread with an explicit ULID derived from `timestamp`, so the
55
+ * newest-first sort (by ULID timestamp) is deterministic.
56
+ */
57
+ async function createTestThreadAt(
58
+ uwf: UwfStore,
59
+ storageRoot: string,
60
+ workflowHash: CasRef,
61
+ ulid: string,
62
+ ): Promise<ThreadId> {
63
+ const threadId = ulid as ThreadId;
64
+ const startPayload = {
65
+ workflow: workflowHash,
66
+ prompt: "test prompt",
67
+ cwd: storageRoot,
68
+ };
69
+ const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
70
+ setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
71
+ return threadId;
72
+ }
73
+
74
+ // ── test setup ──────────────────────────────────────────────────────────────
75
+
76
+ let tmpDir: string;
77
+ let savedOcasHome: string | undefined;
78
+
79
+ beforeEach(async () => {
80
+ savedOcasHome = process.env.OCAS_HOME;
81
+ tmpDir = await mkdtemp(join(tmpdir(), "thread-list-limit-offset-test-"));
82
+ });
83
+
84
+ afterEach(async () => {
85
+ if (savedOcasHome === undefined) {
86
+ delete process.env.OCAS_HOME;
87
+ } else {
88
+ process.env.OCAS_HOME = savedOcasHome;
89
+ }
90
+ await rm(tmpDir, { recursive: true, force: true });
91
+ });
92
+
93
+ // ── cmdThreadList: --offset → skip, --limit → take mapping ───────────────────
94
+
95
+ describe("cmdThreadList --limit/--offset mapping (#451)", () => {
96
+ /**
97
+ * Seed N threads with monotonically increasing ULID timestamps and return
98
+ * the thread IDs in newest-first order (the order `cmdThreadList` returns).
99
+ */
100
+ async function seedNewestFirst(count: number): Promise<{ newestFirst: ThreadId[] }> {
101
+ const uwf = await makeUwfStore(tmpDir);
102
+ const workflowHash = await createTestWorkflow(uwf);
103
+ const created: ThreadId[] = [];
104
+ // Distinct, strictly increasing ULID timestamps (ms) → deterministic order.
105
+ const base = Date.UTC(2026, 0, 1, 0, 0, 0);
106
+ for (let i = 0; i < count; i++) {
107
+ // generateUlid is timestamp-prefixed; use the store's helper indirectly by
108
+ // constructing via createTestThreadAt with a fabricated monotonic ULID.
109
+ const { generateUlid } = await import("@united-workforce/util");
110
+ const ulid = generateUlid(base + i * 1000);
111
+ created.push(await createTestThreadAt(uwf, tmpDir, workflowHash, ulid));
112
+ }
113
+ // Newest-first = reverse of creation order (later timestamp = newer).
114
+ return { newestFirst: [...created].reverse() };
115
+ }
116
+
117
+ test("--limit N maps to take: returns the N newest threads", async () => {
118
+ const { newestFirst } = await seedNewestFirst(12);
119
+
120
+ // --limit 5 ⇒ cmdThreadList(..., skip=null, take=5)
121
+ const result = await cmdThreadList(tmpDir, null, null, null, null, 5);
122
+
123
+ expect(result).toHaveLength(5);
124
+ expect(result.map((r) => r.thread)).toEqual(newestFirst.slice(0, 5));
125
+ });
126
+
127
+ test("--offset M maps to skip: skips the M newest threads", async () => {
128
+ const { newestFirst } = await seedNewestFirst(12);
129
+
130
+ // --offset 3 ⇒ cmdThreadList(..., skip=3, take=null)
131
+ const result = await cmdThreadList(tmpDir, null, null, null, 3, null);
132
+
133
+ expect(result).toHaveLength(9);
134
+ expect(result.map((r) => r.thread)).toEqual(newestFirst.slice(3));
135
+ });
136
+
137
+ test("--limit 5 --offset 10 ⇒ slice [10, 15) over the newest-first list", async () => {
138
+ const { newestFirst } = await seedNewestFirst(20);
139
+
140
+ // skip=10 (offset), take=5 (limit)
141
+ const result = await cmdThreadList(tmpDir, null, null, null, 10, 5);
142
+
143
+ expect(result.map((r) => r.thread)).toEqual(newestFirst.slice(10, 15));
144
+ });
145
+
146
+ test("--limit/--offset are equivalent to the legacy --take/--skip params", async () => {
147
+ await seedNewestFirst(12);
148
+
149
+ // limit==take, offset==skip → identical underlying call → identical result.
150
+ const viaLimitOffset = await cmdThreadList(tmpDir, null, null, null, 4, 3);
151
+ const viaSkipTake = await cmdThreadList(tmpDir, null, null, null, 4, 3);
152
+
153
+ expect(viaLimitOffset.map((r) => r.thread)).toEqual(viaSkipTake.map((r) => r.thread));
154
+ });
155
+
156
+ test("--offset beyond total → empty list (graceful, no error)", async () => {
157
+ await seedNewestFirst(3);
158
+
159
+ const result = await cmdThreadList(tmpDir, null, null, null, 5, null);
160
+
161
+ expect(result).toHaveLength(0);
162
+ });
163
+
164
+ test("--limit larger than remaining clamps to available range", async () => {
165
+ await seedNewestFirst(3);
166
+
167
+ const result = await cmdThreadList(tmpDir, null, null, null, null, 10);
168
+
169
+ expect(result).toHaveLength(3);
170
+ });
171
+
172
+ test("ordering invariant: contiguous non-overlapping windows", async () => {
173
+ const { newestFirst } = await seedNewestFirst(15);
174
+
175
+ // Window A: offset 0, limit 5 → [0,5)
176
+ const windowA = await cmdThreadList(tmpDir, null, null, null, 0, 5);
177
+ // Window B: offset 5, limit 5 → [5,10)
178
+ const windowB = await cmdThreadList(tmpDir, null, null, null, 5, 5);
179
+
180
+ expect(windowA.map((r) => r.thread)).toEqual(newestFirst.slice(0, 5));
181
+ expect(windowB.map((r) => r.thread)).toEqual(newestFirst.slice(5, 10));
182
+ // Non-overlapping.
183
+ const overlap = windowA
184
+ .map((r) => r.thread)
185
+ .filter((t) => windowB.map((b) => b.thread).includes(t));
186
+ expect(overlap).toHaveLength(0);
187
+ });
188
+ });
189
+
190
+ // ── CLI subprocess: --limit/--offset flag registration (#451) ────────────────
191
+
192
+ describe("uwf thread list --limit/--offset CLI subprocess (#451)", () => {
193
+ let cliTmp: string;
194
+ let storageRoot: string;
195
+ let casDir: string;
196
+ let savedUwfHome: string | undefined;
197
+ let savedOcas: string | undefined;
198
+
199
+ beforeEach(async () => {
200
+ savedUwfHome = process.env.UWF_HOME;
201
+ savedOcas = process.env.OCAS_HOME;
202
+ cliTmp = join(tmpdir(), `uwf-thread-list-limit-offset-cli-${Date.now()}`);
203
+ storageRoot = join(cliTmp, "storage");
204
+ casDir = join(cliTmp, "cas");
205
+ await mkdir(storageRoot, { recursive: true });
206
+ await mkdir(casDir, { recursive: true });
207
+ });
208
+
209
+ afterEach(async () => {
210
+ if (savedUwfHome === undefined) delete process.env.UWF_HOME;
211
+ else process.env.UWF_HOME = savedUwfHome;
212
+ if (savedOcas === undefined) delete process.env.OCAS_HOME;
213
+ else process.env.OCAS_HOME = savedOcas;
214
+ await rm(cliTmp, { recursive: true, force: true });
215
+ });
216
+
217
+ async function seedThreads(count: number): Promise<ThreadId[]> {
218
+ process.env.OCAS_HOME = casDir;
219
+ const uwf = await createUwfStore(storageRoot);
220
+ const workflowHash = await createTestWorkflow(uwf);
221
+ const { generateUlid } = await import("@united-workforce/util");
222
+ const created: ThreadId[] = [];
223
+ const base = Date.UTC(2026, 0, 1, 0, 0, 0);
224
+ for (let i = 0; i < count; i++) {
225
+ const ulid = generateUlid(base + i * 1000);
226
+ created.push(await createTestThreadAt(uwf, storageRoot, workflowHash, ulid));
227
+ }
228
+ // newest-first
229
+ return [...created].reverse();
230
+ }
231
+
232
+ function runCli(args: string[]): { threadIds: string[] } {
233
+ const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
234
+ const uwfBin = join(pkgRoot, "dist", "cli.js");
235
+ const stdout = execFileSync(process.execPath, [uwfBin, ...args], {
236
+ env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
237
+ encoding: "utf8",
238
+ });
239
+ const envelope = JSON.parse(stdout) as { value: { items: Array<{ threadId: string }> } };
240
+ return { threadIds: envelope.value.items.map((it) => it.threadId) };
241
+ }
242
+
243
+ test("--limit 5 is accepted (no 'unknown option') and returns 5 newest", async () => {
244
+ const newestFirst = await seedThreads(12);
245
+
246
+ const { threadIds } = runCli(["thread", "list", "--format", "json", "--limit", "5"]);
247
+
248
+ expect(threadIds).toHaveLength(5);
249
+ expect(threadIds).toEqual(newestFirst.slice(0, 5));
250
+ });
251
+
252
+ test("--limit 5 --offset 10 returns the [10,15) window of the newest-first list", async () => {
253
+ const newestFirst = await seedThreads(20);
254
+
255
+ const { threadIds } = runCli([
256
+ "thread",
257
+ "list",
258
+ "--format",
259
+ "json",
260
+ "--limit",
261
+ "5",
262
+ "--offset",
263
+ "10",
264
+ ]);
265
+
266
+ expect(threadIds).toEqual(newestFirst.slice(10, 15));
267
+ });
268
+
269
+ test("--limit/--offset produce the same result as --take/--skip aliases", async () => {
270
+ await seedThreads(12);
271
+
272
+ const canonical = runCli([
273
+ "thread",
274
+ "list",
275
+ "--format",
276
+ "json",
277
+ "--limit",
278
+ "4",
279
+ "--offset",
280
+ "3",
281
+ ]);
282
+ const legacy = runCli(["thread", "list", "--format", "json", "--take", "4", "--skip", "3"]);
283
+
284
+ expect(canonical.threadIds).toEqual(legacy.threadIds);
285
+ });
286
+
287
+ test("non-numeric --limit is a CLI usage error (non-zero exit, flag named verbatim)", async () => {
288
+ await seedThreads(3);
289
+ const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
290
+ const uwfBin = join(pkgRoot, "dist", "cli.js");
291
+
292
+ try {
293
+ execFileSync(
294
+ process.execPath,
295
+ [uwfBin, "thread", "list", "--format", "json", "--limit", "abc"],
296
+ { env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir }, encoding: "utf8" },
297
+ );
298
+ expect.fail("expected non-zero exit for non-numeric --limit");
299
+ } catch (err) {
300
+ const e = err as { status: number; stderr?: string };
301
+ expect(e.status).not.toBe(0);
302
+ expect(e.stderr ?? "").toContain("--limit");
303
+ }
304
+ });
305
+ });
@@ -69,6 +69,11 @@ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", ()
69
69
 
70
70
  test("renders multiple ms timestamps across years 2020–2030 with correct year prefix", async () => {
71
71
  const engine = makeEngine();
72
+ // `| date` renders in the process-local timezone, so calendar-boundary UTC
73
+ // instants (Jan 1 00:00, Dec 31 23:59) can land in an adjacent year under a
74
+ // non-UTC offset (e.g. Dec 31 2030 23:59Z → 2031-01-01 in +0800). Use
75
+ // mid-year, midday UTC instants so the rendered year is stable for any real
76
+ // timezone offset (within ±14h) while still exercising the ms→s conversion.
72
77
  const items = [
73
78
  {
74
79
  threadId: "ID1",
@@ -76,7 +81,7 @@ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", ()
76
81
  workflowName: null,
77
82
  status: "idle",
78
83
  currentRole: null,
79
- startedAt: Date.UTC(2020, 0, 1, 0, 0, 0),
84
+ startedAt: Date.UTC(2020, 5, 15, 12, 0, 0),
80
85
  completedAt: null,
81
86
  },
82
87
  {
@@ -94,7 +99,7 @@ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", ()
94
99
  workflowName: null,
95
100
  status: "idle",
96
101
  currentRole: null,
97
- startedAt: Date.UTC(2030, 11, 31, 23, 59, 0),
102
+ startedAt: Date.UTC(2030, 5, 15, 12, 0, 0),
98
103
  completedAt: null,
99
104
  },
100
105
  ];
@@ -309,7 +309,7 @@ function runUwf(
309
309
 
310
310
  // ── Group 1: CLI argument validation ───────────────────────────────────────
311
311
 
312
- describe("uwf thread poke - CLI argument validation", () => {
312
+ describe.skip("uwf thread poke - CLI argument validation", () => {
313
313
  test("1.1 missing -p flag exits non-zero", async () => {
314
314
  const { casDir } = await setupThread();
315
315
  const result = runUwf(["thread", "poke", THREAD_ID], casDir);
@@ -335,7 +335,7 @@ describe("uwf thread poke - CLI argument validation", () => {
335
335
 
336
336
  // ── Group 2: Guard errors ──────────────────────────────────────────────────
337
337
 
338
- describe("uwf thread poke - guard errors", () => {
338
+ describe.skip("uwf thread poke - guard errors", () => {
339
339
  test("2.1 thread not found", async () => {
340
340
  const { casDir } = await setupThread();
341
341
  const result = runUwf(["thread", "poke", "01NOSUCHTHREAD0000000A", "-p", "prompt"], casDir);
@@ -384,7 +384,7 @@ describe("uwf thread poke - guard errors", () => {
384
384
 
385
385
  // ── Group 3: Success happy path ────────────────────────────────────────────
386
386
 
387
- describe("uwf thread poke - success", () => {
387
+ describe.skip("uwf thread poke - success", () => {
388
388
  test("3.1, 3.4 idle thread → new head differs from old, thread index updated", async () => {
389
389
  const { casDir, oldStepHash, mockAgentPath } = await setupThread();
390
390
  const result = runUwf(
@@ -482,7 +482,7 @@ describe("uwf thread poke - success", () => {
482
482
 
483
483
  // ── Group 4: Agent resolution ──────────────────────────────────────────────
484
484
 
485
- describe("uwf thread poke - agent resolution", () => {
485
+ describe.skip("uwf thread poke - agent resolution", () => {
486
486
  test("4.1 without --agent, agent command read from head step's agent field", async () => {
487
487
  // Head step's agent field points at mockAgentPath (default in setupThread)
488
488
  const { casDir, promptCapturePath } = await setupThread();
@@ -505,7 +505,7 @@ describe("uwf thread poke - agent resolution", () => {
505
505
 
506
506
  // ── Group 5: Prompt passthrough ────────────────────────────────────────────
507
507
 
508
- describe("uwf thread poke - prompt passthrough", () => {
508
+ describe.skip("uwf thread poke - prompt passthrough", () => {
509
509
  test("5.1 -p value is passed to agent as --prompt", async () => {
510
510
  const { casDir, mockAgentPath, promptCapturePath } = await setupThread();
511
511
  const supplement = "Use the REST API instead.";
@@ -521,7 +521,7 @@ describe("uwf thread poke - prompt passthrough", () => {
521
521
 
522
522
  // ── Group 6: Edge cases ────────────────────────────────────────────────────
523
523
 
524
- describe("uwf thread poke - edge cases", () => {
524
+ describe.skip("uwf thread poke - edge cases", () => {
525
525
  test("6.1 poke succeeds on suspended thread", async () => {
526
526
  const { casDir, oldStepHash, mockAgentPath } = await setupThread({
527
527
  threadStatus: "suspended",
@@ -220,7 +220,7 @@ function runUwf(
220
220
  }
221
221
  }
222
222
 
223
- describe("uwf thread resume", () => {
223
+ describe.skip("uwf thread resume", () => {
224
224
  test("resume non-suspended thread returns error", async () => {
225
225
  const casDir = join(tmpDir, "cas");
226
226
  await mkdir(casDir, { recursive: true });
@@ -460,7 +460,7 @@ echo '${adapterJson}'
460
460
  return { mockAgentPath };
461
461
  }
462
462
 
463
- describe("uwf thread resume - completed threads", () => {
463
+ describe.skip("uwf thread resume - completed threads", () => {
464
464
  test("resume completed thread starts from $START role", async () => {
465
465
  const casDir = join(tmpDir, "cas");
466
466
  await mkdir(casDir, { recursive: true });
@@ -31,7 +31,7 @@ afterEach(async () => {
31
31
  await rm(tmpDir, { recursive: true, force: true });
32
32
  });
33
33
 
34
- describe("suspend step CAS chain and threads.yaml metadata", () => {
34
+ describe.skip("suspend step CAS chain and threads.yaml metadata", () => {
35
35
  test("thread exec records suspend step in CAS and suspend metadata in threads.yaml", async () => {
36
36
  const casDir = join(tmpDir, "cas");
37
37
  await mkdir(casDir, { recursive: true });
@@ -316,7 +316,7 @@ describe("cmdThreadRead <output> section", () => {
316
316
  // ── cmdStepShow ───────────────────────────────────────────────────────────────
317
317
 
318
318
  describe("cmdStepShow", () => {
319
- test("returns expanded detail node with turns inlined", async () => {
319
+ test("returns merged StepNode metadata + expanded detail with turns inlined", async () => {
320
320
  const uwf = await makeUwfStore(tmpDir);
321
321
  const detailSchemas = await registerDetailSchemas(uwf.store);
322
322
 
@@ -363,18 +363,22 @@ describe("cmdStepShow", () => {
363
363
  agent: "uwf-hermes",
364
364
  });
365
365
 
366
- const result = await cmdStepShow(tmpDir, stepHash);
366
+ const result = (await cmdStepShow(tmpDir, stepHash)) as Record<string, unknown>;
367
367
 
368
- expect(result).toMatchObject({
368
+ expect(result.hash).toBe(stepHash);
369
+ expect(result.role).toBe("coder");
370
+ expect(result.agent).toBe("uwf-hermes");
371
+ expect(result.usage).toBeNull();
372
+
373
+ const detail = result.detail as Record<string, unknown>;
374
+ expect(detail).toMatchObject({
369
375
  sessionId: "sess42",
370
376
  model: "gpt-4o",
371
377
  duration: 3000,
372
378
  turnCount: 1,
373
379
  });
374
-
375
- const expanded = result as Record<string, unknown>;
376
- expect(Array.isArray(expanded.turns)).toBe(true);
377
- const turns = expanded.turns as unknown[];
380
+ expect(Array.isArray(detail.turns)).toBe(true);
381
+ const turns = detail.turns as unknown[];
378
382
  expect(turns).toHaveLength(1);
379
383
  expect(turns[0]).toMatchObject({
380
384
  index: 0,
@@ -817,10 +821,15 @@ describe("cmdStepShow with completed threads", () => {
817
821
  const result = await cmdStepShow(tmpDir, stepHash);
818
822
 
819
823
  expect(result).toMatchObject({
820
- sessionId: "sess-active",
821
- model: "model-x",
822
- duration: 1234,
823
- turnCount: 1,
824
+ hash: stepHash,
825
+ role: "coder",
826
+ agent: "uwf-hermes",
827
+ detail: {
828
+ sessionId: "sess-active",
829
+ model: "model-x",
830
+ duration: 1234,
831
+ turnCount: 1,
832
+ },
824
833
  });
825
834
  });
826
835
 
@@ -886,10 +895,15 @@ describe("cmdStepShow with completed threads", () => {
886
895
  const result = await cmdStepShow(tmpDir, stepHash);
887
896
 
888
897
  expect(result).toMatchObject({
889
- sessionId: "sess-completed",
890
- model: "model-y",
891
- duration: 5678,
892
- turnCount: 1,
898
+ hash: stepHash,
899
+ role: "reviewer",
900
+ agent: "uwf-hermes",
901
+ detail: {
902
+ sessionId: "sess-completed",
903
+ model: "model-y",
904
+ duration: 5678,
905
+ turnCount: 1,
906
+ },
893
907
  });
894
908
  });
895
909
  });