@united-workforce/cli 0.2.1-rc.9 → 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
@@ -1,21 +1,12 @@
1
1
  import { mkdir, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { putSchema } from "@ocas/core";
5
4
  import type { CasRef, ThreadId } from "@united-workforce/protocol";
6
5
  import { describe, expect, test } from "vitest";
7
- import { createMarker, deleteMarker } from "../background/index.js";
6
+ import { createMarker, deleteMarker, getProcessStartTime } from "../background/index.js";
8
7
  import { cmdThreadShow, cmdThreadStart } from "../commands/thread.js";
9
8
  import { completeThread, createUwfStore, loadAllThreads, setThread } from "../store.js";
10
9
 
11
- const OUTPUT_SCHEMA = {
12
- type: "object" as const,
13
- properties: {
14
- $status: { type: "string" as const },
15
- question: { type: "string" as const },
16
- },
17
- };
18
-
19
10
  const TEST_WORKFLOW_YAML = `
20
11
  name: test-status
21
12
  description: Test workflow for status field
@@ -59,15 +50,12 @@ roles:
59
50
  capabilities: ["coding"]
60
51
  procedure: Work
61
52
  output: |
62
- $status: "needs_input"
63
- question: "Which API?"
53
+ $status: "done"
64
54
  frontmatter:
65
- oneOf:
66
- - type: object
67
- required: ["$status", "question"]
68
- properties:
69
- $status: { const: "needs_input" }
70
- question: { type: string }
55
+ type: object
56
+ required: ["$status"]
57
+ properties:
58
+ $status: { const: "done" }
71
59
  graph:
72
60
  $START:
73
61
  new:
@@ -79,9 +67,9 @@ graph:
79
67
  prompt: "Resume work"
80
68
  location: null
81
69
  worker:
82
- needs_input:
83
- role: $SUSPEND
84
- prompt: "Please clarify: {{{question}}}"
70
+ done:
71
+ role: $END
72
+ prompt: "Done"
85
73
  location: null
86
74
  `;
87
75
 
@@ -97,8 +85,7 @@ async function insertStepNode(
97
85
  if (headEntry === undefined) throw new Error(`thread ${threadId} not in index`);
98
86
  const head = headEntry.head;
99
87
 
100
- const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
101
- const outputHash = await uwf.store.cas.put(outputSchemaHash, outputPayload);
88
+ const outputHash = await uwf.store.cas.put(uwf.schemas.suspendOutput, outputPayload);
102
89
  const detailHash = await uwf.store.cas.put(uwf.schemas.text, "detail-placeholder");
103
90
 
104
91
  const headNode = uwf.store.cas.get(head);
@@ -183,6 +170,7 @@ describe("thread show status field", () => {
183
170
  workflow,
184
171
  pid: process.pid,
185
172
  startedAt: Date.now(),
173
+ processStartTime: getProcessStartTime(process.pid),
186
174
  });
187
175
 
188
176
  try {
@@ -216,11 +204,11 @@ describe("thread show status field", () => {
216
204
  const head = index[threadId]!.head;
217
205
  if (!head) throw new Error("Thread not found in index");
218
206
 
219
- completeThread(uwfForIndex.varStore, threadId, "completed");
207
+ completeThread(uwfForIndex.varStore, threadId, "end");
220
208
 
221
209
  const result = await cmdThreadShow(storageRoot, threadId);
222
210
 
223
- expect(result.status).toBe("completed");
211
+ expect(result.status).toBe("end");
224
212
  expect(result.done).toBe(true);
225
213
  expect(result.background).toBe(null);
226
214
  expect(result.thread).toBe(threadId);
@@ -274,11 +262,11 @@ describe("thread show status field", () => {
274
262
  const head = index[threadId]!.head;
275
263
  if (!head) throw new Error("Thread not found in index");
276
264
 
277
- completeThread(uwfForIndex.varStore, threadId, "completed");
265
+ completeThread(uwfForIndex.varStore, threadId, "end");
278
266
 
279
267
  const result = await cmdThreadShow(storageRoot, threadId);
280
268
 
281
- expect(result.status).toBe("completed");
269
+ expect(result.status).toBe("end");
282
270
  expect(result.done).toBe(true);
283
271
  expect(result.background).toBe(null);
284
272
 
@@ -300,8 +288,8 @@ describe("thread show status field", () => {
300
288
  const threadId = startResult.thread as ThreadId;
301
289
 
302
290
  await insertStepNode(storageRoot, threadId, "worker", {
303
- $status: "needs_input",
304
- question: "Which API?",
291
+ $status: "$SUSPEND",
292
+ reason: "Please clarify: Which API?",
305
293
  });
306
294
 
307
295
  const result = await cmdThreadShow(storageRoot, threadId);
@@ -62,13 +62,7 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
62
62
  new: { role: "worker", prompt: "Start work", location: null },
63
63
  resume: { role: "worker", prompt: "Resume work", location: null },
64
64
  },
65
- worker: {
66
- needs_input: {
67
- role: "$SUSPEND",
68
- prompt: "Please clarify: {{{question}}}",
69
- location: null,
70
- },
71
- },
65
+ worker: {},
72
66
  },
73
67
  });
74
68
 
@@ -81,9 +75,9 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
81
75
  const threadId = "01SUSPENDSTEPTEST0000000" as ThreadId;
82
76
  await seedThreads(tmpDir, { [threadId]: startHash });
83
77
 
84
- const outputHash = await store.cas.put(outputSchemaHash, {
85
- $status: "needs_input",
86
- question: "Which API?",
78
+ const outputHash = await store.cas.put(schemas.suspendOutput, {
79
+ $status: "$SUSPEND",
80
+ reason: "Please clarify: Which API?",
87
81
  });
88
82
  const detailHash = await store.cas.put(schemas.text, "mock detail");
89
83
 
@@ -109,7 +103,7 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
109
103
  stepHash,
110
104
  detailHash,
111
105
  role: "worker",
112
- frontmatter: { $status: "needs_input", question: "Which API?" },
106
+ frontmatter: { $status: "$SUSPEND", reason: "Please clarify: Which API?" },
113
107
  body: "",
114
108
  startedAtMs,
115
109
  completedAtMs,
@@ -119,7 +113,7 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
119
113
  const configPath = join(tmpDir, "config.yaml");
120
114
  await writeFile(
121
115
  configPath,
122
- `defaultAgent: uwf-hermes\ndefaultModel: test-model\nagentOverrides: null\nagents: {}\nproviders: {}\nmodels: {}\n`,
116
+ `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
123
117
  );
124
118
 
125
119
  const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
@@ -154,8 +148,8 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
154
148
 
155
149
  const outputNode = storeAfter.cas.get(outputHash);
156
150
  expect(outputNode?.payload).toEqual({
157
- $status: "needs_input",
158
- question: "Which API?",
151
+ $status: "$SUSPEND",
152
+ reason: "Please clarify: Which API?",
159
153
  });
160
154
 
161
155
  const { createUwfStore, getThread } = await import("../store.js");
@@ -59,13 +59,7 @@ describe("suspended thread display", () => {
59
59
  new: { role: "worker", prompt: "Start work", location: null },
60
60
  resume: { role: "worker", prompt: "Resume work", location: null },
61
61
  },
62
- worker: {
63
- needs_input: {
64
- role: "$SUSPEND",
65
- prompt: "Please provide more details: {{{question}}}",
66
- location: null,
67
- },
68
- },
62
+ worker: {},
69
63
  },
70
64
  });
71
65
 
@@ -77,9 +71,9 @@ describe("suspended thread display", () => {
77
71
 
78
72
  // Create suspended thread
79
73
  const suspendedThreadId = "01SUSPENDEDTHREAD0000000" as ThreadId;
80
- const outputHash = await uwf.store.cas.put(outputSchemaHash, {
81
- $status: "needs_input",
82
- question: "What is the target API?",
74
+ const outputHash = await uwf.store.cas.put(uwf.schemas.suspendOutput, {
75
+ $status: "$SUSPEND",
76
+ reason: "Please provide more details: What is the target API?",
83
77
  });
84
78
  const detailHash = await uwf.store.cas.put(uwf.schemas.text, "mock detail");
85
79
 
@@ -118,8 +112,8 @@ describe("suspended thread display", () => {
118
112
  [idleThreadId]: idleEntry,
119
113
  });
120
114
 
121
- // Test thread list
122
- const listResult = await cmdThreadList(tmpDir, null, null, null, null, null);
115
+ // Test thread list — pass showAll=true to include suspended threads
116
+ const listResult = await cmdThreadList(tmpDir, null, null, null, null, null, true);
123
117
 
124
118
  // Find the suspended and idle threads in results
125
119
  const suspendedItem = listResult.find((item) => item.thread === suspendedThreadId);
@@ -169,13 +163,7 @@ describe("suspended thread display", () => {
169
163
  new: { role: "worker", prompt: "Start work", location: null },
170
164
  resume: { role: "worker", prompt: "Resume work", location: null },
171
165
  },
172
- worker: {
173
- needs_input: {
174
- role: "$SUSPEND",
175
- prompt: "Need clarification: {{{question}}}",
176
- location: null,
177
- },
178
- },
166
+ worker: {},
179
167
  },
180
168
  });
181
169
 
@@ -186,9 +174,9 @@ describe("suspended thread display", () => {
186
174
  });
187
175
 
188
176
  const threadId = "01SUSPENDSHOW000000000" as ThreadId;
189
- const outputHash = await uwf.store.cas.put(outputSchemaHash, {
190
- $status: "needs_input",
191
- question: "Which database to use?",
177
+ const outputHash = await uwf.store.cas.put(uwf.schemas.suspendOutput, {
178
+ $status: "$SUSPEND",
179
+ reason: "Need clarification: Which database to use?",
192
180
  });
193
181
  const detailHash = await uwf.store.cas.put(uwf.schemas.text, "mock detail");
194
182
 
@@ -752,7 +752,7 @@ describe("cmdStepList with completed threads", () => {
752
752
  suspendMessage: null,
753
753
  completedAt: null,
754
754
  });
755
- completeThread(uwf.varStore, threadId, "completed");
755
+ completeThread(uwf.varStore, threadId, "end");
756
756
 
757
757
  const result = await cmdStepList(tmpDir, threadId);
758
758
 
@@ -881,7 +881,7 @@ describe("cmdStepShow with completed threads", () => {
881
881
  suspendMessage: null,
882
882
  completedAt: null,
883
883
  });
884
- completeThread(uwf.varStore, threadId, "completed");
884
+ completeThread(uwf.varStore, threadId, "end");
885
885
 
886
886
  const result = await cmdStepShow(tmpDir, stepHash);
887
887
 
@@ -944,7 +944,7 @@ describe("cmdThreadRead with completed threads", () => {
944
944
  suspendMessage: null,
945
945
  completedAt: null,
946
946
  });
947
- completeThread(uwf.varStore, threadId, "completed");
947
+ completeThread(uwf.varStore, threadId, "end");
948
948
 
949
949
  const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
950
950
 
@@ -1007,7 +1007,7 @@ describe("cmdThreadRead with completed threads", () => {
1007
1007
  suspendMessage: null,
1008
1008
  completedAt: null,
1009
1009
  });
1010
- completeThread(uwf.varStore, threadId, "completed");
1010
+ completeThread(uwf.varStore, threadId, "end");
1011
1011
 
1012
1012
  const markdown = await cmdThreadRead(
1013
1013
  tmpDir,
@@ -5,6 +5,7 @@ import { validateWorkflow } from "../validate-semantic.js";
5
5
  /** Build a valid two-role workflow that passes all checks. */
6
6
  function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
7
7
  const base: WorkflowPayload = {
8
+ version: 1,
8
9
  name: "test-workflow",
9
10
  description: "A test workflow",
10
11
  roles: {
@@ -55,10 +56,10 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
55
56
  new: { role: "writer", prompt: "Begin writing", location: null },
56
57
  resume: { role: "writer", prompt: "Review previous output and continue", location: null },
57
58
  },
58
- writer: { done: { role: "reviewer", prompt: "Review this: {{{plan}}}", location: null } },
59
+ writer: { done: { role: "reviewer", prompt: "Review this: {{ plan }}", location: null } },
59
60
  reviewer: {
60
- approved: { role: "$END", prompt: "Done: {{{summary}}}", location: null },
61
- rejected: { role: "writer", prompt: "Fix: {{{reason}}}", location: null },
61
+ approved: { role: "$END", prompt: "Done: {{ summary }}", location: null },
62
+ rejected: { role: "writer", prompt: "Fix: {{ reason }}", location: null },
62
63
  },
63
64
  },
64
65
  };
@@ -208,8 +209,6 @@ describe("Suite 2: Graph Structure", () => {
208
209
 
209
210
  describe("Suite 3: Status-Edge Consistency", () => {
210
211
  test("3.1 user role using _ graph key is treated as an unknown status", () => {
211
- // "_" is no longer special-cased — it's just a status key that does not
212
- // match the role's $status enum, so it surfaces as extra/missing keys.
213
212
  const wf = makeWorkflow();
214
213
  wf.graph.writer = { _: { role: "reviewer", prompt: "Review", location: null } };
215
214
  const errors = validateWorkflow(wf);
@@ -288,7 +287,7 @@ describe("Suite 3b: Enum-Based $status is Rejected", () => {
288
287
  };
289
288
  wf.graph.reviewer = {
290
289
  approved: { role: "$END", prompt: "Done", location: null },
291
- rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
290
+ rejected: { role: "writer", prompt: "Fix: {{ comments }}", location: null },
292
291
  };
293
292
  const errors = validateWorkflow(wf);
294
293
  expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
@@ -307,7 +306,9 @@ describe("Suite 3b: Enum-Based $status is Rejected", () => {
307
306
  required: ["$status", "plan"],
308
307
  } as unknown as string,
309
308
  };
310
- wf.graph.writer = { ready: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } };
309
+ wf.graph.writer = {
310
+ ready: { role: "reviewer", prompt: "Review: {{ plan }}", location: null },
311
+ };
311
312
  const errors = validateWorkflow(wf);
312
313
  expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
313
314
  });
@@ -352,7 +353,7 @@ describe("Suite 3c: Const-Based Flat Schema", () => {
352
353
  expect(errors.some((e) => e.includes("extra status keys") && e.includes("extra"))).toBe(true);
353
354
  });
354
355
 
355
- test("3c.3 flat schema with const $status validates mustache vars", () => {
356
+ test("3c.3 flat schema with const $status validates template vars", () => {
356
357
  const wf = makeWorkflow();
357
358
  wf.roles.writer = {
358
359
  ...wf.roles.writer,
@@ -366,46 +367,36 @@ describe("Suite 3c: Const-Based Flat Schema", () => {
366
367
  } as unknown as string,
367
368
  };
368
369
  wf.graph.writer = {
369
- done: { role: "reviewer", prompt: "Review: {{{nonexistent}}}", location: null },
370
+ done: { role: "reviewer", prompt: "Review: {{ nonexistent }}", location: null },
370
371
  };
371
372
  const errors = validateWorkflow(wf);
372
- expect(
373
- errors.some(
374
- (e) => e.includes('prompt variable "nonexistent"') && e.includes('role "writer"'),
375
- ),
376
- ).toBe(true);
373
+ expect(errors.some((e) => e.includes("nonexistent") && e.includes('role "writer"'))).toBe(true);
377
374
  });
378
375
  });
379
376
 
380
- describe("Suite 4: Mustache Template Variable Existence", () => {
381
- test("4.1 prompt references nonexistent variable (enum status)", () => {
377
+ describe("Suite 4: Template Variable Existence (LiquidJS strict-render)", () => {
378
+ test("4.1 prompt references nonexistent variable (flat schema)", () => {
382
379
  const wf = makeWorkflow();
383
380
  wf.graph.writer = {
384
- done: { role: "reviewer", prompt: "Review: {{{branch}}}", location: null },
381
+ done: { role: "reviewer", prompt: "Review: {{ branch }}", location: null },
385
382
  };
386
383
  const errors = validateWorkflow(wf);
387
- expect(
388
- errors.some(
389
- (e) => e.includes('prompt variable "branch"') && e.includes('role "writer" frontmatter'),
390
- ),
391
- ).toBe(true);
384
+ expect(errors.some((e) => e.includes("branch") && e.includes('role "writer"'))).toBe(true);
392
385
  });
393
386
 
394
387
  test("4.2 prompt references nonexistent variable (multi-exit)", () => {
395
388
  const wf = makeWorkflow();
396
389
  wf.graph.reviewer = {
397
- approved: { role: "$END", prompt: "Done: {{{branch}}}", location: null },
398
- rejected: { role: "writer", prompt: "Fix: {{{reason}}}", location: null },
390
+ approved: { role: "$END", prompt: "Done: {{ branch }}", location: null },
391
+ rejected: { role: "writer", prompt: "Fix: {{ reason }}", location: null },
399
392
  };
400
393
  const errors = validateWorkflow(wf);
401
394
  expect(
402
- errors.some((e) =>
403
- e.includes('prompt variable "branch" not found in role "reviewer" variant "approved"'),
404
- ),
395
+ errors.some((e) => e.includes("branch") && e.includes("reviewer") && e.includes("approved")),
405
396
  ).toBe(true);
406
397
  });
407
398
 
408
- test("4.3 valid mustache variables pass", () => {
399
+ test("4.3 valid template variables pass", () => {
409
400
  const wf = makeWorkflow();
410
401
  const errors = validateWorkflow(wf);
411
402
  expect(errors).toEqual([]);
@@ -413,7 +404,9 @@ describe("Suite 4: Mustache Template Variable Existence", () => {
413
404
 
414
405
  test("4.4 $status variable is always valid", () => {
415
406
  const wf = makeWorkflow();
416
- wf.graph.writer = { done: { role: "reviewer", prompt: "Status: {{$status}}", location: null } };
407
+ wf.graph.writer = {
408
+ done: { role: "reviewer", prompt: "Status: {{ $status }}", location: null },
409
+ };
417
410
  const errors = validateWorkflow(wf);
418
411
  expect(errors).toEqual([]);
419
412
  });
@@ -469,6 +462,41 @@ describe("Suite 5: oneOf Discriminant Validity", () => {
469
462
  });
470
463
  });
471
464
 
465
+ describe("Suite 7: $SUSPEND is no longer a valid graph target", () => {
466
+ test("7.1 edge targeting $SUSPEND is rejected with a migration hint", () => {
467
+ const wf = makeWorkflow();
468
+ wf.graph.writer = {
469
+ done: { role: "$SUSPEND", prompt: "Need more info", location: null },
470
+ };
471
+ const errors = validateWorkflow(wf);
472
+ expect(
473
+ errors.some(
474
+ (e) =>
475
+ e.includes("$SUSPEND") &&
476
+ e.includes("no longer a valid graph target") &&
477
+ e.includes('Emit $status: "$SUSPEND"'),
478
+ ),
479
+ ).toBe(true);
480
+ });
481
+
482
+ test("7.2 $SUSPEND as a graph node is rejected", () => {
483
+ const wf = makeWorkflow();
484
+ (wf.graph as Record<string, unknown>).$SUSPEND = {
485
+ done: { role: "$END", prompt: "done", location: null },
486
+ };
487
+ const errors = validateWorkflow(wf);
488
+ expect(
489
+ errors.some((e) => e.includes("$SUSPEND") && e.includes("no longer a valid graph node")),
490
+ ).toBe(true);
491
+ });
492
+
493
+ test("7.3 a role emitting $SUSPEND from its output (not the graph) passes", () => {
494
+ const wf = makeWorkflow();
495
+ const errors = validateWorkflow(wf);
496
+ expect(errors.some((e) => e.includes("$SUSPEND"))).toBe(false);
497
+ });
498
+ });
499
+
472
500
  describe("Suite 6: Multiple Errors Collection", () => {
473
501
  test("6.1 multiple errors collected", () => {
474
502
  const wf = makeWorkflow();
@@ -487,8 +515,8 @@ describe("Suite 6: Multiple Errors Collection", () => {
487
515
  };
488
516
  // unknown graph reference
489
517
  wf.graph.nonexistent = { done: { role: "$END", prompt: "done", location: null } };
490
- // bad mustache var
491
- wf.graph.writer = { done: { role: "reviewer", prompt: "{{{badvar}}}", location: null } };
518
+ // bad template var
519
+ wf.graph.writer = { done: { role: "reviewer", prompt: "{{ badvar }}", location: null } };
492
520
  const errors = validateWorkflow(wf);
493
521
  expect(errors.length).toBeGreaterThanOrEqual(3);
494
522
  });