@ryanfw/prompt-orchestration-pipeline 1.3.3 → 1.3.5
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.
- package/docs/http-api.md +17 -4
- package/docs/pop-task-guide.md +8 -0
- package/package.json +5 -5
- package/src/api/__tests__/index.test.ts +144 -41
- package/src/api/index.ts +15 -83
- package/src/cli/__tests__/analyze-task.test.ts +39 -7
- package/src/cli/__tests__/index.test.ts +337 -17
- package/src/cli/__tests__/types.test.ts +0 -18
- package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
- package/src/cli/analyze-task.ts +3 -14
- package/src/cli/index.ts +73 -61
- package/src/cli/types.ts +0 -13
- package/src/cli/update-pipeline-json.ts +3 -14
- package/src/config/__tests__/config-schema.test.ts +42 -0
- package/src/config/__tests__/legacy-literal-guard.test.ts +156 -0
- package/src/config/__tests__/models.test.ts +139 -1
- package/src/config/__tests__/paths.test.ts +46 -1
- package/src/config/__tests__/pipeline-registry.test.ts +767 -0
- package/src/config/__tests__/sse-events.test.ts +470 -0
- package/src/config/models.ts +121 -19
- package/src/config/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/core/__tests__/agent-step.test.ts +167 -0
- package/src/core/__tests__/config.test.ts +517 -107
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +200 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +583 -3
- package/src/core/__tests__/json-file.test.ts +111 -0
- package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
- package/src/core/__tests__/logger.test.ts +22 -0
- package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
- package/src/core/__tests__/orchestrator.test.ts +373 -2
- package/src/core/__tests__/pipeline-runner.test.ts +946 -9
- package/src/core/__tests__/redact.test.ts +153 -0
- package/src/core/__tests__/runner-liveness.test.ts +45 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +1 -1
- package/src/core/__tests__/task-runner.test.ts +594 -3
- package/src/core/__tests__/task-telemetry.test.ts +241 -0
- package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
- package/src/core/agent-step.ts +9 -2
- package/src/core/agent-types.ts +10 -4
- package/src/core/config.ts +127 -234
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +50 -19
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +183 -19
- package/src/core/json-file.ts +45 -0
- package/src/core/lifecycle-policy.ts +23 -8
- package/src/core/logger.ts +0 -29
- package/src/core/orchestrator.ts +124 -70
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +8 -4
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +3 -28
- package/src/core/task-runner.ts +356 -319
- package/src/core/task-telemetry.ts +107 -0
- package/src/harness/__tests__/subprocess.test.ts +112 -1
- package/src/harness/subprocess.ts +55 -16
- package/src/llm/__tests__/dispatch.test.ts +130 -0
- package/src/llm/__tests__/index.test.ts +693 -43
- package/src/llm/__tests__/legacy-normalization.test.ts +109 -0
- package/src/llm/index.ts +405 -165
- package/src/providers/__tests__/alibaba.test.ts +67 -6
- package/src/providers/__tests__/anthropic.test.ts +35 -14
- package/src/providers/__tests__/base.test.ts +62 -0
- package/src/providers/__tests__/claude-code.test.ts +108 -17
- package/src/providers/__tests__/deepseek.test.ts +16 -6
- package/src/providers/__tests__/gemini.test.ts +47 -25
- package/src/providers/__tests__/moonshot.test.ts +60 -3
- package/src/providers/__tests__/openai.test.ts +262 -74
- package/src/providers/__tests__/opencode.test.ts +77 -0
- package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
- package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
- package/src/providers/__tests__/types.test.ts +128 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +49 -40
- package/src/providers/alibaba.ts +20 -39
- package/src/providers/anthropic.ts +23 -11
- package/src/providers/base.ts +19 -0
- package/src/providers/claude-code.ts +27 -18
- package/src/providers/deepseek.ts +9 -28
- package/src/providers/gemini.ts +20 -58
- package/src/providers/moonshot.ts +22 -11
- package/src/providers/openai.ts +79 -61
- package/src/providers/opencode.ts +16 -33
- package/src/providers/stream-accumulator.ts +27 -21
- package/src/providers/types.ts +36 -9
- package/src/providers/zhipu.ts +15 -46
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +69 -2
- package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
- package/src/task-analysis/__tests__/repository.test.ts +65 -0
- package/src/task-analysis/__tests__/types.test.ts +63 -130
- package/src/task-analysis/analyzer.ts +85 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +12 -2
- package/src/task-analysis/index.ts +2 -36
- package/src/task-analysis/repository.ts +45 -0
- package/src/task-analysis/types.ts +42 -58
- package/src/ui/client/__tests__/api.test.ts +143 -1
- package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
- package/src/ui/client/__tests__/job-adapter.test.ts +125 -2
- package/src/ui/client/__tests__/load-state.test.ts +70 -0
- package/src/ui/client/__tests__/types.test.ts +66 -3
- package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
- package/src/ui/client/__tests__/useJobList.test.ts +198 -23
- package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
- package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
- package/src/ui/client/adapters/job-adapter.ts +31 -16
- package/src/ui/client/api.ts +38 -15
- package/src/ui/client/bootstrap.ts +19 -14
- package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
- package/src/ui/client/hooks/useJobList.ts +26 -31
- package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
- package/src/ui/client/load-state.ts +20 -0
- package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
- package/src/ui/client/reducers/job-events.ts +137 -0
- package/src/ui/client/types.ts +14 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +13 -2
- package/src/ui/components/StageTimeline.tsx +8 -20
- package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
- package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
- package/src/ui/components/__tests__/JobTable.test.tsx +83 -0
- package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
- package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
- package/src/ui/components/types.ts +33 -15
- package/src/ui/dist/assets/{index-L6cvsCAx.js → index-lBbVeNZC.js} +4304 -253
- package/src/ui/dist/assets/index-lBbVeNZC.js.map +1 -0
- package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/pages/Code.tsx +5 -2
- package/src/ui/pages/PipelineDetail.tsx +60 -4
- package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
- package/src/ui/pages/__tests__/pages.test.tsx +236 -42
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
- package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
- package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
- package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
- package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
- package/src/ui/server/__tests__/http-api-contract.test.ts +838 -0
- package/src/ui/server/__tests__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +582 -21
- package/src/ui/server/__tests__/job-control-service.test.ts +282 -0
- package/src/ui/server/__tests__/job-endpoints.test.ts +43 -1
- package/src/ui/server/__tests__/job-process-registry.test.ts +151 -0
- package/src/ui/server/__tests__/job-repository.test.ts +142 -0
- package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
- package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
- package/src/ui/server/__tests__/router-threading.test.ts +148 -0
- package/src/ui/server/__tests__/router.test.ts +104 -0
- package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
- package/src/ui/server/__tests__/sse-enhancer.test.ts +39 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/config-bridge.ts +9 -6
- package/src/ui/server/embedded-assets-imports.d.ts +44 -0
- package/src/ui/server/embedded-assets.ts +13 -2
- package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
- package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
- package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
- package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
- package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
- package/src/ui/server/endpoints/file-endpoints.ts +15 -10
- package/src/ui/server/endpoints/gate-endpoints.ts +24 -12
- package/src/ui/server/endpoints/job-control-endpoints.ts +19 -714
- package/src/ui/server/endpoints/job-endpoints.ts +1 -0
- package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
- package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
- package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
- package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
- package/src/ui/server/index.ts +19 -14
- package/src/ui/server/job-control-service.ts +520 -0
- package/src/ui/server/job-process-registry.ts +250 -0
- package/src/ui/server/job-reader.ts +14 -2
- package/src/ui/server/job-repository.ts +316 -0
- package/src/ui/server/router.ts +33 -10
- package/src/ui/server/sse-broadcast.ts +14 -3
- package/src/ui/server/sse-enhancer.ts +12 -2
- package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
- package/src/ui/state/__tests__/snapshot.test.ts +70 -15
- package/src/ui/state/__tests__/types.test.ts +6 -0
- package/src/ui/state/snapshot.ts +1 -1
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +42 -0
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +45 -3
- package/src/ui/state/transformers/list-transformer.ts +6 -0
- package/src/ui/state/types.ts +6 -1
- package/src/utils/__tests__/path-containment.test.ts +160 -0
- package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
- package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
- package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
- package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
- package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
- package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
- package/src/task-analysis/__tests__/index.test.ts +0 -143
- package/src/task-analysis/__tests__/parser.test.ts +0 -41
- package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
- package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
- package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
- package/src/task-analysis/extractors/artifacts.ts +0 -143
- package/src/task-analysis/extractors/llm-calls.ts +0 -117
- package/src/task-analysis/extractors/stages.ts +0 -45
- package/src/task-analysis/parser.ts +0 -20
- package/src/task-analysis/utils/ast.ts +0 -45
- package/src/ui/dist/assets/index-L6cvsCAx.js.map +0 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
- package/src/ui/embedded-assets.js +0 -12
- package/src/ui/server/__tests__/path-containment.test.ts +0 -54
package/docs/http-api.md
CHANGED
|
@@ -81,7 +81,10 @@ or on error:
|
|
|
81
81
|
| `forbidden_origin` | 403 | Cross-origin mutation rejected |
|
|
82
82
|
| `status_unavailable` | 500 | Job status file unreadable |
|
|
83
83
|
| `no_pending_gate` | 409 | Job has no pending gate decision |
|
|
84
|
+
| `INVALID_JSON` | 409 | Job status JSON is invalid |
|
|
85
|
+
| `STATUS_CORRUPT` | 409 | Job status file is corrupt |
|
|
84
86
|
| `FS_ERROR` | 500 | Filesystem operation failed |
|
|
87
|
+
| `BAD_PATH` | 400 | Task analysis path is invalid |
|
|
85
88
|
| `BAD_REQUEST` | 400 | Malformed request body |
|
|
86
89
|
| `NOT_FOUND` | 404 | Route or resource not found |
|
|
87
90
|
|
|
@@ -203,11 +206,21 @@ Clients should treat a gap longer than the keep-alive interval as a potential di
|
|
|
203
206
|
|
|
204
207
|
| Event | Data Shape | Description |
|
|
205
208
|
|-------|-----------|-------------|
|
|
206
|
-
| `state:change` | `{ jobId,
|
|
207
|
-
| `state:summary` | `{ changeCount }` | Summary of accumulated state changes |
|
|
208
|
-
| `job:created` | `JobSummary` | A new job was detected |
|
|
209
|
-
| `job:updated` | `JobSummary` | An existing job was updated |
|
|
209
|
+
| `state:change` | `{ jobId, path }` | A job's state file changed; the `jobId` is derived from the changed path |
|
|
210
|
+
| `state:summary` | `{ changeCount }` | Summary of accumulated state changes when no per-job change is prioritized |
|
|
211
|
+
| `job:created` | `JobSummary` | A new job was detected (summary granularity; see Caveat below) |
|
|
212
|
+
| `job:updated` | `JobSummary` | An existing job was updated (summary granularity; see Caveat below) |
|
|
213
|
+
| `job:removed` | `{ jobId }` | A job was removed from a watched location |
|
|
210
214
|
| `heartbeat` | `{ ok, timestamp }` | Application-level heartbeat (distinct from the keep-alive comment) |
|
|
215
|
+
| `seed:uploaded` | `{}` | A seed was uploaded; clients should refetch the job list |
|
|
216
|
+
|
|
217
|
+
The keep-alive comment (`: keep-alive`) sent on a fixed interval is **not** an application event;
|
|
218
|
+
clients must not treat it as one. Unknown event types and unknown fields within a known event
|
|
219
|
+
are tolerated by the contract; clients should ignore them. `task:updated`, `status:changed`,
|
|
220
|
+
bare `state`, and `message` are **not** part of the browser-facing contract and are not
|
|
221
|
+
emitted on `/api/events`. `job:created` and `job:updated` carry summary-granularity
|
|
222
|
+
`JobSummary` data; deep per-task detail (costs, files, per-stage state) continues to arrive
|
|
223
|
+
via the existing `state:change` → refetch path.
|
|
211
224
|
|
|
212
225
|
### Job Filtering
|
|
213
226
|
|
package/docs/pop-task-guide.md
CHANGED
|
@@ -209,6 +209,7 @@ export const inference = async ({ runAgent, io, data, flags }) => {
|
|
|
209
209
|
// timeoutMs?: number // optional wall-clock cap
|
|
210
210
|
// idleTimeoutMs?: number // optional no-output cap
|
|
211
211
|
// captureDiff?: boolean // capture a git diff as 'agent.patch'
|
|
212
|
+
// onEvent?: (event) => {} // observe adapter harness events during the run
|
|
212
213
|
});
|
|
213
214
|
|
|
214
215
|
if (!result.ok) {
|
|
@@ -226,6 +227,13 @@ By default (`io` is `true`) the agent shares the task's file I/O: it can call th
|
|
|
226
227
|
task sees, and its `agent-result.md` is written automatically. Token usage and
|
|
227
228
|
cost flow into the job status like any other LLM call.
|
|
228
229
|
|
|
230
|
+
Use `onEvent` when a task needs to observe live harness activity, such as
|
|
231
|
+
assistant messages, tool calls, usage events, or run completion. POP invokes the
|
|
232
|
+
callback after it has buffered the event and appended the redacted debug log, so
|
|
233
|
+
omitting the callback leaves behavior unchanged. The `event.raw` payload comes
|
|
234
|
+
directly from the underlying CLI adapter; treat it as unstable and avoid
|
|
235
|
+
persisting it unless you have considered sensitive values in the raw stream.
|
|
236
|
+
|
|
229
237
|
**`runAgent` vs `llm`**: use `llm.<provider>.chat()` for a single request/response
|
|
230
238
|
LLM call; use `runAgent()` when you need a tool-using CLI agent that reads and
|
|
231
239
|
writes files over multiple turns.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server/index.ts",
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
"ui": "NODE_ENV=development bun --watch src/ui/server/index.ts",
|
|
33
33
|
"ui:dev": "vite",
|
|
34
34
|
"ui:build": "vite build && bun run generate:embedded-assets",
|
|
35
|
-
"
|
|
35
|
+
"verify:package": "bun scripts/verify-package-assets.js",
|
|
36
|
+
"smoke:packed-consumer": "bun scripts/smoke-packed-consumer.js",
|
|
37
|
+
"release:check": "bun run ui:build && bun run verify:package && bun run smoke:packed-consumer",
|
|
38
|
+
"prepack": "bun run ui:build && bun run verify:package",
|
|
36
39
|
"ui:prod": "bun src/ui/server/index.ts",
|
|
37
40
|
"demo:ui": "NODE_ENV=production PO_ROOT=demo bun src/ui/server/index.ts",
|
|
38
41
|
"demo:orchestrator": "PO_ROOT=demo NODE_ENV=production bun -e \"import('./src/core/orchestrator.ts').then(m => m.startOrchestrator({ dataDir: process.env.PO_ROOT || 'demo' })).catch(err => { console.error(err); process.exit(1) })\"",
|
|
@@ -42,9 +45,6 @@
|
|
|
42
45
|
"typecheck": "tsc --noEmit"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
|
-
"@babel/parser": "^7.28.5",
|
|
46
|
-
"@babel/traverse": "^7.28.5",
|
|
47
|
-
"@babel/types": "^7.28.5",
|
|
48
48
|
"@radix-ui/react-progress": "^1.1.7",
|
|
49
49
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
50
50
|
"@radix-ui/react-toast": "^1.2.15",
|
|
@@ -29,8 +29,11 @@ async function scaffoldWorkspace(tmpDir: string): Promise<void> {
|
|
|
29
29
|
await Bun.write(
|
|
30
30
|
join(tmpDir, "pipeline-config", "registry.json"),
|
|
31
31
|
JSON.stringify({
|
|
32
|
+
version: 1,
|
|
32
33
|
pipelines: {
|
|
33
34
|
"test-pipeline": {
|
|
35
|
+
name: "Test Pipeline",
|
|
36
|
+
description: "test pipeline",
|
|
34
37
|
configDir: join(tmpDir, "pipeline-config", "test-pipeline"),
|
|
35
38
|
tasksDir: join(tmpDir, "pipeline-config", "test-pipeline", "tasks"),
|
|
36
39
|
},
|
|
@@ -60,7 +63,7 @@ describe("submitJobWithValidation", () => {
|
|
|
60
63
|
|
|
61
64
|
it("writes seed file to pending and returns success", async () => {
|
|
62
65
|
const result = await submitJobWithValidation({
|
|
63
|
-
|
|
66
|
+
root: tmpDir,
|
|
64
67
|
seedObject: { pipeline: "test-pipeline" },
|
|
65
68
|
});
|
|
66
69
|
|
|
@@ -79,7 +82,7 @@ describe("submitJobWithValidation", () => {
|
|
|
79
82
|
|
|
80
83
|
it("returns seed name as jobName when provided", async () => {
|
|
81
84
|
const result = await submitJobWithValidation({
|
|
82
|
-
|
|
85
|
+
root: tmpDir,
|
|
83
86
|
seedObject: { pipeline: "test-pipeline", name: "my-job" },
|
|
84
87
|
});
|
|
85
88
|
|
|
@@ -87,74 +90,88 @@ describe("submitJobWithValidation", () => {
|
|
|
87
90
|
expect((result as SubmitSuccessResult).jobName).toBe("my-job");
|
|
88
91
|
});
|
|
89
92
|
|
|
90
|
-
it("rejects non-object seed with message
|
|
93
|
+
it("rejects non-object seed with exact message", async () => {
|
|
91
94
|
const result = await submitJobWithValidation({
|
|
92
|
-
|
|
95
|
+
root: tmpDir,
|
|
93
96
|
seedObject: "not an object",
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
expect(result.success).toBe(false);
|
|
97
|
-
expect((result as SubmitFailureResult).message).
|
|
100
|
+
expect((result as SubmitFailureResult).message).toBe("seed must be a JSON object");
|
|
98
101
|
});
|
|
99
102
|
|
|
100
|
-
it("rejects array seed with message
|
|
103
|
+
it("rejects array seed with exact message", async () => {
|
|
101
104
|
const result = await submitJobWithValidation({
|
|
102
|
-
|
|
105
|
+
root: tmpDir,
|
|
103
106
|
seedObject: [1, 2, 3],
|
|
104
107
|
});
|
|
105
108
|
|
|
106
109
|
expect(result.success).toBe(false);
|
|
107
|
-
expect((result as SubmitFailureResult).message).
|
|
110
|
+
expect((result as SubmitFailureResult).message).toBe("seed must be a JSON object");
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
it("rejects null seed with message
|
|
113
|
+
it("rejects null seed with exact message", async () => {
|
|
111
114
|
const result = await submitJobWithValidation({
|
|
112
|
-
|
|
115
|
+
root: tmpDir,
|
|
113
116
|
seedObject: null,
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
expect(result.success).toBe(false);
|
|
117
|
-
expect((result as SubmitFailureResult).message).
|
|
120
|
+
expect((result as SubmitFailureResult).message).toBe("seed must be a JSON object");
|
|
118
121
|
});
|
|
119
122
|
|
|
120
|
-
it("rejects seed missing pipeline field with message
|
|
123
|
+
it("rejects seed missing pipeline field with exact message", async () => {
|
|
121
124
|
const result = await submitJobWithValidation({
|
|
122
|
-
|
|
125
|
+
root: tmpDir,
|
|
123
126
|
seedObject: { name: "my-job" },
|
|
124
127
|
});
|
|
125
128
|
|
|
126
129
|
expect(result.success).toBe(false);
|
|
127
|
-
expect((result as SubmitFailureResult).message).
|
|
130
|
+
expect((result as SubmitFailureResult).message).toBe("seed.pipeline must be a non-empty string");
|
|
128
131
|
});
|
|
129
132
|
|
|
130
133
|
it("rejects seed with empty pipeline string", async () => {
|
|
131
134
|
const result = await submitJobWithValidation({
|
|
132
|
-
|
|
135
|
+
root: tmpDir,
|
|
133
136
|
seedObject: { pipeline: "" },
|
|
134
137
|
});
|
|
135
138
|
|
|
136
139
|
expect(result.success).toBe(false);
|
|
137
|
-
expect((result as SubmitFailureResult).message).
|
|
140
|
+
expect((result as SubmitFailureResult).message).toBe("seed.pipeline must be a non-empty string");
|
|
138
141
|
});
|
|
139
142
|
|
|
140
|
-
it("rejects non-
|
|
143
|
+
it("rejects non-string pipeline value", async () => {
|
|
141
144
|
const result = await submitJobWithValidation({
|
|
142
|
-
|
|
145
|
+
root: tmpDir,
|
|
146
|
+
seedObject: { pipeline: 123 },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(false);
|
|
150
|
+
expect((result as SubmitFailureResult).message).toBe("seed.pipeline must be a non-empty string");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rejects non-existent pipeline slug with exact message", async () => {
|
|
154
|
+
const result = await submitJobWithValidation({
|
|
155
|
+
root: tmpDir,
|
|
143
156
|
seedObject: { pipeline: "does-not-exist" },
|
|
144
157
|
});
|
|
145
158
|
|
|
146
159
|
expect(result.success).toBe(false);
|
|
147
|
-
expect((result as SubmitFailureResult).message).
|
|
160
|
+
expect((result as SubmitFailureResult).message).toBe(
|
|
161
|
+
"Pipeline 'does-not-exist' not found in registry",
|
|
162
|
+
);
|
|
148
163
|
});
|
|
149
164
|
|
|
150
165
|
it("rejects seed with empty name string", async () => {
|
|
151
166
|
const result = await submitJobWithValidation({
|
|
152
|
-
|
|
167
|
+
root: tmpDir,
|
|
153
168
|
seedObject: { pipeline: "test-pipeline", name: "" },
|
|
154
169
|
});
|
|
155
170
|
|
|
156
171
|
expect(result.success).toBe(false);
|
|
157
|
-
expect((result as SubmitFailureResult).message).
|
|
172
|
+
expect((result as SubmitFailureResult).message).toBe(
|
|
173
|
+
"seed.name must be a non-empty string if provided",
|
|
174
|
+
);
|
|
158
175
|
});
|
|
159
176
|
});
|
|
160
177
|
|
|
@@ -196,7 +213,7 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
196
213
|
}),
|
|
197
214
|
);
|
|
198
215
|
|
|
199
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
216
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
200
217
|
const record = await orch.getStatus(jobId);
|
|
201
218
|
|
|
202
219
|
expect(record.jobId).toBe(jobId);
|
|
@@ -220,7 +237,7 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
220
237
|
}),
|
|
221
238
|
);
|
|
222
239
|
|
|
223
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
240
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
224
241
|
const record = await orch.getStatus(jobId);
|
|
225
242
|
|
|
226
243
|
expect(record.jobId).toBe(jobId);
|
|
@@ -244,7 +261,7 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
244
261
|
}),
|
|
245
262
|
);
|
|
246
263
|
|
|
247
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
264
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
248
265
|
const record = await orch.getStatus("named-job");
|
|
249
266
|
|
|
250
267
|
expect(record.jobId).toBe(jobId);
|
|
@@ -258,7 +275,7 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
258
275
|
JSON.stringify({ pipeline: "test-pipeline", name: "pending-job" }),
|
|
259
276
|
);
|
|
260
277
|
|
|
261
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
278
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
262
279
|
const record = await orch.getStatus(jobId);
|
|
263
280
|
|
|
264
281
|
expect(record.jobId).toBe(jobId);
|
|
@@ -268,8 +285,24 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
268
285
|
expect(record.createdAt).toBeTruthy();
|
|
269
286
|
});
|
|
270
287
|
|
|
288
|
+
it("returns pending record for non-UUID seed filename parsed by parseSeedFilename", async () => {
|
|
289
|
+
const jobId = "my-job";
|
|
290
|
+
await Bun.write(
|
|
291
|
+
join(tmpDir, "pipeline-data", "pending", `${jobId}-seed.json`),
|
|
292
|
+
JSON.stringify({ pipeline: "test-pipeline", name: "my-job" }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
296
|
+
const record = await orch.getStatus(jobId);
|
|
297
|
+
|
|
298
|
+
expect(record.jobId).toBe(jobId);
|
|
299
|
+
expect(record.jobName).toBe("my-job");
|
|
300
|
+
expect(record.state).toBe("pending");
|
|
301
|
+
expect(record.pipeline).toBe("test-pipeline");
|
|
302
|
+
});
|
|
303
|
+
|
|
271
304
|
it("throws for nonexistent job with message containing 'not found'", async () => {
|
|
272
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
305
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
273
306
|
|
|
274
307
|
await expect(orch.getStatus("nonexistent-id")).rejects.toThrow("not found");
|
|
275
308
|
});
|
|
@@ -280,7 +313,7 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
280
313
|
await mkdir(jobDir, { recursive: true });
|
|
281
314
|
await Bun.write(join(jobDir, "tasks-status.json"), "{{{not valid json");
|
|
282
315
|
|
|
283
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
316
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
284
317
|
const record = await orch.getStatus(jobId);
|
|
285
318
|
|
|
286
319
|
expect(record.jobId).toBe(jobId);
|
|
@@ -315,7 +348,7 @@ describe("PipelineOrchestrator.getStatus", () => {
|
|
|
315
348
|
}),
|
|
316
349
|
);
|
|
317
350
|
|
|
318
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
351
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
319
352
|
const record = await orch.getStatus(jobId);
|
|
320
353
|
|
|
321
354
|
expect(record.jobId).toBe(jobId);
|
|
@@ -388,7 +421,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
388
421
|
}),
|
|
389
422
|
);
|
|
390
423
|
|
|
391
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
424
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
392
425
|
const jobs = await orch.listJobs();
|
|
393
426
|
|
|
394
427
|
expect(jobs.length).toBe(3);
|
|
@@ -406,7 +439,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
406
439
|
});
|
|
407
440
|
|
|
408
441
|
it("returns empty array when all directories are empty", async () => {
|
|
409
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
442
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
410
443
|
const jobs = await orch.listJobs();
|
|
411
444
|
|
|
412
445
|
expect(jobs).toEqual([]);
|
|
@@ -416,7 +449,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
416
449
|
// Remove all pipeline-data directories
|
|
417
450
|
await rm(join(tmpDir, "pipeline-data"), { recursive: true, force: true });
|
|
418
451
|
|
|
419
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
452
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
420
453
|
const jobs = await orch.listJobs();
|
|
421
454
|
|
|
422
455
|
expect(jobs).toEqual([]);
|
|
@@ -464,7 +497,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
464
497
|
}),
|
|
465
498
|
);
|
|
466
499
|
|
|
467
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
500
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
468
501
|
const jobs = await orch.listJobs();
|
|
469
502
|
|
|
470
503
|
expect(jobs.length).toBe(4);
|
|
@@ -514,7 +547,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
514
547
|
}),
|
|
515
548
|
);
|
|
516
549
|
|
|
517
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
550
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
518
551
|
const jobs = await orch.listJobs();
|
|
519
552
|
|
|
520
553
|
const ids = jobs.map((j) => j.jobId);
|
|
@@ -544,7 +577,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
544
577
|
}),
|
|
545
578
|
);
|
|
546
579
|
|
|
547
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
580
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
548
581
|
const jobs = await orch.listJobs();
|
|
549
582
|
|
|
550
583
|
const ids = jobs.map((j) => j.jobId);
|
|
@@ -573,7 +606,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
573
606
|
}),
|
|
574
607
|
);
|
|
575
608
|
|
|
576
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
609
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
577
610
|
const getStatusRecord = await orch.getStatus(jobId);
|
|
578
611
|
const listJobsRecords = await orch.listJobs();
|
|
579
612
|
const listJobsRecord = listJobsRecords.find((j) => j.jobId === jobId)!;
|
|
@@ -599,7 +632,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
599
632
|
}),
|
|
600
633
|
);
|
|
601
634
|
|
|
602
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
635
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
603
636
|
const record = await orch.getStatus(jobId);
|
|
604
637
|
|
|
605
638
|
expect(record.state).toBe("complete");
|
|
@@ -621,7 +654,7 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
621
654
|
}),
|
|
622
655
|
);
|
|
623
656
|
|
|
624
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
657
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
625
658
|
const jobs = await orch.listJobs();
|
|
626
659
|
const record = jobs.find((j) => j.jobId === jobId)!;
|
|
627
660
|
|
|
@@ -645,13 +678,13 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
645
678
|
}),
|
|
646
679
|
);
|
|
647
680
|
|
|
648
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
681
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
649
682
|
const record = await orch.getStatus(jobId);
|
|
650
683
|
|
|
651
684
|
expect(record.state).toBe("failed");
|
|
652
685
|
});
|
|
653
686
|
|
|
654
|
-
it("listJobs preserves JobView extras (
|
|
687
|
+
it("listJobs preserves JobView extras (costs)", async () => {
|
|
655
688
|
const jobId = "cccc-5555-6666-7777-0005";
|
|
656
689
|
const jobDir = join(tmpDir, "pipeline-data", "current", jobId);
|
|
657
690
|
await mkdir(jobDir, { recursive: true });
|
|
@@ -669,12 +702,82 @@ describe("PipelineOrchestrator.listJobs", () => {
|
|
|
669
702
|
}),
|
|
670
703
|
);
|
|
671
704
|
|
|
672
|
-
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
705
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
673
706
|
const jobs = await orch.listJobs();
|
|
674
707
|
const record = jobs.find((j) => j.jobId === jobId)!;
|
|
675
708
|
|
|
676
709
|
expect(record.state).toBe("running");
|
|
677
|
-
expect(record.progress).toBe(
|
|
710
|
+
expect(record.progress).toBe(0);
|
|
678
711
|
expect(record.costs).toEqual({ usd: 1.23 });
|
|
679
712
|
});
|
|
680
713
|
});
|
|
714
|
+
|
|
715
|
+
// ─── AC-4: API strict root resolution ─────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
describe("API strict root resolution (AC-4)", () => {
|
|
718
|
+
let tmpDir: string;
|
|
719
|
+
let savedPoRoot: string | undefined;
|
|
720
|
+
|
|
721
|
+
beforeEach(async () => {
|
|
722
|
+
tmpDir = await mkdtemp(join(tmpdir(), "pop-api-strict-test-"));
|
|
723
|
+
await scaffoldWorkspace(tmpDir);
|
|
724
|
+
savedPoRoot = process.env["PO_ROOT"];
|
|
725
|
+
delete process.env["PO_ROOT"];
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
afterEach(async () => {
|
|
729
|
+
if (savedPoRoot === undefined) {
|
|
730
|
+
delete process.env["PO_ROOT"];
|
|
731
|
+
} else {
|
|
732
|
+
process.env["PO_ROOT"] = savedPoRoot;
|
|
733
|
+
}
|
|
734
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("PipelineOrchestrator throws when neither root nor PO_ROOT is set", () => {
|
|
738
|
+
expect(() => new PipelineOrchestrator({ autoStart: false })).toThrow(
|
|
739
|
+
"Workspace root is required",
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("PipelineOrchestrator succeeds when root is set explicitly", async () => {
|
|
744
|
+
const orch = new PipelineOrchestrator({ autoStart: false, root: tmpDir });
|
|
745
|
+
const jobs = await orch.listJobs();
|
|
746
|
+
expect(jobs).toEqual([]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("PipelineOrchestrator succeeds when PO_ROOT is set and root is absent", async () => {
|
|
750
|
+
process.env["PO_ROOT"] = tmpDir;
|
|
751
|
+
const orch = new PipelineOrchestrator({ autoStart: false });
|
|
752
|
+
const jobs = await orch.listJobs();
|
|
753
|
+
expect(jobs).toEqual([]);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("submitJobWithValidation throws when neither root nor PO_ROOT is set", async () => {
|
|
757
|
+
await expect(
|
|
758
|
+
submitJobWithValidation({ seedObject: { pipeline: "test-pipeline" } }),
|
|
759
|
+
).rejects.toThrow("Workspace root is required");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("submitJobWithValidation succeeds when root is set explicitly", async () => {
|
|
763
|
+
const result = await submitJobWithValidation({
|
|
764
|
+
root: tmpDir,
|
|
765
|
+
seedObject: { pipeline: "test-pipeline" },
|
|
766
|
+
});
|
|
767
|
+
expect(result.success).toBe(true);
|
|
768
|
+
const pendingFiles = await readdir(join(tmpDir, "pipeline-data", "pending"));
|
|
769
|
+
const seedFile = pendingFiles.find((f) => f.endsWith("-seed.json"));
|
|
770
|
+
expect(seedFile).toBeDefined();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("submitJobWithValidation succeeds when PO_ROOT is set and root is absent", async () => {
|
|
774
|
+
process.env["PO_ROOT"] = tmpDir;
|
|
775
|
+
const result = await submitJobWithValidation({
|
|
776
|
+
seedObject: { pipeline: "test-pipeline" },
|
|
777
|
+
});
|
|
778
|
+
expect(result.success).toBe(true);
|
|
779
|
+
const pendingFiles = await readdir(join(tmpDir, "pipeline-data", "pending"));
|
|
780
|
+
const seedFile = pendingFiles.find((f) => f.endsWith("-seed.json"));
|
|
781
|
+
expect(seedFile).toBeDefined();
|
|
782
|
+
});
|
|
783
|
+
});
|
package/src/api/index.ts
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { resolvePipelinePaths,
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { resolvePipelinePaths, resolveWorkspaceRoot } from "../config/paths";
|
|
4
4
|
import type { PipelinePaths } from "../config/paths";
|
|
5
|
-
import {
|
|
5
|
+
import { submitSeed } from "../core/job-submission.ts";
|
|
6
|
+
import type { SubmitResult } from "../core/job-submission.ts";
|
|
6
7
|
import { normalizeJobView } from "../core/job-view";
|
|
7
8
|
import type { JobView } from "../core/job-view";
|
|
8
|
-
import {
|
|
9
|
+
import { parseSeedFilename } from "../core/seed-naming.ts";
|
|
9
10
|
import { readJobStatusResult, STATUS_FILENAME, type StatusReadResult } from "../core/status-writer";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
export interface SubmitSuccessResult {
|
|
13
|
-
success: true;
|
|
14
|
-
jobId: string;
|
|
15
|
-
jobName: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Result of a failed job submission. */
|
|
19
|
-
export interface SubmitFailureResult {
|
|
20
|
-
success: false;
|
|
21
|
-
message: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type SubmitResult = SubmitSuccessResult | SubmitFailureResult;
|
|
12
|
+
export type { SubmitResult, SubmitSuccessResult, SubmitFailureResult } from "../core/job-submission.ts";
|
|
25
13
|
|
|
26
14
|
/** Options for submitJobWithValidation. */
|
|
27
15
|
export interface SubmitJobOptions {
|
|
28
|
-
|
|
16
|
+
root?: string;
|
|
29
17
|
seedObject: unknown;
|
|
30
18
|
}
|
|
31
19
|
|
|
@@ -47,28 +35,11 @@ export interface JobStatusRecord {
|
|
|
47
35
|
/** Orchestrator construction options. */
|
|
48
36
|
export interface OrchestratorOptions {
|
|
49
37
|
autoStart: boolean;
|
|
38
|
+
root?: string;
|
|
50
39
|
}
|
|
51
40
|
|
|
52
41
|
// ─── Private Helpers ──────────────────────────────────────────────────────────
|
|
53
42
|
|
|
54
|
-
async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
|
|
55
|
-
const tmp = `${filePath}.${Date.now()}.tmp`;
|
|
56
|
-
await Bun.write(tmp, JSON.stringify(data, null, 2));
|
|
57
|
-
await rename(tmp, filePath);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function assertSeedObject(value: unknown): asserts value is Record<string, unknown> {
|
|
61
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
62
|
-
throw new Error("seed must be a JSON object");
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function assertPipelineSlug(value: unknown): asserts value is string {
|
|
67
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
68
|
-
throw new Error("seed.pipeline must be a non-empty string");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
43
|
async function safeReaddir(dirPath: string): Promise<string[]> {
|
|
73
44
|
try {
|
|
74
45
|
return await readdir(dirPath);
|
|
@@ -147,44 +118,8 @@ async function readStatusForApi(jobDir: string, label: string): Promise<StatusRe
|
|
|
147
118
|
export async function submitJobWithValidation(
|
|
148
119
|
opts: SubmitJobOptions,
|
|
149
120
|
): Promise<SubmitResult> {
|
|
150
|
-
const rootDir =
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
assertSeedObject(opts.seedObject);
|
|
154
|
-
assertPipelineSlug(opts.seedObject["pipeline"]);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
return { success: false, message: (err as Error).message };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const seed = opts.seedObject as Record<string, unknown>;
|
|
160
|
-
|
|
161
|
-
if (seed["name"] !== undefined) {
|
|
162
|
-
if (typeof seed["name"] !== "string" || seed["name"].length === 0) {
|
|
163
|
-
return { success: false, message: "seed.name must be a non-empty string if provided" };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
getPipelineConfig(seed["pipeline"] as string, rootDir);
|
|
169
|
-
} catch (err) {
|
|
170
|
-
return { success: false, message: (err as Error).message };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const jobId = crypto.randomUUID();
|
|
174
|
-
const jobName = (typeof seed["name"] === "string" && seed["name"].length > 0)
|
|
175
|
-
? seed["name"]
|
|
176
|
-
: jobId;
|
|
177
|
-
const pendingPath = getPendingSeedPath(rootDir, jobId);
|
|
178
|
-
|
|
179
|
-
await mkdir(path.dirname(pendingPath), { recursive: true });
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
await atomicWriteJson(pendingPath, opts.seedObject);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
throw new Error(`failed to write seed file for job ${jobId}: ${(err as Error).message}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return { success: true, jobId, jobName };
|
|
121
|
+
const rootDir = resolveWorkspaceRoot(opts.root);
|
|
122
|
+
return submitSeed({ root: rootDir, seedObject: opts.seedObject });
|
|
188
123
|
}
|
|
189
124
|
|
|
190
125
|
export class PipelineOrchestrator {
|
|
@@ -194,7 +129,7 @@ export class PipelineOrchestrator {
|
|
|
194
129
|
|
|
195
130
|
constructor(opts: OrchestratorOptions) {
|
|
196
131
|
this.autoStart = opts.autoStart;
|
|
197
|
-
this.root =
|
|
132
|
+
this.root = resolveWorkspaceRoot(opts.root);
|
|
198
133
|
this.paths = resolvePipelinePaths(this.root);
|
|
199
134
|
}
|
|
200
135
|
|
|
@@ -232,10 +167,8 @@ export class PipelineOrchestrator {
|
|
|
232
167
|
// 3. Pending fallback
|
|
233
168
|
const pendingFiles = await safeReaddir(this.paths.pending);
|
|
234
169
|
for (const file of pendingFiles) {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
const id = match[1]!;
|
|
238
|
-
if (id !== jobName) continue;
|
|
170
|
+
const id = parseSeedFilename(file);
|
|
171
|
+
if (id === null || id !== jobName) continue;
|
|
239
172
|
|
|
240
173
|
const filePath = path.join(this.paths.pending, file);
|
|
241
174
|
const seed = await readJsonFile<Record<string, unknown>>(filePath);
|
|
@@ -258,9 +191,8 @@ export class PipelineOrchestrator {
|
|
|
258
191
|
// Pending jobs
|
|
259
192
|
const pendingFiles = await safeReaddir(this.paths.pending);
|
|
260
193
|
for (const file of pendingFiles) {
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
const id = match[1]!;
|
|
194
|
+
const id = parseSeedFilename(file);
|
|
195
|
+
if (id === null) continue;
|
|
264
196
|
const filePath = path.join(this.paths.pending, file);
|
|
265
197
|
try {
|
|
266
198
|
const seed = await readJsonFile<Record<string, unknown>>(filePath);
|