@united-workforce/cli 0.4.0 → 0.6.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 (223) hide show
  1. package/README.md +30 -3
  2. package/dist/.build-fingerprint +1 -0
  3. package/dist/__tests__/adapter-json-roundtrip.test.js +16 -6
  4. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  5. package/dist/__tests__/concurrency.test.d.ts +2 -0
  6. package/dist/__tests__/concurrency.test.d.ts.map +1 -0
  7. package/dist/__tests__/concurrency.test.js +196 -0
  8. package/dist/__tests__/concurrency.test.js.map +1 -0
  9. package/dist/__tests__/config-text-renderer.test.d.ts +2 -0
  10. package/dist/__tests__/config-text-renderer.test.d.ts.map +1 -0
  11. package/dist/__tests__/config-text-renderer.test.js +137 -0
  12. package/dist/__tests__/config-text-renderer.test.js.map +1 -0
  13. package/dist/__tests__/e2e-mock-agent.test.js +23 -7
  14. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  15. package/dist/__tests__/format-text-default.test.d.ts +2 -0
  16. package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
  17. package/dist/__tests__/format-text-default.test.js +43 -0
  18. package/dist/__tests__/format-text-default.test.js.map +1 -0
  19. package/dist/__tests__/format-text-registry.test.d.ts +2 -0
  20. package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
  21. package/dist/__tests__/format-text-registry.test.js +158 -0
  22. package/dist/__tests__/format-text-registry.test.js.map +1 -0
  23. package/dist/__tests__/issue-180-workflow-ref-removed.test.js +1 -1
  24. package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
  25. package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
  26. package/dist/__tests__/log-text-renderer.test.js +265 -0
  27. package/dist/__tests__/log-text-renderer.test.js.map +1 -0
  28. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
  29. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
  30. package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
  31. package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
  32. package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
  33. package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
  34. package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
  35. package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
  36. package/dist/__tests__/pid-recycling.test.js +9 -7
  37. package/dist/__tests__/pid-recycling.test.js.map +1 -1
  38. package/dist/__tests__/prompt.test.js +46 -4
  39. package/dist/__tests__/prompt.test.js.map +1 -1
  40. package/dist/__tests__/resolve-head-hash.test.js +8 -0
  41. package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
  42. package/dist/__tests__/solve-issue-tea-worktree.test.js +3 -1
  43. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
  44. package/dist/__tests__/step-ask.test.js +9 -1
  45. package/dist/__tests__/step-ask.test.js.map +1 -1
  46. package/dist/__tests__/store-unified-threads.test.js +19 -17
  47. package/dist/__tests__/store-unified-threads.test.js.map +1 -1
  48. package/dist/__tests__/thread-agent-failure-suspended.test.d.ts +2 -0
  49. package/dist/__tests__/thread-agent-failure-suspended.test.d.ts.map +1 -0
  50. package/dist/__tests__/thread-agent-failure-suspended.test.js +332 -0
  51. package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -0
  52. package/dist/__tests__/thread-cancel-status.test.js +19 -13
  53. package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
  54. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
  55. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
  56. package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
  57. package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
  58. package/dist/__tests__/thread-join.test.d.ts +2 -0
  59. package/dist/__tests__/thread-join.test.d.ts.map +1 -0
  60. package/dist/__tests__/thread-join.test.js +77 -0
  61. package/dist/__tests__/thread-join.test.js.map +1 -0
  62. package/dist/__tests__/thread-list-filters.test.js +10 -8
  63. package/dist/__tests__/thread-list-filters.test.js.map +1 -1
  64. package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
  65. package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
  66. package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
  67. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
  68. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
  69. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
  70. package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
  71. package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
  72. package/dist/__tests__/thread-poke.test.js +15 -2
  73. package/dist/__tests__/thread-poke.test.js.map +1 -1
  74. package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
  75. package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
  76. package/dist/__tests__/thread-resume.test.js +11 -1
  77. package/dist/__tests__/thread-resume.test.js.map +1 -1
  78. package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
  79. package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
  80. package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
  81. package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
  82. package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
  83. package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
  84. package/dist/__tests__/thread-suspend-step.test.js +5 -2
  85. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  86. package/dist/__tests__/thread-test-helpers.d.ts +7 -0
  87. package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
  88. package/dist/__tests__/thread-test-helpers.js +13 -0
  89. package/dist/__tests__/thread-test-helpers.js.map +1 -1
  90. package/dist/__tests__/thread.test.js +11 -9
  91. package/dist/__tests__/thread.test.js.map +1 -1
  92. package/dist/__tests__/validate-semantic.test.js +56 -2
  93. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  94. package/dist/__tests__/workflow-list-recursive.test.js +10 -7
  95. package/dist/__tests__/workflow-list-recursive.test.js.map +1 -1
  96. package/dist/__tests__/workflow-paths.test.d.ts +2 -0
  97. package/dist/__tests__/workflow-paths.test.d.ts.map +1 -0
  98. package/dist/__tests__/workflow-paths.test.js +261 -0
  99. package/dist/__tests__/workflow-paths.test.js.map +1 -0
  100. package/dist/__tests__/workflow-resolution.test.js +10 -7
  101. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  102. package/dist/__tests__/workflow-show-resolution.test.js +10 -7
  103. package/dist/__tests__/workflow-show-resolution.test.js.map +1 -1
  104. package/dist/__tests__/workflow-validate.test.js +75 -55
  105. package/dist/__tests__/workflow-validate.test.js.map +1 -1
  106. package/dist/__tests__/write-envelope.test.d.ts +2 -0
  107. package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
  108. package/dist/__tests__/write-envelope.test.js +201 -0
  109. package/dist/__tests__/write-envelope.test.js.map +1 -0
  110. package/dist/cli.js +76 -36
  111. package/dist/cli.js.map +1 -1
  112. package/dist/commands/config.d.ts +5 -0
  113. package/dist/commands/config.d.ts.map +1 -1
  114. package/dist/commands/config.js +81 -3
  115. package/dist/commands/config.js.map +1 -1
  116. package/dist/commands/prompt.d.ts.map +1 -1
  117. package/dist/commands/prompt.js +42 -29
  118. package/dist/commands/prompt.js.map +1 -1
  119. package/dist/commands/setup.d.ts +9 -4
  120. package/dist/commands/setup.d.ts.map +1 -1
  121. package/dist/commands/setup.js +51 -7
  122. package/dist/commands/setup.js.map +1 -1
  123. package/dist/commands/thread.d.ts +12 -0
  124. package/dist/commands/thread.d.ts.map +1 -1
  125. package/dist/commands/thread.js +226 -9
  126. package/dist/commands/thread.js.map +1 -1
  127. package/dist/commands/workflow.d.ts +2 -2
  128. package/dist/commands/workflow.d.ts.map +1 -1
  129. package/dist/commands/workflow.js +26 -10
  130. package/dist/commands/workflow.js.map +1 -1
  131. package/dist/concurrency/concurrency.d.ts +34 -0
  132. package/dist/concurrency/concurrency.d.ts.map +1 -0
  133. package/dist/concurrency/concurrency.js +216 -0
  134. package/dist/concurrency/concurrency.js.map +1 -0
  135. package/dist/concurrency/index.d.ts +3 -0
  136. package/dist/concurrency/index.d.ts.map +1 -0
  137. package/dist/concurrency/index.js +2 -0
  138. package/dist/concurrency/index.js.map +1 -0
  139. package/dist/concurrency/types.d.ts +19 -0
  140. package/dist/concurrency/types.d.ts.map +1 -0
  141. package/dist/concurrency/types.js +2 -0
  142. package/dist/concurrency/types.js.map +1 -0
  143. package/dist/format.d.ts +69 -2
  144. package/dist/format.d.ts.map +1 -1
  145. package/dist/format.js +198 -1
  146. package/dist/format.js.map +1 -1
  147. package/dist/output-mappers.d.ts +122 -0
  148. package/dist/output-mappers.d.ts.map +1 -0
  149. package/dist/output-mappers.js +134 -0
  150. package/dist/output-mappers.js.map +1 -0
  151. package/dist/schemas.d.ts +4 -1
  152. package/dist/schemas.d.ts.map +1 -1
  153. package/dist/schemas.js +31 -4
  154. package/dist/schemas.js.map +1 -1
  155. package/dist/store.d.ts +11 -0
  156. package/dist/store.d.ts.map +1 -1
  157. package/dist/store.js +20 -1
  158. package/dist/store.js.map +1 -1
  159. package/dist/text-renderers.d.ts +30 -0
  160. package/dist/text-renderers.d.ts.map +1 -0
  161. package/dist/text-renderers.js +251 -0
  162. package/dist/text-renderers.js.map +1 -0
  163. package/dist/validate-semantic.d.ts.map +1 -1
  164. package/dist/validate-semantic.js +28 -11
  165. package/dist/validate-semantic.js.map +1 -1
  166. package/examples/brainstorm.yaml +130 -0
  167. package/examples/debate.yaml +169 -0
  168. package/examples/socratic-questioning.yaml +112 -0
  169. package/package.json +12 -11
  170. package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
  171. package/src/__tests__/concurrency.test.ts +266 -0
  172. package/src/__tests__/config-text-renderer.test.ts +156 -0
  173. package/src/__tests__/e2e-mock-agent.test.ts +45 -7
  174. package/src/__tests__/format-text-default.test.ts +49 -0
  175. package/src/__tests__/format-text-registry.test.ts +173 -0
  176. package/src/__tests__/issue-180-workflow-ref-removed.test.ts +1 -1
  177. package/src/__tests__/log-text-renderer.test.ts +294 -0
  178. package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
  179. package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
  180. package/src/__tests__/pid-recycling.test.ts +9 -8
  181. package/src/__tests__/prompt.test.ts +48 -4
  182. package/src/__tests__/resolve-head-hash.test.ts +7 -0
  183. package/src/__tests__/solve-issue-tea-worktree.test.ts +3 -1
  184. package/src/__tests__/step-ask.test.ts +8 -1
  185. package/src/__tests__/store-unified-threads.test.ts +21 -18
  186. package/src/__tests__/thread-agent-failure-suspended.test.ts +406 -0
  187. package/src/__tests__/thread-cancel-status.test.ts +21 -14
  188. package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
  189. package/src/__tests__/thread-join.test.ts +103 -0
  190. package/src/__tests__/thread-list-filters.test.ts +9 -9
  191. package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
  192. package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
  193. package/src/__tests__/thread-poke.test.ts +14 -2
  194. package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
  195. package/src/__tests__/thread-resume.test.ts +10 -1
  196. package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
  197. package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
  198. package/src/__tests__/thread-suspend-step.test.ts +5 -2
  199. package/src/__tests__/thread-test-helpers.ts +15 -1
  200. package/src/__tests__/thread.test.ts +10 -10
  201. package/src/__tests__/validate-semantic.test.ts +59 -2
  202. package/src/__tests__/workflow-list-recursive.test.ts +9 -9
  203. package/src/__tests__/workflow-paths.test.ts +337 -0
  204. package/src/__tests__/workflow-resolution.test.ts +9 -8
  205. package/src/__tests__/workflow-show-resolution.test.ts +9 -8
  206. package/src/__tests__/workflow-validate.test.ts +78 -56
  207. package/src/__tests__/write-envelope.test.ts +257 -0
  208. package/src/cli.ts +111 -35
  209. package/src/commands/config.ts +85 -3
  210. package/src/commands/prompt.ts +42 -29
  211. package/src/commands/setup.ts +57 -7
  212. package/src/commands/thread.ts +280 -9
  213. package/src/commands/workflow.ts +32 -11
  214. package/src/concurrency/concurrency.ts +245 -0
  215. package/src/concurrency/index.ts +10 -0
  216. package/src/concurrency/types.ts +19 -0
  217. package/src/format.ts +282 -2
  218. package/src/output-mappers.ts +255 -0
  219. package/src/schemas.ts +39 -3
  220. package/src/store.ts +25 -1
  221. package/src/text-renderers.ts +355 -0
  222. package/src/validate-semantic.ts +33 -12
  223. package/LICENSE +0 -21
@@ -0,0 +1,255 @@
1
+ import type { CasRef, StartOutput, StepOutput } from "@united-workforce/protocol";
2
+ import { extractUlidTimestamp } from "@united-workforce/util";
3
+ import type { ThreadListItemWithStatus } from "./commands/thread.js";
4
+ import type {
5
+ WorkflowAddOutput,
6
+ WorkflowListEntry,
7
+ WorkflowShowOutput,
8
+ } from "./commands/workflow.js";
9
+
10
+ /**
11
+ * Mappers that convert the existing rich command outputs into the
12
+ * schema-aligned payload shapes registered under `@uwf/output/*`.
13
+ *
14
+ * Each mapper returns plain payload data — no CAS refs, no JSON encoding,
15
+ * no I/O. The CLI calls one of these immediately before handing the payload
16
+ * to `writeEnvelope`.
17
+ */
18
+
19
+ export type ThreadStartPayload = {
20
+ threadId: string;
21
+ workflowHash: string;
22
+ };
23
+
24
+ export function toThreadStartPayload(out: StartOutput): ThreadStartPayload {
25
+ return { threadId: out.thread, workflowHash: out.workflow };
26
+ }
27
+
28
+ export type ThreadStatusPayload = {
29
+ threadId: string;
30
+ workflowHash: string;
31
+ head: string | null;
32
+ status: string;
33
+ currentRole: string | null;
34
+ suspendedRole: string | null;
35
+ suspendMessage: string | null;
36
+ done: boolean;
37
+ };
38
+
39
+ export function toThreadStatusPayload(out: StepOutput): ThreadStatusPayload {
40
+ return {
41
+ threadId: out.thread,
42
+ workflowHash: out.workflow,
43
+ head: out.head ?? null,
44
+ status: out.status,
45
+ currentRole: out.currentRole,
46
+ suspendedRole: out.suspendedRole,
47
+ suspendMessage: out.suspendMessage,
48
+ done: out.done,
49
+ };
50
+ }
51
+
52
+ export type ThreadListPayload = {
53
+ items: Array<{
54
+ threadId: string;
55
+ workflowHash: string;
56
+ workflowName: string | null;
57
+ status: string;
58
+ currentRole: string | null;
59
+ startedAt: number | null;
60
+ completedAt: number | null;
61
+ }>;
62
+ };
63
+
64
+ export function toThreadListPayload(items: ThreadListItemWithStatus[]): ThreadListPayload {
65
+ return {
66
+ items: items.map((it) => ({
67
+ threadId: it.thread,
68
+ workflowHash: it.workflow,
69
+ workflowName: it.workflowName,
70
+ status: it.status,
71
+ currentRole: it.currentRole,
72
+ startedAt: extractUlidTimestamp(it.thread),
73
+ completedAt: null,
74
+ })),
75
+ };
76
+ }
77
+
78
+ export type ThreadExecPayload = {
79
+ threadId: string;
80
+ workflowHash: string;
81
+ steps: Array<{
82
+ head: string;
83
+ status: string;
84
+ currentRole: string | null;
85
+ done: boolean;
86
+ role: string | null;
87
+ suspendedRole: string | null;
88
+ suspendMessage: string | null;
89
+ }>;
90
+ };
91
+
92
+ export function toThreadExecPayload(results: StepOutput[]): ThreadExecPayload {
93
+ const first = results[0];
94
+ return {
95
+ threadId: first?.thread ?? "",
96
+ workflowHash: first?.workflow ?? "",
97
+ steps: results.map((r) => ({
98
+ head: r.head,
99
+ status: r.status,
100
+ currentRole: r.currentRole,
101
+ done: r.done,
102
+ role: r.currentRole ?? r.suspendedRole ?? null,
103
+ suspendedRole: r.suspendedRole,
104
+ suspendMessage: r.suspendMessage,
105
+ })),
106
+ };
107
+ }
108
+
109
+ export type StepDetailPayload = {
110
+ hash: string;
111
+ role: string;
112
+ agent: string;
113
+ status: string;
114
+ startedAtMs: number | null;
115
+ completedAtMs: number | null;
116
+ durationMs: number | null;
117
+ frontmatter: Record<string, unknown>;
118
+ turns: Array<{ role: string; content: string; timestamp: number | null }>;
119
+ };
120
+
121
+ export function toStepDetailPayload(stepHash: CasRef, raw: unknown): StepDetailPayload {
122
+ const r = (raw ?? {}) as Record<string, unknown>;
123
+ const turnsIn = Array.isArray(r.turns) ? (r.turns as unknown[]) : [];
124
+ const startedAtMs = numericOrNull(r.startedAtMs);
125
+ const completedAtMs = numericOrNull(r.completedAtMs);
126
+ const durationMs =
127
+ startedAtMs !== null && completedAtMs !== null && completedAtMs >= startedAtMs
128
+ ? completedAtMs - startedAtMs
129
+ : null;
130
+ const frontmatter =
131
+ r.frontmatter !== null && typeof r.frontmatter === "object" && !Array.isArray(r.frontmatter)
132
+ ? (r.frontmatter as Record<string, unknown>)
133
+ : {};
134
+ const status =
135
+ typeof frontmatter.$status === "string"
136
+ ? (frontmatter.$status as string)
137
+ : typeof r.status === "string"
138
+ ? (r.status as string)
139
+ : "";
140
+ return {
141
+ hash: stepHash,
142
+ role: typeof r.role === "string" ? r.role : "",
143
+ agent: typeof r.agent === "string" ? r.agent : "",
144
+ status,
145
+ startedAtMs,
146
+ completedAtMs,
147
+ durationMs,
148
+ frontmatter,
149
+ turns: turnsIn.map((t) => {
150
+ const o = (t ?? {}) as Record<string, unknown>;
151
+ return {
152
+ role: typeof o.role === "string" ? o.role : "",
153
+ content: typeof o.content === "string" ? o.content : "",
154
+ timestamp: numericOrNull(o.timestamp),
155
+ };
156
+ }),
157
+ };
158
+ }
159
+
160
+ function numericOrNull(v: unknown): number | null {
161
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
162
+ }
163
+
164
+ export type StepListPayload = {
165
+ threadId: string;
166
+ items: Array<{ hash: string; role: string; durationMs: number | null }>;
167
+ };
168
+
169
+ type StepsLikeOutput = {
170
+ thread: string;
171
+ steps: Array<{
172
+ hash: CasRef;
173
+ role?: string;
174
+ durationMs?: number;
175
+ }>;
176
+ };
177
+
178
+ export function toStepListPayload(out: StepsLikeOutput): StepListPayload {
179
+ return {
180
+ threadId: out.thread,
181
+ items: out.steps
182
+ .filter((s) => typeof s.role === "string")
183
+ .map((s) => ({
184
+ hash: s.hash,
185
+ role: s.role ?? "",
186
+ durationMs: typeof s.durationMs === "number" ? s.durationMs : null,
187
+ })),
188
+ };
189
+ }
190
+
191
+ export type WorkflowDetailPayload = {
192
+ name: string;
193
+ hash: string;
194
+ version: number;
195
+ description: string;
196
+ roles: Record<string, { description: string; goal: string }>;
197
+ graph: Record<string, Record<string, { role: string; prompt: string }>>;
198
+ };
199
+
200
+ export function toWorkflowDetailPayload(out: WorkflowShowOutput): WorkflowDetailPayload {
201
+ const roles: Record<string, { description: string; goal: string }> = {};
202
+ for (const [name, def] of Object.entries(out.payload.roles)) {
203
+ roles[name] = { description: def.description, goal: def.goal };
204
+ }
205
+ const graph: Record<string, Record<string, { role: string; prompt: string }>> = {};
206
+ for (const [from, transitions] of Object.entries(out.payload.graph)) {
207
+ const t: Record<string, { role: string; prompt: string }> = {};
208
+ for (const [status, target] of Object.entries(transitions)) {
209
+ t[status] = { role: target.role, prompt: target.prompt };
210
+ }
211
+ graph[from] = t;
212
+ }
213
+ return {
214
+ name: out.name ?? out.payload.name,
215
+ hash: out.hash,
216
+ version: out.payload.version,
217
+ description: out.payload.description,
218
+ roles,
219
+ graph,
220
+ };
221
+ }
222
+
223
+ export type WorkflowListPayload = {
224
+ items: Array<{ name: string; hash: string; source: string; description: string }>;
225
+ };
226
+
227
+ export function toWorkflowListPayload(entries: WorkflowListEntry[]): WorkflowListPayload {
228
+ return {
229
+ items: entries.map((e) => ({
230
+ name: e.name,
231
+ hash: e.hash,
232
+ source:
233
+ e.origin === "local" ? ".workflows" : e.origin === "paths" ? "workflowPaths" : "registry",
234
+ description: "",
235
+ })),
236
+ };
237
+ }
238
+
239
+ export type WorkflowAddPayload = {
240
+ name: string;
241
+ hash: string;
242
+ };
243
+
244
+ export function toWorkflowAddPayload(out: WorkflowAddOutput): WorkflowAddPayload {
245
+ return { name: out.name, hash: out.hash };
246
+ }
247
+
248
+ export type ValidateResultPayload = {
249
+ valid: boolean;
250
+ errors: string[];
251
+ };
252
+
253
+ export function toValidateResultPayload(errors: string[]): ValidateResultPayload {
254
+ return { valid: errors.length === 0, errors };
255
+ }
package/src/schemas.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import type { Hash, Store } from "@ocas/core";
2
- import { putSchema } from "@ocas/core";
2
+ import { bootstrap, putSchema } from "@ocas/core";
3
3
  import {
4
4
  ERROR_OUTPUT_SCHEMA,
5
+ OUTPUT_SCHEMAS,
6
+ OUTPUT_TEMPLATES,
7
+ type OutputSchemaName,
8
+ outputSchemaVarName,
5
9
  START_NODE_SCHEMA,
6
10
  STEP_NODE_SCHEMA,
7
11
  SUSPEND_OUTPUT_SCHEMA,
@@ -17,10 +21,12 @@ export type UwfSchemaHashes = {
17
21
  text: Hash;
18
22
  errorOutput: Hash;
19
23
  suspendOutput: Hash;
24
+ outputs: Record<OutputSchemaName, Hash>;
20
25
  };
21
26
 
22
27
  /**
23
- * Register Workflow, StartNode, and StepNode JSON Schemas in the CAS store.
28
+ * Register every uwf JSON Schema (workflow / start / step / error / suspend
29
+ * + the 9 CLI output envelopes) and the matching `text` Liquid templates.
24
30
  * Idempotent: safe to call on every CLI invocation.
25
31
  */
26
32
  export async function registerUwfSchemas(store: Store): Promise<UwfSchemaHashes> {
@@ -32,5 +38,35 @@ export async function registerUwfSchemas(store: Store): Promise<UwfSchemaHashes>
32
38
  putSchema(store, ERROR_OUTPUT_SCHEMA),
33
39
  putSchema(store, SUSPEND_OUTPUT_SCHEMA),
34
40
  ]);
35
- return { workflow, startNode, stepNode, text, errorOutput, suspendOutput };
41
+ const outputs = await registerOutputSchemas(store);
42
+ return { workflow, startNode, stepNode, text, errorOutput, suspendOutput, outputs };
43
+ }
44
+
45
+ /**
46
+ * Register the 9 CLI output schemas, bind `@uwf/output/<name>` to each, store
47
+ * each Liquid template as an `@ocas/string` CAS node, and bind
48
+ * `@ocas/template/text/<schemaHash>` to the template content hash.
49
+ *
50
+ * Idempotent: writes are content-addressed so repeat invocations no-op.
51
+ */
52
+ async function registerOutputSchemas(store: Store): Promise<Record<OutputSchemaName, Hash>> {
53
+ const aliases = bootstrap(store);
54
+ const stringHash = aliases["@ocas/string"];
55
+ if (stringHash === undefined) {
56
+ throw new Error("@ocas/string schema not found in bootstrap result");
57
+ }
58
+
59
+ const result = {} as Record<OutputSchemaName, Hash>;
60
+ const names = Object.keys(OUTPUT_SCHEMAS) as OutputSchemaName[];
61
+ for (const name of names) {
62
+ const schemaHash = await putSchema(store, OUTPUT_SCHEMAS[name]);
63
+ store.var.set(outputSchemaVarName(name), schemaHash);
64
+
65
+ const template = OUTPUT_TEMPLATES[name];
66
+ const contentHash = store.cas.put(stringHash, template);
67
+ store.var.set(`@ocas/template/text/${schemaHash}`, contentHash);
68
+
69
+ result[name] = schemaHash;
70
+ }
71
+ return result;
36
72
  }
package/src/store.ts CHANGED
@@ -56,7 +56,7 @@ async function findIndexWorkflow(
56
56
  * Scan a single directory for workflow entries (flat YAML files + folder/index.yaml).
57
57
  * Returns discovered entries. Returns empty array if directory does not exist.
58
58
  */
59
- async function scanWorkflowDir(dir: string): Promise<ProjectWorkflowEntry[]> {
59
+ export async function scanWorkflowDir(dir: string): Promise<ProjectWorkflowEntry[]> {
60
60
  let dirents: Dirent[];
61
61
  try {
62
62
  dirents = await readdir(dir, { withFileTypes: true });
@@ -159,6 +159,30 @@ export function getDefaultStorageRoot(): string {
159
159
  return join(homedir(), ".uwf");
160
160
  }
161
161
 
162
+ /**
163
+ * Discover workflows from workflowPaths directories.
164
+ * Each directory is scanned directly for YAML files (like scanWorkflowDir).
165
+ * Earlier dirs in the list take priority on name collisions.
166
+ */
167
+ export async function discoverWorkflowPathsEntries(
168
+ dirs: ReadonlyArray<string>,
169
+ ): Promise<ProjectWorkflowEntry[]> {
170
+ const seen = new Set<string>();
171
+ const result: ProjectWorkflowEntry[] = [];
172
+
173
+ for (const dir of dirs) {
174
+ const entries = await scanWorkflowDir(dir);
175
+ for (const entry of entries) {
176
+ if (!seen.has(entry.name)) {
177
+ seen.add(entry.name);
178
+ result.push(entry);
179
+ }
180
+ }
181
+ }
182
+
183
+ return result;
184
+ }
185
+
162
186
  /**
163
187
  * Resolve storage root.
164
188
  * Priority: `UWF_HOME` → default.
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Per-command text renderers — the per-command registry from spec
3
+ * `cli-format-text-renderer-registry.md`.
4
+ *
5
+ * Each renderer accepts the command's payload (already mapped via the
6
+ * output-mappers module) and returns a human-readable string. Renderers must
7
+ * never throw on partial/missing data and must never return `undefined`.
8
+ *
9
+ * Distinct from the existing Liquid template registry in `format.ts`: this
10
+ * registry is plain JS functions (the spec contract is
11
+ * `Record<string, (data: unknown) => string>`). The Liquid templates remain
12
+ * the primary rendering path inside `writeEnvelope`; these renderers are the
13
+ * fallback contract surface so callers can resolve `text` rendering without
14
+ * needing access to a CAS store.
15
+ */
16
+
17
+ type ThreadListItem = {
18
+ threadId: string;
19
+ workflowHash: string;
20
+ workflowName: string | null;
21
+ status: string;
22
+ currentRole: string | null;
23
+ startedAt: number | null;
24
+ completedAt: number | null;
25
+ };
26
+
27
+ type ThreadListPayload = { items: ThreadListItem[] };
28
+
29
+ type ThreadStatusPayload = {
30
+ threadId: string;
31
+ workflowHash: string;
32
+ head: string | null;
33
+ status: string;
34
+ currentRole: string | null;
35
+ suspendedRole: string | null;
36
+ suspendMessage: string | null;
37
+ done: boolean;
38
+ };
39
+
40
+ type ThreadStartPayload = {
41
+ threadId: string;
42
+ workflowHash: string;
43
+ };
44
+
45
+ type WorkflowListItem = {
46
+ name: string;
47
+ hash: string;
48
+ source: string;
49
+ description: string;
50
+ };
51
+
52
+ type WorkflowListPayload = { items: WorkflowListItem[] };
53
+
54
+ type WorkflowDetailPayload = {
55
+ name: string;
56
+ hash: string;
57
+ version: number;
58
+ description: string;
59
+ roles: Record<string, { description: string; goal: string }>;
60
+ graph: Record<string, Record<string, { role: string; prompt: string }>>;
61
+ };
62
+
63
+ type StepListItem = {
64
+ hash: string;
65
+ role: string;
66
+ durationMs: number | null;
67
+ };
68
+
69
+ type StepListPayload = {
70
+ threadId: string;
71
+ items: StepListItem[];
72
+ };
73
+
74
+ type ThreadCancelPayload = {
75
+ thread: string;
76
+ cancelled: boolean;
77
+ };
78
+
79
+ type ThreadStopPayload = {
80
+ thread: string;
81
+ stopped: boolean;
82
+ };
83
+
84
+ type StepDetailPayload = {
85
+ hash: string;
86
+ role: string;
87
+ agent: string;
88
+ status: string;
89
+ startedAtMs: number | null;
90
+ completedAtMs: number | null;
91
+ durationMs: number | null;
92
+ frontmatter: Record<string, unknown>;
93
+ turns: Array<{ role: string; content: string; timestamp: number | null }>;
94
+ };
95
+
96
+ function asObject(data: unknown): Record<string, unknown> {
97
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
98
+ return data as Record<string, unknown>;
99
+ }
100
+ return {};
101
+ }
102
+
103
+ function asString(value: unknown, fallback = "-"): string {
104
+ if (typeof value === "string" && value.length > 0) return value;
105
+ return fallback;
106
+ }
107
+
108
+ function asArray(value: unknown): unknown[] {
109
+ return Array.isArray(value) ? value : [];
110
+ }
111
+
112
+ function formatDuration(durationMs: unknown): string {
113
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) return "-";
114
+ if (durationMs >= 1000) return `${(durationMs / 1000).toFixed(1)}s`;
115
+ return `${durationMs}ms`;
116
+ }
117
+
118
+ function pad(s: string, width: number): string {
119
+ if (s.length >= width) return s.slice(0, width);
120
+ return s + " ".repeat(width - s.length);
121
+ }
122
+
123
+ function formatTimestamp(ts: unknown): string {
124
+ if (typeof ts !== "number" || !Number.isFinite(ts)) return "-";
125
+ const d = new Date(ts);
126
+ if (Number.isNaN(d.getTime())) return "-";
127
+ const pad2 = (n: number): string => n.toString().padStart(2, "0");
128
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
129
+ }
130
+
131
+ export function renderThreadList(data: unknown): string {
132
+ const payload = asObject(data) as Partial<ThreadListPayload>;
133
+ const items = asArray(payload.items) as ThreadListItem[];
134
+ const lines: string[] = [
135
+ "THREAD WORKFLOW STATUS ROLE STARTED",
136
+ ];
137
+ for (const item of items) {
138
+ const it = asObject(item);
139
+ const threadId = asString(it.threadId);
140
+ const workflowHash = asString(it.workflowHash);
141
+ const status = pad(asString(it.status), 9);
142
+ const role = pad(asString(it.currentRole), 10);
143
+ const started = formatTimestamp(it.startedAt);
144
+ lines.push(`${threadId} ${workflowHash} ${status} ${role} ${started}`);
145
+ }
146
+ return lines.join("\n");
147
+ }
148
+
149
+ export function renderThreadShow(data: unknown): string {
150
+ const p = asObject(data) as Partial<ThreadStatusPayload>;
151
+ const status = asString(p.status);
152
+ const role =
153
+ status === "suspended" && typeof p.suspendedRole === "string" && p.suspendedRole.length > 0
154
+ ? p.suspendedRole
155
+ : asString(p.currentRole);
156
+ const head = asString(p.head);
157
+ const lines = [
158
+ `Thread ${asString(p.threadId)}`,
159
+ `Workflow ${asString(p.workflowHash)}`,
160
+ `Status ${status}`,
161
+ `Role ${role}`,
162
+ `Head ${head}`,
163
+ ];
164
+ if (
165
+ status === "suspended" &&
166
+ typeof p.suspendMessage === "string" &&
167
+ p.suspendMessage.length > 0
168
+ ) {
169
+ lines.push(`Suspend ${p.suspendMessage}`);
170
+ }
171
+ return lines.join("\n");
172
+ }
173
+
174
+ export function renderThreadStart(data: unknown): string {
175
+ const p = asObject(data) as Partial<ThreadStartPayload>;
176
+ return `Thread ${asString(p.threadId)}\nWorkflow ${asString(p.workflowHash)}`;
177
+ }
178
+
179
+ export function renderWorkflowList(data: unknown): string {
180
+ const payload = asObject(data) as Partial<WorkflowListPayload>;
181
+ const items = asArray(payload.items) as WorkflowListItem[];
182
+ const lines: string[] = ["NAME HASH SOURCE DESCRIPTION"];
183
+ for (const item of items) {
184
+ const it = asObject(item);
185
+ const name = pad(asString(it.name), 13);
186
+ const hash = asString(it.hash);
187
+ const source = pad(asString(it.source), 10);
188
+ const description = asString(it.description, "");
189
+ lines.push(`${name} ${hash} ${source} ${description}`);
190
+ }
191
+ return lines.join("\n");
192
+ }
193
+
194
+ export function renderWorkflowShow(data: unknown): string {
195
+ const p = asObject(data) as Partial<WorkflowDetailPayload>;
196
+ const roles =
197
+ p.roles !== null && typeof p.roles === "object" && !Array.isArray(p.roles)
198
+ ? Object.keys(p.roles)
199
+ : [];
200
+ const lines = [
201
+ `Workflow ${asString(p.name)}`,
202
+ `Version ${typeof p.version === "number" ? p.version : "-"}`,
203
+ `Hash ${asString(p.hash)}`,
204
+ `Roles ${roles.join(", ")}`,
205
+ ];
206
+ if (typeof p.description === "string" && p.description.length > 0) {
207
+ lines.push(`Description ${p.description}`);
208
+ }
209
+ return lines.join("\n");
210
+ }
211
+
212
+ export function renderStepList(data: unknown): string {
213
+ const payload = asObject(data) as Partial<StepListPayload>;
214
+ const items = asArray(payload.items) as StepListItem[];
215
+ const lines: string[] = ["HASH ROLE DURATION"];
216
+ for (const item of items) {
217
+ const it = asObject(item);
218
+ const hash = asString(it.hash);
219
+ const role = pad(asString(it.role), 10);
220
+ const dur = formatDuration(it.durationMs);
221
+ lines.push(`${hash} ${role} ${dur}`);
222
+ }
223
+ return lines.join("\n");
224
+ }
225
+
226
+ export function renderStepShow(data: unknown): string {
227
+ const p = asObject(data) as Partial<StepDetailPayload>;
228
+ return [
229
+ `Step ${asString(p.hash)}`,
230
+ `Role ${asString(p.role)}`,
231
+ `Agent ${asString(p.agent)}`,
232
+ `Status ${asString(p.status)}`,
233
+ `Duration ${formatDuration(p.durationMs)}`,
234
+ ].join("\n");
235
+ }
236
+
237
+ export function renderThreadCancel(data: unknown): string {
238
+ const p = asObject(data) as Partial<ThreadCancelPayload>;
239
+ const cancelled = typeof p.cancelled === "boolean" ? (p.cancelled ? "yes" : "no") : "-";
240
+ return [
241
+ `Thread ${asString(p.thread)}`,
242
+ `Status cancelled`,
243
+ `Cancelled ${cancelled}`,
244
+ ].join("\n");
245
+ }
246
+
247
+ export function renderThreadStop(data: unknown): string {
248
+ const p = asObject(data) as Partial<ThreadStopPayload>;
249
+ const stopped = typeof p.stopped === "boolean" ? (p.stopped ? "yes" : "no") : "-";
250
+ return [`Thread ${asString(p.thread)}`, `Stopped ${stopped}`].join("\n");
251
+ }
252
+
253
+ // ── Config renderers ────────────────────────────────────────────────
254
+
255
+ /**
256
+ * Flatten a nested object into dot-notation key-value lines.
257
+ * Arrays are rendered as compact JSON; scalars as strings.
258
+ */
259
+ function flattenConfig(obj: Record<string, unknown>, prefix: string): string[] {
260
+ const lines: string[] = [];
261
+ for (const [key, value] of Object.entries(obj)) {
262
+ const fullKey = prefix ? `${prefix}.${key}` : key;
263
+ if (Array.isArray(value)) {
264
+ lines.push(`${fullKey}\t${JSON.stringify(value)}`);
265
+ } else if (value !== null && typeof value === "object") {
266
+ lines.push(...flattenConfig(value as Record<string, unknown>, fullKey));
267
+ } else {
268
+ lines.push(`${fullKey}\t${String(value)}`);
269
+ }
270
+ }
271
+ return lines;
272
+ }
273
+
274
+ export function renderConfigList(data: unknown): string {
275
+ const obj = asObject(data);
276
+ if (Object.keys(obj).length === 0) return "";
277
+ return flattenConfig(obj as Record<string, unknown>, "").join("\n");
278
+ }
279
+
280
+ export function renderConfigGet(data: unknown): string {
281
+ const obj = asObject(data) as Record<string, unknown>;
282
+ const value = obj.value;
283
+ if (value === null || value === undefined) return "";
284
+ if (typeof value === "object" && !Array.isArray(value)) {
285
+ return flattenConfig(value as Record<string, unknown>, "").join("\n");
286
+ }
287
+ if (Array.isArray(value)) return JSON.stringify(value);
288
+ return String(value);
289
+ }
290
+
291
+ export function renderConfigSet(data: unknown): string {
292
+ const obj = asObject(data) as Record<string, unknown>;
293
+ const key = asString(obj.key as string | undefined);
294
+ const value = obj.value;
295
+ const rendered = Array.isArray(value) ? JSON.stringify(value) : String(value ?? "");
296
+ return `${key} = ${rendered}`;
297
+ }
298
+
299
+ // ── Log renderers ───────────────────────────────────────────────────
300
+
301
+ type LogListItem = {
302
+ name: string;
303
+ size: number;
304
+ date: string;
305
+ };
306
+
307
+ type LogEntry = {
308
+ ts: string;
309
+ pid: string;
310
+ tag: string;
311
+ msg: string;
312
+ thread: string | null;
313
+ workflow: string | null;
314
+ };
315
+
316
+ function formatSize(bytes: unknown): string {
317
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return "-";
318
+ if (bytes < 1024) return `${bytes}B`;
319
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
320
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
321
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
322
+ }
323
+
324
+ export function renderLogList(data: unknown): string {
325
+ const items = asArray(data) as LogListItem[];
326
+ if (items.length === 0) return "No log files.";
327
+ const lines: string[] = ["DATE SIZE NAME"];
328
+ for (const item of items) {
329
+ const it = asObject(item);
330
+ const date = pad(asString(it.date), 11);
331
+ const size = pad(formatSize(it.size), 9);
332
+ const name = asString(it.name);
333
+ lines.push(`${date} ${size} ${name}`);
334
+ }
335
+ return lines.join("\n");
336
+ }
337
+
338
+ export function renderLogShow(data: unknown): string {
339
+ const items = asArray(data) as LogEntry[];
340
+ if (items.length === 0) return "No log entries.";
341
+ const lines: string[] = [];
342
+ for (const item of items) {
343
+ const it = asObject(item);
344
+ const ts = asString(it.ts);
345
+ const pid = asString(it.pid);
346
+ const tag = asString(it.tag);
347
+ const msg = asString(it.msg, "");
348
+ const thread = typeof it.thread === "string" && it.thread.length > 0 ? it.thread : null;
349
+ const parts = [ts, `pid=${pid}`, tag];
350
+ if (thread !== null) parts.push(`thread=${thread}`);
351
+ parts.push(msg);
352
+ lines.push(parts.join(" "));
353
+ }
354
+ return lines.join("\n");
355
+ }