@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4

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 (206) hide show
  1. package/docs/http-api.md +80 -4
  2. package/package.json +1 -4
  3. package/src/api/__tests__/index.test.ts +443 -32
  4. package/src/api/index.ts +108 -127
  5. package/src/cli/__tests__/analyze-task.test.ts +40 -7
  6. package/src/cli/__tests__/index.test.ts +337 -17
  7. package/src/cli/__tests__/types.test.ts +0 -18
  8. package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
  9. package/src/cli/analyze-task.ts +3 -14
  10. package/src/cli/index.ts +73 -61
  11. package/src/cli/types.ts +0 -13
  12. package/src/cli/update-pipeline-json.ts +3 -14
  13. package/src/config/__tests__/paths.test.ts +46 -1
  14. package/src/config/__tests__/pipeline-registry.test.ts +767 -0
  15. package/src/config/__tests__/sse-events.test.ts +470 -0
  16. package/src/config/__tests__/statuses.test.ts +41 -0
  17. package/src/config/paths.ts +11 -2
  18. package/src/config/pipeline-registry.ts +258 -0
  19. package/src/config/sse-events.ts +122 -0
  20. package/src/config/statuses.ts +20 -1
  21. package/src/core/__tests__/agent-step.test.ts +115 -0
  22. package/src/core/__tests__/config.test.ts +545 -105
  23. package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
  24. package/src/core/__tests__/job-concurrency.test.ts +260 -0
  25. package/src/core/__tests__/job-submission.test.ts +396 -0
  26. package/src/core/__tests__/job-view.test.ts +1341 -0
  27. package/src/core/__tests__/json-file.test.ts +111 -0
  28. package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
  29. package/src/core/__tests__/logger.test.ts +22 -0
  30. package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
  31. package/src/core/__tests__/orchestrator.test.ts +615 -2
  32. package/src/core/__tests__/pipeline-runner.test.ts +946 -9
  33. package/src/core/__tests__/redact.test.ts +153 -0
  34. package/src/core/__tests__/runner-liveness.test.ts +910 -0
  35. package/src/core/__tests__/seed-naming.test.ts +57 -0
  36. package/src/core/__tests__/single-derivation.test.ts +159 -0
  37. package/src/core/__tests__/task-runner.test.ts +594 -3
  38. package/src/core/__tests__/task-telemetry.test.ts +241 -0
  39. package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
  40. package/src/core/agent-step.ts +4 -1
  41. package/src/core/agent-types.ts +5 -4
  42. package/src/core/config.ts +134 -222
  43. package/src/core/control.ts +3 -0
  44. package/src/core/job-concurrency.ts +56 -20
  45. package/src/core/job-submission.ts +133 -0
  46. package/src/core/job-view.ts +473 -0
  47. package/src/core/json-file.ts +45 -0
  48. package/src/core/lifecycle-policy.ts +23 -8
  49. package/src/core/logger.ts +0 -29
  50. package/src/core/orchestrator.ts +200 -74
  51. package/src/core/pipeline-runner.ts +85 -53
  52. package/src/core/redact.ts +40 -0
  53. package/src/core/runner-liveness.ts +280 -0
  54. package/src/core/seed-naming.ts +9 -0
  55. package/src/core/status-writer.ts +27 -33
  56. package/src/core/task-runner.ts +356 -319
  57. package/src/core/task-telemetry.ts +107 -0
  58. package/src/harness/__tests__/subprocess.test.ts +112 -1
  59. package/src/harness/subprocess.ts +55 -16
  60. package/src/llm/__tests__/index.test.ts +684 -33
  61. package/src/llm/index.ts +310 -67
  62. package/src/providers/__tests__/alibaba.test.ts +67 -6
  63. package/src/providers/__tests__/anthropic.test.ts +35 -14
  64. package/src/providers/__tests__/base.test.ts +62 -0
  65. package/src/providers/__tests__/claude-code.test.ts +99 -14
  66. package/src/providers/__tests__/deepseek.test.ts +16 -6
  67. package/src/providers/__tests__/gemini.test.ts +47 -25
  68. package/src/providers/__tests__/moonshot.test.ts +27 -0
  69. package/src/providers/__tests__/openai.test.ts +262 -74
  70. package/src/providers/__tests__/opencode.test.ts +77 -0
  71. package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
  72. package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
  73. package/src/providers/__tests__/types.test.ts +85 -0
  74. package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
  75. package/src/providers/__tests__/zhipu.test.ts +27 -14
  76. package/src/providers/alibaba.ts +20 -39
  77. package/src/providers/anthropic.ts +23 -11
  78. package/src/providers/base.ts +19 -0
  79. package/src/providers/claude-code.ts +27 -18
  80. package/src/providers/deepseek.ts +9 -28
  81. package/src/providers/gemini.ts +20 -58
  82. package/src/providers/moonshot.ts +15 -11
  83. package/src/providers/openai.ts +79 -61
  84. package/src/providers/opencode.ts +16 -33
  85. package/src/providers/stream-accumulator.ts +27 -21
  86. package/src/providers/types.ts +29 -4
  87. package/src/providers/zhipu.ts +15 -44
  88. package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
  89. package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
  90. package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
  91. package/src/task-analysis/__tests__/repository.test.ts +65 -0
  92. package/src/task-analysis/__tests__/types.test.ts +63 -130
  93. package/src/task-analysis/analyzer.ts +91 -0
  94. package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
  95. package/src/task-analysis/index.ts +2 -36
  96. package/src/task-analysis/repository.ts +45 -0
  97. package/src/task-analysis/types.ts +42 -58
  98. package/src/ui/client/__tests__/api.test.ts +143 -1
  99. package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
  100. package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
  101. package/src/ui/client/__tests__/load-state.test.ts +70 -0
  102. package/src/ui/client/__tests__/types.test.ts +66 -3
  103. package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
  104. package/src/ui/client/__tests__/useJobList.test.ts +198 -23
  105. package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
  106. package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
  107. package/src/ui/client/adapters/job-adapter.ts +41 -16
  108. package/src/ui/client/api.ts +38 -15
  109. package/src/ui/client/bootstrap.ts +19 -14
  110. package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
  111. package/src/ui/client/hooks/useJobList.ts +26 -31
  112. package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
  113. package/src/ui/client/load-state.ts +20 -0
  114. package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
  115. package/src/ui/client/reducers/job-events.ts +137 -0
  116. package/src/ui/client/types.ts +16 -20
  117. package/src/ui/components/DAGGrid.tsx +6 -1
  118. package/src/ui/components/JobDetail.tsx +12 -4
  119. package/src/ui/components/JobTable.tsx +41 -13
  120. package/src/ui/components/StageTimeline.tsx +8 -20
  121. package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
  122. package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
  123. package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
  124. package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
  125. package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
  126. package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
  127. package/src/ui/components/types.ts +35 -15
  128. package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
  129. package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
  130. package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/pages/PipelineDetail.tsx +60 -4
  133. package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
  134. package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
  135. package/src/ui/pages/__tests__/pages.test.tsx +236 -42
  136. package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
  137. package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
  138. package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
  139. package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
  140. package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
  141. package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
  142. package/src/ui/server/__tests__/index.test.ts +63 -0
  143. package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
  144. package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
  145. package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
  146. package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
  147. package/src/ui/server/__tests__/router-threading.test.ts +148 -0
  148. package/src/ui/server/__tests__/router.test.ts +104 -0
  149. package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
  150. package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
  151. package/src/ui/server/config-bridge-node.ts +8 -26
  152. package/src/ui/server/config-bridge.ts +3 -4
  153. package/src/ui/server/embedded-assets-imports.d.ts +44 -0
  154. package/src/ui/server/embedded-assets.ts +13 -2
  155. package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
  156. package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
  157. package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
  158. package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
  159. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
  160. package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
  161. package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
  162. package/src/ui/server/endpoints/file-endpoints.ts +15 -10
  163. package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
  164. package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
  165. package/src/ui/server/endpoints/job-endpoints.ts +7 -0
  166. package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
  167. package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
  168. package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
  169. package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
  170. package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
  171. package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
  172. package/src/ui/server/index.ts +19 -14
  173. package/src/ui/server/job-reader.ts +14 -2
  174. package/src/ui/server/router.ts +33 -10
  175. package/src/ui/server/sse-broadcast.ts +14 -3
  176. package/src/ui/server/sse-enhancer.ts +12 -2
  177. package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
  178. package/src/ui/state/__tests__/snapshot.test.ts +120 -14
  179. package/src/ui/state/__tests__/types.test.ts +104 -5
  180. package/src/ui/state/snapshot.ts +2 -2
  181. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
  182. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
  183. package/src/ui/state/transformers/list-transformer.ts +13 -3
  184. package/src/ui/state/transformers/status-transformer.ts +36 -170
  185. package/src/ui/state/types.ts +15 -48
  186. package/src/utils/__tests__/path-containment.test.ts +160 -0
  187. package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
  188. package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
  189. package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
  190. package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
  191. package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
  192. package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
  193. package/src/task-analysis/__tests__/index.test.ts +0 -143
  194. package/src/task-analysis/__tests__/parser.test.ts +0 -41
  195. package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
  196. package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
  197. package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
  198. package/src/task-analysis/extractors/artifacts.ts +0 -143
  199. package/src/task-analysis/extractors/llm-calls.ts +0 -117
  200. package/src/task-analysis/extractors/stages.ts +0 -45
  201. package/src/task-analysis/parser.ts +0 -20
  202. package/src/task-analysis/utils/ast.ts +0 -45
  203. package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
  204. package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
  205. package/src/ui/embedded-assets.js +0 -12
  206. package/src/ui/server/__tests__/path-containment.test.ts +0 -54
package/src/cli/index.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Command } from "commander";
3
3
  import { mkdir, access } from "node:fs/promises";
4
- import { resolve, join } from "node:path";
4
+ import { join } from "node:path";
5
5
 
6
6
  import { STAGE_NAMES, getStagePurpose, KEBAB_CASE_REGEX } from "./constants.ts";
7
7
  import { buildReexecArgs, isCompiledBinary } from "./self-reexec.ts";
8
8
  import { updatePipelineJson } from "./update-pipeline-json.ts";
9
9
  import { analyzeTaskFile } from "./analyze-task.ts";
10
10
  import { submitJobWithValidation, PipelineOrchestrator } from "../api/index.ts";
11
- import type { Registry } from "./types.ts";
11
+ import { resolveWorkspaceRoot } from "../config/paths.ts";
12
+ import {
13
+ writeRegistry,
14
+ registerPipeline,
15
+ REGISTRY_VERSION,
16
+ } from "../config/pipeline-registry.ts";
12
17
  import pkg from "../../package.json";
13
18
 
14
19
  // ─── init ─────────────────────────────────────────────────────────────────────
@@ -21,10 +26,7 @@ export async function handleInit(root: string): Promise<void> {
21
26
  await mkdir(join(root, "pipeline-data", sub), { recursive: true });
22
27
  await Bun.write(join(root, "pipeline-data", sub, ".gitkeep"), "");
23
28
  }
24
- await Bun.write(
25
- join(root, "registry.json"),
26
- JSON.stringify({ pipelines: {} }, null, 2) + "\n"
27
- );
29
+ await writeRegistry(root, { version: REGISTRY_VERSION, pipelines: {} });
28
30
  }
29
31
 
30
32
  // ─── start ────────────────────────────────────────────────────────────────────
@@ -43,18 +45,49 @@ export function buildUiCorsEnv(opts: StartOptions): Record<string, string> {
43
45
  return env;
44
46
  }
45
47
 
48
+ /**
49
+ * Build the child env objects passed to the spawned `_start-ui` and
50
+ * `_start-orchestrator` processes. The parent `handleStart` resolves the
51
+ * workspace root once via `resolveWorkspaceRoot` and transports the absolute
52
+ * value to the children via `PO_ROOT`; the children perform their own boot
53
+ * resolution from that transported value.
54
+ */
55
+ export function buildStartChildEnv(
56
+ absoluteRoot: string,
57
+ port: string,
58
+ opts?: StartOptions,
59
+ ): { uiEnv: Record<string, string>; orchEnv: Record<string, string> } {
60
+ const uiEnv: Record<string, string> = Object.fromEntries(
61
+ Object.entries({ ...process.env, NODE_ENV: "production", PO_ROOT: absoluteRoot, PORT: port }).filter(
62
+ (entry): entry is [string, string] => entry[1] !== undefined,
63
+ ),
64
+ );
65
+ delete uiEnv["PO_UI_PORT"];
66
+
67
+ const corsEnv = opts ? buildUiCorsEnv(opts) : {};
68
+ Object.assign(uiEnv, corsEnv);
69
+
70
+ const orchEnv: Record<string, string> = Object.fromEntries(
71
+ Object.entries({ ...process.env, NODE_ENV: "production", PO_ROOT: absoluteRoot }).filter(
72
+ (entry): entry is [string, string] => entry[1] !== undefined,
73
+ ),
74
+ );
75
+
76
+ return { uiEnv, orchEnv };
77
+ }
78
+
46
79
  export async function handleStart(
47
80
  root: string | undefined,
48
81
  port: string,
49
82
  opts?: StartOptions,
50
83
  ): Promise<void> {
51
- const rawRoot = root ?? process.env["PO_ROOT"];
52
- if (!rawRoot) {
53
- console.error("Error: --root or PO_ROOT environment variable is required");
84
+ let absoluteRoot: string;
85
+ try {
86
+ absoluteRoot = resolveWorkspaceRoot(root);
87
+ } catch (err) {
88
+ console.error(err instanceof Error ? err.message : String(err));
54
89
  process.exit(1);
55
90
  }
56
-
57
- const absoluteRoot = resolve(rawRoot);
58
91
  process.env["PO_ROOT"] = absoluteRoot;
59
92
 
60
93
  if (!isCompiledBinary()) {
@@ -69,21 +102,7 @@ export async function handleStart(
69
102
  }
70
103
  }
71
104
 
72
- const uiEnv: Record<string, string> = Object.fromEntries(
73
- Object.entries({ ...process.env, NODE_ENV: "production", PO_ROOT: absoluteRoot, PORT: port }).filter(
74
- (entry): entry is [string, string] => entry[1] !== undefined
75
- )
76
- );
77
- delete uiEnv["PO_UI_PORT"];
78
-
79
- const corsEnv = opts ? buildUiCorsEnv(opts) : {};
80
- Object.assign(uiEnv, corsEnv);
81
-
82
- const orchEnv: Record<string, string> = Object.fromEntries(
83
- Object.entries({ ...process.env, NODE_ENV: "production", PO_ROOT: absoluteRoot }).filter(
84
- (entry): entry is [string, string] => entry[1] !== undefined
85
- )
86
- );
105
+ const { uiEnv, orchEnv } = buildStartChildEnv(absoluteRoot, port, opts);
87
106
 
88
107
  const uiReexec = buildReexecArgs(["_start-ui"]);
89
108
  const orchReexec = buildReexecArgs(["_start-orchestrator"]);
@@ -182,7 +201,10 @@ export async function handleStart(
182
201
 
183
202
  // ─── submit ───────────────────────────────────────────────────────────────────
184
203
 
185
- export async function handleSubmit(seedFile: string): Promise<void> {
204
+ export async function handleSubmit(
205
+ seedFile: string,
206
+ opts: { root?: string } = {},
207
+ ): Promise<void> {
186
208
  let seedObject: unknown;
187
209
  try {
188
210
  const text = await Bun.file(seedFile).text();
@@ -192,8 +214,9 @@ export async function handleSubmit(seedFile: string): Promise<void> {
192
214
  process.exit(1);
193
215
  }
194
216
 
217
+ const root = opts.root ?? process.env["PO_ROOT"] ?? process.cwd();
195
218
  try {
196
- const result = await submitJobWithValidation({ dataDir: process.cwd(), seedObject });
219
+ const result = await submitJobWithValidation({ root, seedObject });
197
220
  if (result.success) {
198
221
  console.log(`Job submitted: ${result.jobId} (${result.jobName})`);
199
222
  } else {
@@ -208,8 +231,12 @@ export async function handleSubmit(seedFile: string): Promise<void> {
208
231
 
209
232
  // ─── status ───────────────────────────────────────────────────────────────────
210
233
 
211
- export async function handleStatus(jobName: string | undefined): Promise<void> {
212
- const orchestrator = new PipelineOrchestrator({ autoStart: false });
234
+ export async function handleStatus(
235
+ jobName: string | undefined,
236
+ opts: { root?: string } = {},
237
+ ): Promise<void> {
238
+ const root = opts.root ?? process.env["PO_ROOT"] ?? process.cwd();
239
+ const orchestrator = new PipelineOrchestrator({ autoStart: false, root });
213
240
  if (jobName) {
214
241
  const result = await orchestrator.getStatus(jobName);
215
242
  console.log(JSON.stringify(result, null, 2));
@@ -245,25 +272,7 @@ export async function handleAddPipeline(slug: string, root: string): Promise<voi
245
272
  );
246
273
  await Bun.write(join(tasksDir, "index.ts"), "export default {};\n");
247
274
 
248
- const registryPath = join(root, "registry.json");
249
- let registry: Registry = { pipelines: {} };
250
- try {
251
- const text = await Bun.file(registryPath).text();
252
- registry = JSON.parse(text) as Registry;
253
- } catch {
254
- // fallback to empty registry
255
- }
256
-
257
- const pipelinePath = join(pipelineDir, "pipeline.json");
258
- const taskRegistryPath = join(tasksDir, "index.ts");
259
- registry.pipelines[slug] = {
260
- name: slug,
261
- description: "New pipeline",
262
- pipelinePath,
263
- taskRegistryPath,
264
- };
265
-
266
- await Bun.write(registryPath, JSON.stringify(registry, null, 2) + "\n");
275
+ await registerPipeline(root, slug, { name: slug, description: "New pipeline" });
267
276
  } catch (err) {
268
277
  console.error(`Error creating pipeline: ${err instanceof Error ? err.message : String(err)}`);
269
278
  process.exit(1);
@@ -393,21 +402,21 @@ export async function handleAnalyze(taskPath: string): Promise<void> {
393
402
 
394
403
  // ─── Hidden: _start-ui ───────────────────────────────────────────────────────
395
404
 
396
- async function handleStartUi(): Promise<void> {
405
+ export async function handleStartUi(): Promise<void> {
406
+ // PO_ROOT is the inter-process transport set by `handleStart`; the boot
407
+ // function (startServer) owns the runtime resolution.
397
408
  const { startServer } = await import("../ui/server/index.ts");
398
409
  await startServer({
399
- dataDir: process.env["PO_ROOT"] ?? process.cwd(),
410
+ dataDir: process.env["PO_ROOT"],
400
411
  port: parseInt(process.env["PORT"] ?? "4000", 10),
401
412
  });
402
413
  }
403
414
 
404
415
  // ─── Hidden: _start-orchestrator ─────────────────────────────────────────────
405
416
 
406
- async function handleStartOrchestrator(): Promise<void> {
407
- if (!process.env["PO_ROOT"]) {
408
- console.error("Error: PO_ROOT environment variable is required");
409
- process.exit(1);
410
- }
417
+ export async function handleStartOrchestrator(): Promise<void> {
418
+ // PO_ROOT is the inter-process transport set by `handleStart`; the boot
419
+ // function (startOrchestrator) owns the runtime resolution.
411
420
  const { startOrchestrator } = await import("../core/orchestrator.ts");
412
421
  const handle = await startOrchestrator({ dataDir: process.env["PO_ROOT"] });
413
422
  process.on("SIGINT", () => {
@@ -420,7 +429,8 @@ async function handleStartOrchestrator(): Promise<void> {
420
429
 
421
430
  // ─── Hidden: _run-job ─────────────────────────────────────────────────────────
422
431
 
423
- async function handleRunJob(jobId: string): Promise<void> {
432
+ export async function handleRunJob(jobId: string): Promise<void> {
433
+ // Boot resolution happens inside runPipelineJob; this handler only forwards.
424
434
  const { runPipelineJob } = await import("../core/pipeline-runner.ts");
425
435
  await runPipelineJob(jobId);
426
436
  }
@@ -456,15 +466,17 @@ program
456
466
  program
457
467
  .command("submit <seed-file>")
458
468
  .description("Submit a job from a seed JSON file")
459
- .action(async (seedFile: string) => {
460
- await handleSubmit(seedFile);
469
+ .option("--root <path>", "Workspace root")
470
+ .action(async (seedFile: string, opts: { root?: string }) => {
471
+ await handleSubmit(seedFile, opts);
461
472
  });
462
473
 
463
474
  program
464
475
  .command("status [job-name]")
465
476
  .description("Query job status")
466
- .action(async (jobName: string | undefined) => {
467
- await handleStatus(jobName);
477
+ .option("--root <path>", "Workspace root")
478
+ .action(async (jobName: string | undefined, opts: { root?: string }) => {
479
+ await handleStatus(jobName, opts);
468
480
  });
469
481
 
470
482
  program
package/src/cli/types.ts CHANGED
@@ -1,16 +1,3 @@
1
- /** Central index of all pipelines in the workspace. */
2
- export interface Registry {
3
- pipelines: Record<string, PipelineRegistryEntry>;
4
- }
5
-
6
- /** A single pipeline's entry in the registry. */
7
- export interface PipelineRegistryEntry {
8
- name: string;
9
- description: string;
10
- pipelinePath: string;
11
- taskRegistryPath: string;
12
- }
13
-
14
1
  /** A pipeline's configuration file. */
15
2
  export interface PipelineConfig {
16
3
  name: string;
@@ -1,3 +1,4 @@
1
+ import { readJsonFile, writeJsonFile } from "../core/json-file.ts";
1
2
  import type { PipelineConfig } from "./types.ts";
2
3
 
3
4
  export async function updatePipelineJson(
@@ -7,19 +8,7 @@ export async function updatePipelineJson(
7
8
  ): Promise<void> {
8
9
  const filePath = `${root}/pipeline-config/${pipelineSlug}/pipeline.json`;
9
10
 
10
- let config: PipelineConfig;
11
- try {
12
- const text = await Bun.file(filePath).text();
13
- const parsed = JSON.parse(text) as PipelineConfig;
14
- config = parsed;
15
- } catch {
16
- config = {
17
- name: pipelineSlug,
18
- version: "1.0.0",
19
- description: "New pipeline",
20
- tasks: [],
21
- };
22
- }
11
+ const config = await readJsonFile<PipelineConfig>(filePath);
23
12
 
24
13
  if (!Array.isArray(config.tasks)) {
25
14
  config.tasks = [];
@@ -29,5 +18,5 @@ export async function updatePipelineJson(
29
18
  config.tasks.push(taskSlug);
30
19
  }
31
20
 
32
- await Bun.write(filePath, JSON.stringify(config, null, 2) + "\n");
21
+ await writeJsonFile(filePath, config);
33
22
  }
@@ -1,5 +1,7 @@
1
- import { describe, it, expect } from "bun:test";
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { resolve } from "node:path";
2
3
  import {
4
+ resolveWorkspaceRoot,
3
5
  resolvePipelinePaths,
4
6
  getPendingSeedPath,
5
7
  getCurrentSeedPath,
@@ -119,3 +121,46 @@ describe("getJobPipelinePath", () => {
119
121
  );
120
122
  });
121
123
  });
124
+
125
+ describe("resolveWorkspaceRoot", () => {
126
+ const originalPoRoot = process.env["PO_ROOT"];
127
+
128
+ beforeEach(() => {
129
+ delete process.env["PO_ROOT"];
130
+ });
131
+
132
+ afterEach(() => {
133
+ if (originalPoRoot === undefined) {
134
+ delete process.env["PO_ROOT"];
135
+ } else {
136
+ process.env["PO_ROOT"] = originalPoRoot;
137
+ }
138
+ });
139
+
140
+ it("returns resolve(explicit) when explicit is set", () => {
141
+ process.env["PO_ROOT"] = "/from-env";
142
+ expect(resolveWorkspaceRoot("/from-arg")).toBe("/from-arg");
143
+ });
144
+
145
+ it("returns resolve(PO_ROOT) when only PO_ROOT is set", () => {
146
+ process.env["PO_ROOT"] = "/from-env";
147
+ expect(resolveWorkspaceRoot()).toBe(resolve("/from-env"));
148
+ });
149
+
150
+ it("explicit wins over PO_ROOT", () => {
151
+ process.env["PO_ROOT"] = "/from-env";
152
+ expect(resolveWorkspaceRoot("/from-arg")).not.toBe(resolve("/from-env"));
153
+ });
154
+
155
+ it("throws naming --root and PO_ROOT when neither is set", () => {
156
+ expect(() => resolveWorkspaceRoot()).toThrow(
157
+ "Workspace root is required: pass --root or set PO_ROOT",
158
+ );
159
+ });
160
+
161
+ it("returns an absolute path for a relative input", () => {
162
+ expect(resolveWorkspaceRoot("relative/root")).toBe(
163
+ resolve("relative/root"),
164
+ );
165
+ });
166
+ });