@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
@@ -0,0 +1,370 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
5
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
6
+ import { stringify } from "yaml";
7
+ import { cmdThreadStart } from "../commands/thread.js";
8
+ import { cmdWorkflowList } from "../commands/workflow.js";
9
+ import type { UwfStore } from "../store.js";
10
+ import { createUwfStore, discoverProjectWorkflows } from "../store.js";
11
+
12
+ // ── helpers ───────────────────────────────────────────────────────────────────
13
+
14
+ async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
15
+ const casDir = join(storageRoot, "cas");
16
+ await mkdir(casDir, { recursive: true });
17
+ process.env.OCAS_HOME = casDir;
18
+ return createUwfStore(storageRoot);
19
+ }
20
+
21
+ function makeMinimalPayload(name: string, description: string): WorkflowPayload {
22
+ return {
23
+ version: 1,
24
+ name,
25
+ description,
26
+ roles: {
27
+ worker: {
28
+ description: "worker role",
29
+ goal: "do work",
30
+ capabilities: [],
31
+ procedure: "",
32
+ output: "",
33
+ frontmatter: {
34
+ type: "object",
35
+ properties: {
36
+ $status: { const: "done" },
37
+ },
38
+ required: ["$status"],
39
+ } as unknown as CasRef,
40
+ },
41
+ },
42
+ graph: {
43
+ $START: {
44
+ new: { role: "worker", prompt: "start working", location: null },
45
+ resume: { role: "worker", prompt: "resume working", location: null },
46
+ },
47
+ worker: { done: { role: "$END", prompt: "done", location: null } },
48
+ },
49
+ };
50
+ }
51
+
52
+ async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> {
53
+ const payload = makeMinimalPayload(
54
+ name,
55
+ version !== null ? `Test workflow (${version})` : "Test workflow",
56
+ );
57
+ return stringify(payload);
58
+ }
59
+
60
+ // ── fixture ───────────────────────────────────────────────────────────────────
61
+
62
+ let tmpDir: string;
63
+ let storageRoot: string;
64
+ let projectRoot: string;
65
+
66
+ beforeEach(async () => {
67
+ tmpDir = await mkdtemp(join(tmpdir(), "uwf-wf-list-recursive-"));
68
+ storageRoot = join(tmpDir, "storage");
69
+ projectRoot = join(tmpDir, "project");
70
+ await mkdir(storageRoot, { recursive: true });
71
+ await mkdir(projectRoot, { recursive: true });
72
+ });
73
+
74
+ afterEach(async () => {
75
+ await rm(tmpDir, { recursive: true, force: true });
76
+ });
77
+
78
+ // ── discoverProjectWorkflows — parent traversal ───────────────────────────────
79
+
80
+ describe("discoverProjectWorkflows — parent traversal", () => {
81
+ test("T1: finds workflows in cwd's .workflows/", async () => {
82
+ const wfDir = join(projectRoot, ".workflows");
83
+ await mkdir(wfDir, { recursive: true });
84
+ await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
85
+
86
+ const entries = await discoverProjectWorkflows(projectRoot);
87
+
88
+ expect(entries.map((e) => e.name)).toContain("solve-issue");
89
+ });
90
+
91
+ test("T2: finds workflows in ancestor's .workflows/ when called from subdirectory", async () => {
92
+ const wfDir = join(projectRoot, ".workflows");
93
+ await mkdir(wfDir, { recursive: true });
94
+ await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
95
+
96
+ const subdir = join(projectRoot, "packages", "cli", "src");
97
+ await mkdir(subdir, { recursive: true });
98
+
99
+ const entries = await discoverProjectWorkflows(subdir);
100
+
101
+ expect(entries.map((e) => e.name)).toContain("solve-issue");
102
+ });
103
+
104
+ test("T3: returns [] when no .workflows/ or .workflow/ exists in any ancestor", async () => {
105
+ // Use a deep path under tmpDir that has no .workflows/ or .workflow/ on the way up.
106
+ // (Traversal will stop at filesystem root and find nothing.)
107
+ const deepPath = join(tmpDir, "isolated", "no", "workflow", "here");
108
+ await mkdir(deepPath, { recursive: true });
109
+
110
+ const entries = await discoverProjectWorkflows(deepPath);
111
+
112
+ expect(entries).toEqual([]);
113
+ });
114
+
115
+ test("T4: .workflows/ entries win over .workflow/ within the same directory", async () => {
116
+ const primaryDir = join(projectRoot, ".workflows");
117
+ const legacyDir = join(projectRoot, ".workflow");
118
+ await mkdir(primaryDir, { recursive: true });
119
+ await mkdir(legacyDir, { recursive: true });
120
+
121
+ await writeFile(
122
+ join(primaryDir, "solve-issue.yaml"),
123
+ await createWorkflowYaml("solve-issue", "new"),
124
+ );
125
+ await writeFile(
126
+ join(legacyDir, "solve-issue.yaml"),
127
+ await createWorkflowYaml("solve-issue", "legacy"),
128
+ );
129
+
130
+ const entries = await discoverProjectWorkflows(projectRoot);
131
+
132
+ const match = entries.find((e) => e.name === "solve-issue");
133
+ expect(match).toBeDefined();
134
+ expect(match?.filePath).toBe(join(primaryDir, "solve-issue.yaml"));
135
+ });
136
+
137
+ test("T5: nearest .workflows/ wins over ancestor's .workflows/", async () => {
138
+ const ancestorWf = join(projectRoot, ".workflows");
139
+ await mkdir(ancestorWf, { recursive: true });
140
+ await writeFile(join(ancestorWf, "foo.yaml"), await createWorkflowYaml("foo", "ancestor"));
141
+
142
+ const nearDir = join(projectRoot, "pkg");
143
+ const nearWf = join(nearDir, ".workflows");
144
+ await mkdir(nearWf, { recursive: true });
145
+ await writeFile(join(nearWf, "foo.yaml"), await createWorkflowYaml("foo", "near"));
146
+
147
+ const entries = await discoverProjectWorkflows(nearDir);
148
+
149
+ const match = entries.find((e) => e.name === "foo");
150
+ expect(match).toBeDefined();
151
+ expect(match?.filePath).toBe(join(nearWf, "foo.yaml"));
152
+ // Should not include duplicates from ancestor
153
+ expect(entries.filter((e) => e.name === "foo")).toHaveLength(1);
154
+ });
155
+
156
+ test("T6: returns all entries from the nearest .workflows/ when called from a deep subdir", async () => {
157
+ const wfDir = join(projectRoot, ".workflows");
158
+ await mkdir(wfDir, { recursive: true });
159
+ await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
160
+ await writeFile(join(wfDir, "review-code.yaml"), await createWorkflowYaml("review-code"));
161
+
162
+ const deep = join(projectRoot, "a", "b", "c", "d");
163
+ await mkdir(deep, { recursive: true });
164
+
165
+ const entries = await discoverProjectWorkflows(deep);
166
+
167
+ const names = entries.map((e) => e.name).sort();
168
+ expect(names).toEqual(["review-code", "solve-issue"]);
169
+ });
170
+
171
+ test("T7: discovers folder-based layout (name/index.yaml) via parent traversal under .workflows/", async () => {
172
+ const folderDir = join(projectRoot, ".workflows", "solve-issue");
173
+ await mkdir(folderDir, { recursive: true });
174
+ await writeFile(join(folderDir, "index.yaml"), await createWorkflowYaml("solve-issue"));
175
+
176
+ const subdir = join(projectRoot, "deep", "sub");
177
+ await mkdir(subdir, { recursive: true });
178
+
179
+ const entries = await discoverProjectWorkflows(subdir);
180
+
181
+ const match = entries.find((e) => e.name === "solve-issue");
182
+ expect(match).toBeDefined();
183
+ expect(match?.filePath).toBe(join(folderDir, "index.yaml"));
184
+ });
185
+
186
+ test("T8: .workflow/ (legacy) is still discovered when .workflows/ does not exist", async () => {
187
+ const legacyDir = join(projectRoot, ".workflow");
188
+ await mkdir(legacyDir, { recursive: true });
189
+ await writeFile(join(legacyDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
190
+
191
+ const entries = await discoverProjectWorkflows(projectRoot);
192
+
193
+ const match = entries.find((e) => e.name === "solve-issue");
194
+ expect(match).toBeDefined();
195
+ expect(match?.filePath).toBe(join(legacyDir, "solve-issue.yaml"));
196
+ });
197
+
198
+ test("T9: nearest directory with EITHER variant stops traversal", async () => {
199
+ // Setup: ancestor .workflows/ + near .workflow/ only — near wins, ancestor not merged.
200
+ const ancestorWf = join(tmpDir, ".workflows");
201
+ await mkdir(ancestorWf, { recursive: true });
202
+ await writeFile(join(ancestorWf, "leak.yaml"), await createWorkflowYaml("leak"));
203
+
204
+ const nearLegacyDir = join(projectRoot, ".workflow");
205
+ await mkdir(nearLegacyDir, { recursive: true });
206
+ await writeFile(join(nearLegacyDir, "local.yaml"), await createWorkflowYaml("local"));
207
+
208
+ const entries = await discoverProjectWorkflows(projectRoot);
209
+ const names = entries.map((e) => e.name);
210
+ expect(names).toContain("local");
211
+ expect(names).not.toContain("leak");
212
+ });
213
+ });
214
+
215
+ // ── discoverProjectWorkflows — .git boundary ─────────────────────────────────
216
+
217
+ describe("discoverProjectWorkflows — .git boundary", () => {
218
+ test("G1: .git directory stops traversal", async () => {
219
+ // Setup: tmpDir/repo/.git/ (dir), tmpDir/.workflows/leak.yaml, start from tmpDir/repo/sub/deep/
220
+ const repoDir = join(tmpDir, "repo");
221
+ const gitDir = join(repoDir, ".git");
222
+ await mkdir(gitDir, { recursive: true });
223
+
224
+ // Workflow above repo root — should NOT be reachable
225
+ const leakDir = join(tmpDir, ".workflows");
226
+ await mkdir(leakDir, { recursive: true });
227
+ await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
228
+
229
+ const startFrom = join(repoDir, "sub", "deep");
230
+ await mkdir(startFrom, { recursive: true });
231
+
232
+ const entries = await discoverProjectWorkflows(startFrom);
233
+ expect(entries).toEqual([]);
234
+ });
235
+
236
+ test("G2: .git file (worktree) stops traversal", async () => {
237
+ // Setup: tmpDir/repo/.git as a FILE, tmpDir/.workflows/leak.yaml, start from tmpDir/repo/pkg/
238
+ const repoDir = join(tmpDir, "repo");
239
+ await mkdir(repoDir, { recursive: true });
240
+ await writeFile(join(repoDir, ".git"), "gitdir: /some/other/path/.git/worktrees/repo");
241
+
242
+ const leakDir = join(tmpDir, ".workflows");
243
+ await mkdir(leakDir, { recursive: true });
244
+ await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
245
+
246
+ const startFrom = join(repoDir, "pkg");
247
+ await mkdir(startFrom, { recursive: true });
248
+
249
+ const entries = await discoverProjectWorkflows(startFrom);
250
+ expect(entries).toEqual([]);
251
+ });
252
+
253
+ test("G3: workflow at .git boundary IS found (primary .workflows/)", async () => {
254
+ // Setup: tmpDir/repo/.git/ (dir), tmpDir/repo/.workflows/local.yaml, start from tmpDir/repo/sub/
255
+ const repoDir = join(tmpDir, "repo");
256
+ const gitDir = join(repoDir, ".git");
257
+ await mkdir(gitDir, { recursive: true });
258
+
259
+ const wfDir = join(repoDir, ".workflows");
260
+ await mkdir(wfDir, { recursive: true });
261
+ await writeFile(join(wfDir, "local.yaml"), await createWorkflowYaml("local"));
262
+
263
+ const startFrom = join(repoDir, "sub");
264
+ await mkdir(startFrom, { recursive: true });
265
+
266
+ const entries = await discoverProjectWorkflows(startFrom);
267
+ expect(entries.map((e) => e.name)).toContain("local");
268
+ });
269
+
270
+ test("G4: workflow below .git is found, above is not", async () => {
271
+ // Setup: tmpDir/repo/.git/ + tmpDir/repo/.workflows/local.yaml + tmpDir/.workflows/leak.yaml
272
+ const repoDir = join(tmpDir, "repo");
273
+ const gitDir = join(repoDir, ".git");
274
+ await mkdir(gitDir, { recursive: true });
275
+
276
+ const localWfDir = join(repoDir, ".workflows");
277
+ await mkdir(localWfDir, { recursive: true });
278
+ await writeFile(join(localWfDir, "local.yaml"), await createWorkflowYaml("local"));
279
+
280
+ const leakDir = join(tmpDir, ".workflows");
281
+ await mkdir(leakDir, { recursive: true });
282
+ await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
283
+
284
+ const startFrom = join(repoDir, "sub");
285
+ await mkdir(startFrom, { recursive: true });
286
+
287
+ const entries = await discoverProjectWorkflows(startFrom);
288
+ expect(entries.map((e) => e.name)).toEqual(["local"]);
289
+ });
290
+ });
291
+
292
+ // ── findWorkflowInParents (via cmdThreadStart) — .git boundary ───────────────
293
+
294
+ describe("findWorkflowInParents via cmdThreadStart — .git boundary", () => {
295
+ test("G5: .git stops traversal — workflow above boundary is not found", async () => {
296
+ await makeUwfStore(storageRoot);
297
+ const repoDir = join(tmpDir, "repo");
298
+ const gitDir = join(repoDir, ".git");
299
+ await mkdir(gitDir, { recursive: true });
300
+
301
+ // Workflow above .git boundary
302
+ const leakDir = join(tmpDir, ".workflows");
303
+ await mkdir(leakDir, { recursive: true });
304
+ await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
305
+
306
+ const startFrom = join(repoDir, "sub");
307
+ await mkdir(startFrom, { recursive: true });
308
+
309
+ // cmdThreadStart should fail — "leak" is above the .git boundary
310
+ await expect(cmdThreadStart(storageRoot, "leak", "prompt", startFrom)).rejects.toThrow();
311
+ });
312
+
313
+ test("G6: workflow at .git boundary IS found via cmdThreadStart", async () => {
314
+ await makeUwfStore(storageRoot);
315
+ const repoDir = join(tmpDir, "repo");
316
+ const gitDir = join(repoDir, ".git");
317
+ await mkdir(gitDir, { recursive: true });
318
+
319
+ const wfDir = join(repoDir, ".workflows");
320
+ await mkdir(wfDir, { recursive: true });
321
+ await writeFile(join(wfDir, "local.yaml"), await createWorkflowYaml("local"));
322
+
323
+ const startFrom = join(repoDir, "sub");
324
+ await mkdir(startFrom, { recursive: true });
325
+
326
+ const result = await cmdThreadStart(storageRoot, "local", "prompt", startFrom);
327
+ expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
328
+ });
329
+ });
330
+
331
+ // ── cmdWorkflowList — parent traversal ───────────────────────────────────────
332
+
333
+ describe("cmdWorkflowList — parent traversal", () => {
334
+ test("B9: lists local workflows discovered from a subdirectory", async () => {
335
+ await makeUwfStore(storageRoot);
336
+ const wfDir = join(projectRoot, ".workflows");
337
+ await mkdir(wfDir, { recursive: true });
338
+ await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
339
+
340
+ const subdir = join(projectRoot, "packages", "foo", "src");
341
+ await mkdir(subdir, { recursive: true });
342
+
343
+ const result = await cmdWorkflowList(storageRoot, subdir);
344
+
345
+ const match = result.find((e) => e.name === "solve-issue");
346
+ expect(match).toBeDefined();
347
+ expect(match?.hash).toBe("(local)");
348
+ expect(match?.origin).toBe("local");
349
+ });
350
+
351
+ test("aligns with cmdThreadStart discovery from same subdirectory", async () => {
352
+ await makeUwfStore(storageRoot);
353
+ const wfDir = join(projectRoot, ".workflows");
354
+ await mkdir(wfDir, { recursive: true });
355
+ await writeFile(join(wfDir, "foo.yaml"), await createWorkflowYaml("foo"));
356
+
357
+ const subdir = join(projectRoot, "packages", "foo", "src");
358
+ await mkdir(subdir, { recursive: true });
359
+
360
+ // cmdThreadStart already resolves foo successfully from subdir (existing behavior)
361
+ const startResult = await cmdThreadStart(storageRoot, "foo", "prompt", subdir);
362
+ expect(startResult.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
363
+
364
+ // cmdWorkflowList must ALSO include foo (newly aligned behavior)
365
+ const listResult = await cmdWorkflowList(storageRoot, subdir);
366
+ const match = listResult.find((e) => e.name === "foo");
367
+ expect(match).toBeDefined();
368
+ expect(match?.origin).toBe("local");
369
+ });
370
+ });
@@ -19,6 +19,7 @@ async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
19
19
 
20
20
  function makeMinimalPayload(name: string, description: string): WorkflowPayload {
21
21
  return {
22
+ version: 1,
22
23
  name,
23
24
  description,
24
25
  roles: {
@@ -180,9 +181,9 @@ describe("Strategy 2: File Path Resolution", () => {
180
181
  // ── Strategy 3: Local Discovery (Parent Traversal) ────────────────────────────
181
182
 
182
183
  describe("Strategy 3: Local Discovery", () => {
183
- test("should find workflow in current directory .workflow/", async () => {
184
+ test("should find workflow in current directory .workflows/", async () => {
184
185
  await makeUwfStore(storageRoot);
185
- const workflowDir = join(projectRoot, ".workflow");
186
+ const workflowDir = join(projectRoot, ".workflows");
186
187
  await mkdir(workflowDir, { recursive: true });
187
188
  await writeFile(join(workflowDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
188
189
 
@@ -197,9 +198,9 @@ describe("Strategy 3: Local Discovery", () => {
197
198
  }
198
199
  });
199
200
 
200
- test("should find workflow in parent directory .workflow/", async () => {
201
+ test("should find workflow in parent directory .workflows/", async () => {
201
202
  await makeUwfStore(storageRoot);
202
- const workflowDir = join(projectRoot, ".workflow");
203
+ const workflowDir = join(projectRoot, ".workflows");
203
204
  await mkdir(workflowDir, { recursive: true });
204
205
  await writeFile(join(workflowDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
205
206
 
@@ -219,19 +220,19 @@ describe("Strategy 3: Local Discovery", () => {
219
220
  await expect(cmdThreadStart(storageRoot, "nonexistent", "prompt", deepPath)).rejects.toThrow();
220
221
  });
221
222
 
222
- test("should prefer .workflow/ over .workflows/ directory", async () => {
223
+ test("should prefer .workflows/ over .workflow/ directory", async () => {
223
224
  await makeUwfStore(storageRoot);
224
- const workflowDir = join(projectRoot, ".workflow");
225
- const workflowsDir = join(projectRoot, ".workflows");
226
- await mkdir(workflowDir, { recursive: true });
227
- await mkdir(workflowsDir, { recursive: true });
225
+ const primaryDir = join(projectRoot, ".workflows");
226
+ const legacyDir = join(projectRoot, ".workflow");
227
+ await mkdir(primaryDir, { recursive: true });
228
+ await mkdir(legacyDir, { recursive: true });
228
229
 
229
230
  await writeFile(
230
- join(workflowDir, "solve-issue.yaml"),
231
+ join(primaryDir, "solve-issue.yaml"),
231
232
  await createWorkflowYaml("solve-issue", "1"),
232
233
  );
233
234
  await writeFile(
234
- join(workflowsDir, "solve-issue.yaml"),
235
+ join(legacyDir, "solve-issue.yaml"),
235
236
  await createWorkflowYaml("solve-issue", "2"),
236
237
  );
237
238
 
@@ -245,9 +246,9 @@ describe("Strategy 3: Local Discovery", () => {
245
246
  }
246
247
  });
247
248
 
248
- test("should support .yml extension in local discovery", async () => {
249
+ test("should support .yml extension in local discovery under .workflows/", async () => {
249
250
  await makeUwfStore(storageRoot);
250
- const workflowDir = join(projectRoot, ".workflow");
251
+ const workflowDir = join(projectRoot, ".workflows");
251
252
  await mkdir(workflowDir, { recursive: true });
252
253
  await writeFile(join(workflowDir, "solve-issue.yml"), await createWorkflowYaml("solve-issue"));
253
254
 
@@ -256,9 +257,9 @@ describe("Strategy 3: Local Discovery", () => {
256
257
  expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
257
258
  });
258
259
 
259
- test("should find workflow in folder-based layout (name/index.yaml)", async () => {
260
+ test("should find workflow in folder-based layout (.workflows/<name>/index.yaml)", async () => {
260
261
  await makeUwfStore(storageRoot);
261
- const workflowDir = join(projectRoot, ".workflow", "solve-issue");
262
+ const workflowDir = join(projectRoot, ".workflows", "solve-issue");
262
263
  await mkdir(workflowDir, { recursive: true });
263
264
  await writeFile(join(workflowDir, "index.yaml"), await createWorkflowYaml("solve-issue"));
264
265
 
@@ -273,9 +274,9 @@ describe("Strategy 3: Local Discovery", () => {
273
274
  }
274
275
  });
275
276
 
276
- test("should prefer flat file over folder-based layout", async () => {
277
+ test("should prefer flat file over folder-based layout under .workflows/", async () => {
277
278
  await makeUwfStore(storageRoot);
278
- const workflowDir = join(projectRoot, ".workflow");
279
+ const workflowDir = join(projectRoot, ".workflows");
279
280
  await mkdir(workflowDir, { recursive: true });
280
281
  await writeFile(
281
282
  join(workflowDir, "solve-issue.yaml"),
@@ -298,6 +299,23 @@ describe("Strategy 3: Local Discovery", () => {
298
299
  expect((node.payload as WorkflowPayload).description).toBe("Test workflow (flat)");
299
300
  }
300
301
  });
302
+
303
+ test("should resolve from legacy .workflow/ when .workflows/ is absent", async () => {
304
+ await makeUwfStore(storageRoot);
305
+ const legacyDir = join(projectRoot, ".workflow");
306
+ await mkdir(legacyDir, { recursive: true });
307
+ await writeFile(join(legacyDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
308
+
309
+ const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
310
+
311
+ expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
312
+ const uwf = await makeUwfStore(storageRoot);
313
+ const node = uwf.store.cas.get(result.workflow);
314
+ expect(node).not.toBeNull();
315
+ if (node !== null) {
316
+ expect((node.payload as WorkflowPayload).name).toBe("solve-issue");
317
+ }
318
+ });
301
319
  });
302
320
 
303
321
  // ── Strategy 4: Global Registry Fallback ──────────────────────────────────────
@@ -329,8 +347,8 @@ describe("Resolution Priority", () => {
329
347
  test("should use explicit file path over local discovery", async () => {
330
348
  await makeUwfStore(storageRoot);
331
349
 
332
- // Setup: Create workflow in .workflow/ AND as explicit file
333
- const workflowDir = join(projectRoot, ".workflow");
350
+ // Setup: Create workflow in .workflows/ AND as explicit file
351
+ const workflowDir = join(projectRoot, ".workflows");
334
352
  await mkdir(workflowDir, { recursive: true });
335
353
  await writeFile(
336
354
  join(workflowDir, "solve-issue.yaml"),
@@ -358,8 +376,8 @@ describe("Resolution Priority", () => {
358
376
  const globalHash = await storeWorkflow(uwf, "solve-issue");
359
377
  saveWorkflowRegistry(uwf.varStore, "solve-issue", globalHash);
360
378
 
361
- // Setup: Create local .workflow/
362
- const workflowDir = join(projectRoot, ".workflow");
379
+ // Setup: Create local .workflows/
380
+ const workflowDir = join(projectRoot, ".workflows");
363
381
  await mkdir(workflowDir, { recursive: true });
364
382
  const localYaml = await createWorkflowYaml("solve-issue", "local");
365
383
  await writeFile(join(workflowDir, "solve-issue.yaml"), localYaml);