@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
@@ -0,0 +1,545 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { putSchema } from "@ocas/core";
7
+ import { openStore } from "@ocas/fs";
8
+ import type {
9
+ CasRef,
10
+ StepNodePayload,
11
+ ThreadId,
12
+ ThreadIndexEntry,
13
+ } from "@united-workforce/protocol";
14
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
15
+ import { registerUwfSchemas } from "../schemas.js";
16
+ import { seedThreads } from "./thread-test-helpers.js";
17
+
18
+ const OUTPUT_SCHEMA = {
19
+ type: "object" as const,
20
+ properties: {
21
+ $status: { type: "string" as const },
22
+ note: { type: "string" as const },
23
+ },
24
+ required: ["$status"],
25
+ additionalProperties: false,
26
+ };
27
+
28
+ const THREAD_ID = "01POKESTEPTEST00000000" as ThreadId;
29
+
30
+ let tmpDir: string;
31
+
32
+ beforeEach(async () => {
33
+ tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-poke-test-"));
34
+ });
35
+
36
+ afterEach(async () => {
37
+ await rm(tmpDir, { recursive: true, force: true });
38
+ });
39
+
40
+ type SetupResult = {
41
+ casDir: string;
42
+ oldStepHash: CasRef;
43
+ oldStepPrev: CasRef | null;
44
+ oldStepCompletedAtMs: number;
45
+ startHash: CasRef;
46
+ workflowHash: CasRef;
47
+ mockAgentPath: string;
48
+ failingAgentPath: string;
49
+ promptCapturePath: string;
50
+ envCapturePath: string;
51
+ };
52
+
53
+ type SetupOpts = {
54
+ threadStatus: ThreadIndexEntry["status"];
55
+ multipleSteps: boolean;
56
+ newCompletedAtMs: number;
57
+ newStatus: string;
58
+ // The agent name to record in the head StepNode.agent field. Defaults to mockAgentPath.
59
+ stepAgentNameOverride: string | null;
60
+ // Whether to seed an actual head StepNode (false → only StartNode is the head).
61
+ withHeadStep: boolean;
62
+ };
63
+
64
+ async function setupThread(opts: Partial<SetupOpts> = {}): Promise<SetupResult> {
65
+ const cfg: SetupOpts = {
66
+ threadStatus: opts.threadStatus ?? "idle",
67
+ multipleSteps: opts.multipleSteps ?? false,
68
+ newCompletedAtMs: opts.newCompletedAtMs ?? 1716600005000,
69
+ newStatus: opts.newStatus ?? "ok",
70
+ stepAgentNameOverride: opts.stepAgentNameOverride ?? null,
71
+ withHeadStep: opts.withHeadStep ?? true,
72
+ };
73
+
74
+ const casDir = join(tmpDir, "cas");
75
+ await mkdir(casDir, { recursive: true });
76
+
77
+ const store = await openStore(casDir);
78
+ const schemas = await registerUwfSchemas(store);
79
+ const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
80
+
81
+ const workflowHash = await store.cas.put(schemas.workflow, {
82
+ name: "test-poke",
83
+ description: "poke command integration test",
84
+ roles: {
85
+ worker: {
86
+ description: "Worker role",
87
+ goal: "Work",
88
+ capabilities: [],
89
+ procedure: "work",
90
+ output: "result",
91
+ frontmatter: outputSchemaHash,
92
+ },
93
+ reviewer: {
94
+ description: "Reviewer role",
95
+ goal: "Review",
96
+ capabilities: [],
97
+ procedure: "review",
98
+ output: "result",
99
+ frontmatter: outputSchemaHash,
100
+ },
101
+ },
102
+ graph: {
103
+ $START: {
104
+ new: { role: "worker", prompt: "Start work", location: null },
105
+ resume: { role: "worker", prompt: "Resume the work", location: null },
106
+ },
107
+ worker: {
108
+ ok: { role: "reviewer", prompt: "Review the work", location: null },
109
+ },
110
+ reviewer: { done: { role: "$END", prompt: "Done", location: null } },
111
+ },
112
+ });
113
+
114
+ const startHash = await store.cas.put(schemas.startNode, {
115
+ workflow: workflowHash,
116
+ prompt: "Test poke task",
117
+ cwd: tmpDir,
118
+ });
119
+
120
+ process.env.OCAS_HOME = casDir;
121
+
122
+ // Paths for mock agent and capture files (set early so we can use mockAgentPath as the recorded agent name)
123
+ const promptCapturePath = join(tmpDir, "captured-prompt.txt");
124
+ const envCapturePath = join(tmpDir, "captured-env.txt");
125
+ const mockAgentPath = join(tmpDir, "mock-agent.sh");
126
+ const failingAgentPath = join(tmpDir, "failing-agent.sh");
127
+
128
+ // Build head StepNode chain
129
+ let oldStepPrev: CasRef | null = null;
130
+ if (cfg.multipleSteps) {
131
+ // First step: prev=null
132
+ const firstOutputHash = await store.cas.put(outputSchemaHash, { $status: "ok" });
133
+ const firstDetailHash = await store.cas.put(schemas.text, "first detail");
134
+ const firstStepHash = await store.cas.put(schemas.stepNode, {
135
+ start: startHash,
136
+ prev: null,
137
+ role: "worker",
138
+ output: firstOutputHash,
139
+ detail: firstDetailHash,
140
+ agent: cfg.stepAgentNameOverride ?? mockAgentPath,
141
+ edgePrompt: "Start work",
142
+ startedAtMs: 1716600000000,
143
+ completedAtMs: 1716600001000,
144
+ cwd: tmpDir,
145
+ assembledPrompt: null,
146
+ usage: null,
147
+ });
148
+ oldStepPrev = firstStepHash;
149
+ }
150
+
151
+ let oldStepHash: CasRef = startHash;
152
+ const oldStepCompletedAtMs = 1716600002000;
153
+ if (cfg.withHeadStep) {
154
+ const outputHash = await store.cas.put(outputSchemaHash, { $status: "ok" });
155
+ const detailHash = await store.cas.put(schemas.text, "head step detail");
156
+ oldStepHash = await store.cas.put(schemas.stepNode, {
157
+ start: startHash,
158
+ prev: oldStepPrev,
159
+ role: "worker",
160
+ output: outputHash,
161
+ detail: detailHash,
162
+ agent: cfg.stepAgentNameOverride ?? mockAgentPath,
163
+ edgePrompt: "Start work",
164
+ startedAtMs: 1716600001500,
165
+ completedAtMs: oldStepCompletedAtMs,
166
+ cwd: tmpDir,
167
+ assembledPrompt: null,
168
+ usage: null,
169
+ });
170
+ }
171
+
172
+ // Seed thread index entry. For "running" we let the test create the marker separately.
173
+ await seedThreads(tmpDir, {
174
+ [THREAD_ID]: {
175
+ head: oldStepHash,
176
+ status: cfg.threadStatus,
177
+ suspendedRole: cfg.threadStatus === "suspended" ? "worker" : null,
178
+ suspendMessage: cfg.threadStatus === "suspended" ? "Please clarify" : null,
179
+ completedAt:
180
+ cfg.threadStatus === "end" || cfg.threadStatus === "cancelled"
181
+ ? oldStepCompletedAtMs
182
+ : null,
183
+ },
184
+ });
185
+
186
+ // Mock agent always emits a stepNode keyed off the current thread head (which we
187
+ // observe through OCAS_HOME). The script writes prompt/env captures and then prints
188
+ // an adapter JSON that references a pre-built stepHash.
189
+ // We pre-build the agent's stepHash with prev=oldStepHash (normal append behaviour).
190
+ const newOutputHash = await store.cas.put(outputSchemaHash, {
191
+ $status: cfg.newStatus,
192
+ note: "poked output",
193
+ });
194
+ const newDetailHash = await store.cas.put(schemas.text, "poked detail");
195
+ const agentStepHash = await store.cas.put(schemas.stepNode, {
196
+ start: startHash,
197
+ prev: cfg.withHeadStep ? oldStepHash : null,
198
+ role: "worker",
199
+ output: newOutputHash,
200
+ detail: newDetailHash,
201
+ agent: "mock-agent-output",
202
+ edgePrompt: "poke prompt placeholder",
203
+ startedAtMs: cfg.newCompletedAtMs - 100,
204
+ completedAtMs: cfg.newCompletedAtMs,
205
+ cwd: tmpDir,
206
+ assembledPrompt: null,
207
+ usage: null,
208
+ });
209
+
210
+ const adapterJson = JSON.stringify({
211
+ stepHash: agentStepHash,
212
+ detailHash: newDetailHash,
213
+ role: "worker",
214
+ frontmatter: { $status: cfg.newStatus, note: "poked output" },
215
+ body: "",
216
+ startedAtMs: cfg.newCompletedAtMs - 100,
217
+ completedAtMs: cfg.newCompletedAtMs,
218
+ usage: null,
219
+ });
220
+
221
+ await writeFile(
222
+ mockAgentPath,
223
+ `#!/bin/sh
224
+ prompt=""
225
+ while [ $# -gt 0 ]; do
226
+ if [ "$1" = "--prompt" ]; then
227
+ prompt="$2"
228
+ shift 2
229
+ else
230
+ shift
231
+ fi
232
+ done
233
+ printf '%s' "$prompt" > '${promptCapturePath}'
234
+ printf 'OCAS_HOME=%s\\n' "$OCAS_HOME" > '${envCapturePath}'
235
+ echo '${adapterJson}'
236
+ `,
237
+ { mode: 0o755 },
238
+ );
239
+
240
+ await writeFile(
241
+ failingAgentPath,
242
+ `#!/bin/sh
243
+ echo "boom" >&2
244
+ exit 7
245
+ `,
246
+ { mode: 0o755 },
247
+ );
248
+
249
+ const configPath = join(tmpDir, "config.yaml");
250
+ await writeFile(
251
+ configPath,
252
+ `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
253
+ );
254
+
255
+ return {
256
+ casDir,
257
+ oldStepHash,
258
+ oldStepPrev,
259
+ oldStepCompletedAtMs,
260
+ startHash,
261
+ workflowHash,
262
+ mockAgentPath,
263
+ failingAgentPath,
264
+ promptCapturePath,
265
+ envCapturePath,
266
+ };
267
+ }
268
+
269
+ function runUwf(
270
+ args: string[],
271
+ casDir: string,
272
+ ): { stdout: string; stderr: string; status: number } {
273
+ const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
274
+ try {
275
+ const stdout = execFileSync(process.execPath, [cliPath, ...args], {
276
+ encoding: "utf8",
277
+ stdio: ["ignore", "pipe", "pipe"],
278
+ env: {
279
+ ...process.env,
280
+ UWF_HOME: tmpDir,
281
+ OCAS_HOME: casDir,
282
+ },
283
+ cwd: tmpDir,
284
+ timeout: 30000,
285
+ });
286
+ return { stdout, stderr: "", status: 0 };
287
+ } catch (error) {
288
+ const err = error as NodeJS.ErrnoException & {
289
+ stdout?: string | Buffer;
290
+ stderr?: string | Buffer;
291
+ status?: number;
292
+ };
293
+ return {
294
+ stdout: typeof err.stdout === "string" ? err.stdout : (err.stdout?.toString("utf8") ?? ""),
295
+ stderr: typeof err.stderr === "string" ? err.stderr : (err.stderr?.toString("utf8") ?? ""),
296
+ status: err.status ?? 1,
297
+ };
298
+ }
299
+ }
300
+
301
+ // ── Group 1: CLI argument validation ───────────────────────────────────────
302
+
303
+ describe("uwf thread poke - CLI argument validation", () => {
304
+ test("1.1 missing -p flag exits non-zero", async () => {
305
+ const { casDir } = await setupThread();
306
+ const result = runUwf(["thread", "poke", THREAD_ID], casDir);
307
+ expect(result.status).not.toBe(0);
308
+ expect(result.stderr.toLowerCase()).toMatch(/required|missing|prompt/);
309
+ });
310
+
311
+ test("1.2 -p without --agent succeeds", async () => {
312
+ const { casDir } = await setupThread();
313
+ const result = runUwf(["thread", "poke", THREAD_ID, "-p", "do it again"], casDir);
314
+ expect(result.status).toBe(0);
315
+ });
316
+
317
+ test("1.3 -p with --agent succeeds", async () => {
318
+ const { casDir, mockAgentPath } = await setupThread();
319
+ const result = runUwf(
320
+ ["thread", "poke", THREAD_ID, "-p", "do it again", "--agent", mockAgentPath],
321
+ casDir,
322
+ );
323
+ expect(result.status).toBe(0);
324
+ });
325
+ });
326
+
327
+ // ── Group 2: Guard errors ──────────────────────────────────────────────────
328
+
329
+ describe("uwf thread poke - guard errors", () => {
330
+ test("2.1 thread not found", async () => {
331
+ const { casDir } = await setupThread();
332
+ const result = runUwf(["thread", "poke", "01NOSUCHTHREAD0000000A", "-p", "prompt"], casDir);
333
+ expect(result.status).not.toBe(0);
334
+ expect(result.stderr.toLowerCase()).toMatch(/not found|not active/);
335
+ });
336
+
337
+ test("2.2 thread running rejects poke", async () => {
338
+ const { casDir, workflowHash } = await setupThread();
339
+ // Create background marker to simulate running
340
+ const { createMarker, getProcessStartTime } = await import("../background/index.js");
341
+ await createMarker(tmpDir, {
342
+ thread: THREAD_ID,
343
+ workflow: workflowHash,
344
+ pid: process.pid,
345
+ startedAt: Date.now(),
346
+ processStartTime: getProcessStartTime(process.pid),
347
+ });
348
+
349
+ const result = runUwf(["thread", "poke", THREAD_ID, "-p", "prompt"], casDir);
350
+ expect(result.status).not.toBe(0);
351
+ expect(result.stderr.toLowerCase()).toContain("already executing");
352
+ });
353
+
354
+ test("2.3 completed thread rejects poke", async () => {
355
+ const { casDir } = await setupThread({ threadStatus: "end" });
356
+ const result = runUwf(["thread", "poke", THREAD_ID, "-p", "prompt"], casDir);
357
+ expect(result.status).not.toBe(0);
358
+ expect(result.stderr.toLowerCase()).toMatch(/cannot be poked|end/);
359
+ });
360
+
361
+ test("2.4 cancelled thread rejects poke", async () => {
362
+ const { casDir } = await setupThread({ threadStatus: "cancelled" });
363
+ const result = runUwf(["thread", "poke", THREAD_ID, "-p", "prompt"], casDir);
364
+ expect(result.status).not.toBe(0);
365
+ expect(result.stderr.toLowerCase()).toMatch(/cannot be poked|cancelled/);
366
+ });
367
+
368
+ test("2.5 thread head is StartNode (no StepNode) rejects poke", async () => {
369
+ const { casDir } = await setupThread({ withHeadStep: false });
370
+ const result = runUwf(["thread", "poke", THREAD_ID, "-p", "prompt"], casDir);
371
+ expect(result.status).not.toBe(0);
372
+ expect(result.stderr.toLowerCase()).toMatch(/no step|cannot be poked/);
373
+ });
374
+ });
375
+
376
+ // ── Group 3: Success happy path ────────────────────────────────────────────
377
+
378
+ describe("uwf thread poke - success", () => {
379
+ test("3.1, 3.4 idle thread → new head differs from old, thread index updated", async () => {
380
+ const { casDir, oldStepHash, mockAgentPath } = await setupThread();
381
+ const result = runUwf(
382
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
383
+ casDir,
384
+ );
385
+ expect(result.status).toBe(0);
386
+ const cliOutput = JSON.parse(result.stdout.trim());
387
+ expect(cliOutput.head).not.toBe(oldStepHash);
388
+
389
+ const { createUwfStore, getThread } = await import("../store.js");
390
+ const uwf = await createUwfStore(tmpDir);
391
+ const entry = getThread(uwf.varStore, THREAD_ID);
392
+ expect(entry?.head).toBe(cliOutput.head);
393
+ });
394
+
395
+ test("3.2 new step's prev equals old head's prev (replace, not append)", async () => {
396
+ const { casDir, oldStepPrev, mockAgentPath } = await setupThread({ multipleSteps: true });
397
+ const result = runUwf(
398
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
399
+ casDir,
400
+ );
401
+ expect(result.status).toBe(0);
402
+ const cliOutput = JSON.parse(result.stdout.trim());
403
+
404
+ const { createUwfStore } = await import("../store.js");
405
+ const uwf = await createUwfStore(tmpDir);
406
+ const node = uwf.store.cas.get(cliOutput.head as CasRef);
407
+ expect(node).not.toBeNull();
408
+ expect(node?.type).toBe(uwf.schemas.stepNode);
409
+ const payload = node?.payload as StepNodePayload;
410
+ expect(payload.prev).toBe(oldStepPrev);
411
+ });
412
+
413
+ test("3.2b new step's prev is null when old head was the first step", async () => {
414
+ // multipleSteps:false means oldHead.prev = null
415
+ const { casDir, mockAgentPath } = await setupThread({ multipleSteps: false });
416
+ const result = runUwf(
417
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
418
+ casDir,
419
+ );
420
+ expect(result.status).toBe(0);
421
+ const cliOutput = JSON.parse(result.stdout.trim());
422
+
423
+ const { createUwfStore } = await import("../store.js");
424
+ const uwf = await createUwfStore(tmpDir);
425
+ const node = uwf.store.cas.get(cliOutput.head as CasRef);
426
+ const payload = node?.payload as StepNodePayload;
427
+ expect(payload.prev).toBeNull();
428
+ });
429
+
430
+ test("3.3 new step's completedAtMs is later than old", async () => {
431
+ const { casDir, oldStepCompletedAtMs, mockAgentPath } = await setupThread();
432
+ const result = runUwf(
433
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
434
+ casDir,
435
+ );
436
+ expect(result.status).toBe(0);
437
+ const cliOutput = JSON.parse(result.stdout.trim());
438
+
439
+ const { createUwfStore } = await import("../store.js");
440
+ const uwf = await createUwfStore(tmpDir);
441
+ const node = uwf.store.cas.get(cliOutput.head as CasRef);
442
+ const payload = node?.payload as StepNodePayload;
443
+ expect(payload.completedAtMs).toBeGreaterThan(oldStepCompletedAtMs);
444
+ });
445
+
446
+ test("3.5 status remains idle after poke (no completion/suspend)", async () => {
447
+ const { casDir, mockAgentPath } = await setupThread();
448
+ const result = runUwf(
449
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
450
+ casDir,
451
+ );
452
+ expect(result.status).toBe(0);
453
+ const cliOutput = JSON.parse(result.stdout.trim());
454
+ expect(cliOutput.status).toBe("idle");
455
+ expect(cliOutput.done).toBe(false);
456
+ expect(cliOutput.suspendedRole).toBeNull();
457
+ expect(cliOutput.suspendMessage).toBeNull();
458
+ });
459
+
460
+ test("3.6 currentRole unchanged after poke (no moderator re-route)", async () => {
461
+ // Before poke: idle thread with worker step having $status=ok → moderator would route to reviewer.
462
+ // After poke (mock returns same $status=ok), moderator routing remains the same.
463
+ const { casDir, mockAgentPath } = await setupThread();
464
+ const result = runUwf(
465
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
466
+ casDir,
467
+ );
468
+ expect(result.status).toBe(0);
469
+ const cliOutput = JSON.parse(result.stdout.trim());
470
+ expect(cliOutput.currentRole).toBe("reviewer");
471
+ });
472
+ });
473
+
474
+ // ── Group 4: Agent resolution ──────────────────────────────────────────────
475
+
476
+ describe("uwf thread poke - agent resolution", () => {
477
+ test("4.1 without --agent, agent command read from head step's agent field", async () => {
478
+ // Head step's agent field points at mockAgentPath (default in setupThread)
479
+ const { casDir, promptCapturePath } = await setupThread();
480
+ const result = runUwf(["thread", "poke", THREAD_ID, "-p", "redo"], casDir);
481
+ expect(result.status).toBe(0);
482
+ const captured = await readFile(promptCapturePath, "utf8");
483
+ expect(captured).toBe("redo");
484
+ });
485
+
486
+ test("4.2 with --agent, explicit override is used", async () => {
487
+ // Head step records "uwf-mock" (which is not a real binary). Override with mockAgentPath.
488
+ const { casDir, mockAgentPath } = await setupThread({ stepAgentNameOverride: "uwf-mock" });
489
+ const result = runUwf(
490
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
491
+ casDir,
492
+ );
493
+ expect(result.status).toBe(0);
494
+ });
495
+ });
496
+
497
+ // ── Group 5: Prompt passthrough ────────────────────────────────────────────
498
+
499
+ describe("uwf thread poke - prompt passthrough", () => {
500
+ test("5.1 -p value is passed to agent as --prompt", async () => {
501
+ const { casDir, mockAgentPath, promptCapturePath } = await setupThread();
502
+ const supplement = "Use the REST API instead.";
503
+ const result = runUwf(
504
+ ["thread", "poke", THREAD_ID, "-p", supplement, "--agent", mockAgentPath],
505
+ casDir,
506
+ );
507
+ expect(result.status).toBe(0);
508
+ const captured = await readFile(promptCapturePath, "utf8");
509
+ expect(captured).toBe(supplement);
510
+ });
511
+ });
512
+
513
+ // ── Group 6: Edge cases ────────────────────────────────────────────────────
514
+
515
+ describe("uwf thread poke - edge cases", () => {
516
+ test("6.1 poke succeeds on suspended thread", async () => {
517
+ const { casDir, oldStepHash, mockAgentPath } = await setupThread({
518
+ threadStatus: "suspended",
519
+ });
520
+ const result = runUwf(
521
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", mockAgentPath],
522
+ casDir,
523
+ );
524
+ expect(result.status).toBe(0);
525
+ const cliOutput = JSON.parse(result.stdout.trim());
526
+ expect(cliOutput.head).not.toBe(oldStepHash);
527
+ expect(cliOutput.status).toBe("idle");
528
+ expect(cliOutput.suspendedRole).toBeNull();
529
+ expect(cliOutput.suspendMessage).toBeNull();
530
+ });
531
+
532
+ test("6.2 agent failure leaves thread head unchanged", async () => {
533
+ const { casDir, oldStepHash, failingAgentPath } = await setupThread();
534
+ const result = runUwf(
535
+ ["thread", "poke", THREAD_ID, "-p", "redo", "--agent", failingAgentPath],
536
+ casDir,
537
+ );
538
+ expect(result.status).not.toBe(0);
539
+
540
+ const { createUwfStore, getThread } = await import("../store.js");
541
+ const uwf = await createUwfStore(tmpDir);
542
+ const entry = getThread(uwf.varStore, THREAD_ID);
543
+ expect(entry?.head).toBe(oldStepHash);
544
+ });
545
+ });
@@ -75,11 +75,6 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
75
75
  resume: { role: "worker", prompt: "Resume the work", location: null },
76
76
  },
77
77
  worker: {
78
- needs_input: {
79
- role: "$SUSPEND",
80
- prompt: "Please clarify: {{{question}}}",
81
- location: null,
82
- },
83
78
  ok: { role: "reviewer", prompt: "Review the work", location: null },
84
79
  },
85
80
  reviewer: { done: { role: "$END", prompt: "Done", location: null } },
@@ -95,9 +90,9 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
95
90
  process.env.OCAS_HOME = casDir;
96
91
  await seedThreads(tmpDir, { [THREAD_ID]: startHash });
97
92
 
98
- const outputHash = await store.cas.put(outputSchemaHash, {
99
- $status: "needs_input",
100
- question: "Which API?",
93
+ const outputHash = await store.cas.put(schemas.suspendOutput, {
94
+ $status: "$SUSPEND",
95
+ reason: SUSPEND_MESSAGE,
101
96
  });
102
97
  const detailHash = await store.cas.put(schemas.text, "mock detail");
103
98
 
@@ -132,14 +127,15 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
132
127
  const mockAgentPath = join(tmpDir, "mock-agent.sh");
133
128
 
134
129
  const frontmatter =
135
- mode === "suspend" ? { $status: "needs_input", question: "Which API?" } : { $status: "ok" };
130
+ mode === "suspend" ? { $status: "$SUSPEND", reason: SUSPEND_MESSAGE } : { $status: "ok" };
131
+ const frontmatterSchema = mode === "suspend" ? schemas.suspendOutput : outputSchemaHash;
136
132
 
137
133
  const adapterJson = JSON.stringify({
138
134
  stepHash: await store.cas.put(schemas.stepNode, {
139
135
  start: startHash,
140
136
  prev: stepHash,
141
137
  role: "worker",
142
- output: await store.cas.put(outputSchemaHash, frontmatter),
138
+ output: await store.cas.put(frontmatterSchema, frontmatter),
143
139
  detail: detailHash,
144
140
  agent: "uwf-mock",
145
141
  edgePrompt: "resume prompt placeholder",
@@ -177,7 +173,7 @@ echo '${adapterJson}'
177
173
  const configPath = join(tmpDir, "config.yaml");
178
174
  await writeFile(
179
175
  configPath,
180
- `defaultAgent: uwf-hermes\ndefaultModel: test-model\nagentOverrides: null\nagents: {}\nproviders: {}\nmodels: {}\n`,
176
+ `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
181
177
  );
182
178
 
183
179
  return { casDir, mockAgentPath, promptCapturePath };
@@ -338,7 +334,7 @@ describe("uwf thread resume", () => {
338
334
  }
339
335
  });
340
336
 
341
- test("multiple suspend/resume cycles", async () => {
337
+ test("multiple suspend/resume cycles", { timeout: 15_000 }, async () => {
342
338
  const originalCasDir = process.env.OCAS_HOME;
343
339
  const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("suspend");
344
340
  process.env.OCAS_HOME = casDir;
@@ -537,7 +533,7 @@ describe("uwf thread resume - completed threads", () => {
537
533
  await seedThreads(tmpDir, {
538
534
  [THREAD_ID]: {
539
535
  head: reviewerStepHash,
540
- status: "completed",
536
+ status: "end",
541
537
  suspendedRole: null,
542
538
  suspendMessage: null,
543
539
  completedAt: 1716600002000,
@@ -599,7 +595,7 @@ echo '${adapterJson}'
599
595
  const configPath = join(tmpDir, "config.yaml");
600
596
  await writeFile(
601
597
  configPath,
602
- `defaultAgent: uwf-hermes\ndefaultModel: test-model\nagentOverrides: null\nagents: {}\nproviders: {}\nmodels: {}\n`,
598
+ `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
603
599
  );
604
600
 
605
601
  const result = runUwf(