@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.
- package/docs/http-api.md +80 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +443 -32
- package/src/api/index.ts +108 -127
- package/src/cli/__tests__/analyze-task.test.ts +40 -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__/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/__tests__/statuses.test.ts +41 -0
- 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/config/statuses.ts +20 -1
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +545 -105
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +260 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +1341 -0
- 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 +615 -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 +910 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- 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 +4 -1
- package/src/core/agent-types.ts +5 -4
- package/src/core/config.ts +134 -222
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +56 -20
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +473 -0
- 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 +200 -74
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +280 -0
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +27 -33
- 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__/index.test.ts +684 -33
- package/src/llm/index.ts +310 -67
- 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 +99 -14
- 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 +27 -0
- 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 +85 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +27 -14
- 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 +15 -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 +29 -4
- package/src/providers/zhipu.ts +15 -44
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -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 +91 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +13 -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 +203 -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 +41 -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 +16 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +41 -13
- 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 +137 -1
- 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 +35 -15
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
- package/src/ui/dist/assets/index-DN3-zvtP.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/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__/config-bridge.test.ts +9 -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__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
- package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
- 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 +70 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/config-bridge.ts +3 -4
- 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__/meta-endpoint.test.ts +1 -0
- 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 +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
- package/src/ui/server/endpoints/job-endpoints.ts +7 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- 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-reader.ts +14 -2
- 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 +120 -14
- package/src/ui/state/__tests__/types.test.ts +104 -5
- package/src/ui/state/snapshot.ts +2 -2
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
- package/src/ui/state/transformers/list-transformer.ts +13 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +15 -48
- 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--RH3sAt3.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/src/core/config.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolveAllPipelinesSync, resolvePipelineSync } from "../config/pipeline-registry.ts";
|
|
4
3
|
|
|
5
4
|
export interface OrchestratorConfig {
|
|
6
5
|
shutdownTimeout: number;
|
|
7
6
|
processSpawnRetries: number;
|
|
8
7
|
processSpawnRetryDelay: number;
|
|
9
8
|
lockFileTimeout: number;
|
|
9
|
+
staleRunningGraceMs: number;
|
|
10
|
+
staleLeaseTimeoutMs: number;
|
|
10
11
|
watchDebounce: number;
|
|
11
12
|
watchStabilityThreshold: number;
|
|
12
13
|
watchPollInterval: number;
|
|
@@ -23,6 +24,8 @@ export interface TaskRunnerConfig {
|
|
|
23
24
|
export interface LLMConfig {
|
|
24
25
|
defaultProvider: string;
|
|
25
26
|
defaultModel: string;
|
|
27
|
+
internalProvider: string;
|
|
28
|
+
internalModel: string;
|
|
26
29
|
maxConcurrency: number;
|
|
27
30
|
retryMaxAttempts: number;
|
|
28
31
|
retryBackoffMs: number;
|
|
@@ -35,14 +38,6 @@ export interface UIConfig {
|
|
|
35
38
|
maxRecentChanges: number;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export interface PathsConfig {
|
|
39
|
-
root: string;
|
|
40
|
-
dataDir: string;
|
|
41
|
-
pendingDir: string;
|
|
42
|
-
currentDir: string;
|
|
43
|
-
completeDir: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
41
|
export interface PipelineEntry {
|
|
47
42
|
configDir: string;
|
|
48
43
|
tasksDir: string;
|
|
@@ -67,17 +62,11 @@ export interface AppConfig {
|
|
|
67
62
|
taskRunner: TaskRunnerConfig;
|
|
68
63
|
llm: LLMConfig;
|
|
69
64
|
ui: UIConfig;
|
|
70
|
-
paths: PathsConfig;
|
|
71
65
|
pipelines: Record<string, PipelineEntry>;
|
|
72
66
|
validation: ValidationConfig;
|
|
73
67
|
logging: LoggingConfig;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
export interface LoadConfigOptions {
|
|
77
|
-
configPath?: string;
|
|
78
|
-
validate?: boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
70
|
export interface PipelineConfigResult {
|
|
82
71
|
pipelineJsonPath: string;
|
|
83
72
|
tasksDir: string;
|
|
@@ -89,6 +78,8 @@ export const defaultConfig = {
|
|
|
89
78
|
processSpawnRetries: 3,
|
|
90
79
|
processSpawnRetryDelay: 1000,
|
|
91
80
|
lockFileTimeout: 30000,
|
|
81
|
+
staleRunningGraceMs: 30000,
|
|
82
|
+
staleLeaseTimeoutMs: 30000,
|
|
92
83
|
watchDebounce: 500,
|
|
93
84
|
watchStabilityThreshold: 1000,
|
|
94
85
|
watchPollInterval: 100,
|
|
@@ -103,6 +94,8 @@ export const defaultConfig = {
|
|
|
103
94
|
llm: {
|
|
104
95
|
defaultProvider: "openai",
|
|
105
96
|
defaultModel: "gpt-4o",
|
|
97
|
+
internalProvider: "deepseek",
|
|
98
|
+
internalModel: "deepseek-v4-flash",
|
|
106
99
|
maxConcurrency: 5,
|
|
107
100
|
retryMaxAttempts: 3,
|
|
108
101
|
retryBackoffMs: 1000,
|
|
@@ -113,13 +106,6 @@ export const defaultConfig = {
|
|
|
113
106
|
heartbeatInterval: 30000,
|
|
114
107
|
maxRecentChanges: 50,
|
|
115
108
|
},
|
|
116
|
-
paths: {
|
|
117
|
-
root: process.cwd(),
|
|
118
|
-
dataDir: "data",
|
|
119
|
-
pendingDir: "data/pending",
|
|
120
|
-
currentDir: "data/current",
|
|
121
|
-
completeDir: "data/complete",
|
|
122
|
-
},
|
|
123
109
|
pipelines: {},
|
|
124
110
|
validation: {
|
|
125
111
|
seedNameMinLength: 3,
|
|
@@ -133,6 +119,13 @@ export const defaultConfig = {
|
|
|
133
119
|
},
|
|
134
120
|
} satisfies AppConfig;
|
|
135
121
|
|
|
122
|
+
type DeepReadonly<T> =
|
|
123
|
+
T extends (infer U)[]
|
|
124
|
+
? ReadonlyArray<DeepReadonly<U>>
|
|
125
|
+
: T extends object
|
|
126
|
+
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
127
|
+
: T;
|
|
128
|
+
|
|
136
129
|
// ─── Deep Merge ───────────────────────────────────────────────────────────────
|
|
137
130
|
|
|
138
131
|
type PlainObject = Record<string, unknown>;
|
|
@@ -162,162 +155,161 @@ function deepMerge(target: PlainObject, source: PlainObject): PlainObject {
|
|
|
162
155
|
|
|
163
156
|
const VALID_LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
|
|
164
157
|
|
|
165
|
-
function
|
|
166
|
-
|
|
158
|
+
function envInt(
|
|
159
|
+
name: string,
|
|
160
|
+
raw: string | undefined,
|
|
161
|
+
opts?: { min?: number; max?: number }
|
|
162
|
+
): number | undefined {
|
|
163
|
+
if (raw === undefined) return undefined;
|
|
164
|
+
if (raw === "") {
|
|
165
|
+
throw new Error(`${name} must not be empty`);
|
|
166
|
+
}
|
|
167
|
+
const n = Number(raw);
|
|
168
|
+
const min = opts?.min;
|
|
169
|
+
const max = opts?.max;
|
|
170
|
+
if (min !== undefined || max !== undefined) {
|
|
171
|
+
if (!Number.isInteger(n) || (min !== undefined && n < min) || (max !== undefined && n > max)) {
|
|
172
|
+
if (min !== undefined && max !== undefined) {
|
|
173
|
+
throw new Error(`${name} must be between ${min} and ${max}, got ${raw}`);
|
|
174
|
+
}
|
|
175
|
+
if (min !== undefined) {
|
|
176
|
+
throw new Error(`${name} must be an integer >= ${min}, got ${raw}`);
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`${name} must be an integer <= ${max}, got ${raw}`);
|
|
179
|
+
}
|
|
180
|
+
return n;
|
|
181
|
+
}
|
|
182
|
+
if (!Number.isInteger(n)) {
|
|
183
|
+
throw new Error(`${name} must be an integer, got ${raw}`);
|
|
184
|
+
}
|
|
185
|
+
return n;
|
|
186
|
+
}
|
|
167
187
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
...(root ? { root } : {}),
|
|
173
|
-
...(dataDir ? { dataDir } : {}),
|
|
174
|
-
};
|
|
188
|
+
function envString(name: string, raw: string | undefined): string | undefined {
|
|
189
|
+
if (raw === undefined) return undefined;
|
|
190
|
+
if (raw === "") {
|
|
191
|
+
throw new Error(`${name} must not be empty`);
|
|
175
192
|
}
|
|
193
|
+
return raw;
|
|
194
|
+
}
|
|
176
195
|
|
|
177
|
-
|
|
178
|
-
const
|
|
196
|
+
function loadFromEnvironment(config: AppConfig): AppConfig {
|
|
197
|
+
const overrides: PlainObject = {};
|
|
198
|
+
|
|
199
|
+
const port = envInt("PORT", process.env["PORT"], { min: 1, max: 65535 });
|
|
200
|
+
const poPort = envInt("PO_UI_PORT", process.env["PO_UI_PORT"], { min: 1, max: 65535 });
|
|
179
201
|
const host = process.env["PO_HOST"];
|
|
180
202
|
const uiOverrides: PlainObject = {};
|
|
181
|
-
if (
|
|
182
|
-
if (
|
|
203
|
+
if (port !== undefined) uiOverrides["port"] = port;
|
|
204
|
+
if (poPort !== undefined) uiOverrides["port"] = poPort;
|
|
183
205
|
if (host) uiOverrides["host"] = host;
|
|
184
206
|
if (Object.keys(uiOverrides).length > 0) overrides["ui"] = uiOverrides;
|
|
185
207
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
const maxConcurrency = envInt("PO_MAX_CONCURRENCY", process.env["PO_MAX_CONCURRENCY"], { min: 1 });
|
|
209
|
+
const internalProvider = envString("PO_INTERNAL_LLM_PROVIDER", process.env["PO_INTERNAL_LLM_PROVIDER"]);
|
|
210
|
+
const internalModel = envString("PO_INTERNAL_LLM_MODEL", process.env["PO_INTERNAL_LLM_MODEL"]);
|
|
211
|
+
const llmOverrides: PlainObject = {};
|
|
212
|
+
if (maxConcurrency !== undefined) llmOverrides["maxConcurrency"] = maxConcurrency;
|
|
213
|
+
if (internalProvider !== undefined) llmOverrides["internalProvider"] = internalProvider;
|
|
214
|
+
if (internalModel !== undefined) llmOverrides["internalModel"] = internalModel;
|
|
215
|
+
if (Object.keys(llmOverrides).length > 0) overrides["llm"] = llmOverrides;
|
|
216
|
+
|
|
217
|
+
const maxAttempts = envInt("PO_TASK_MAX_ATTEMPTS", process.env["PO_TASK_MAX_ATTEMPTS"], { min: 1 });
|
|
218
|
+
if (maxAttempts !== undefined) {
|
|
197
219
|
overrides["taskRunner"] = {
|
|
198
220
|
...((overrides["taskRunner"] as PlainObject | undefined) ?? {}),
|
|
199
221
|
maxAttempts,
|
|
200
222
|
};
|
|
201
223
|
}
|
|
202
224
|
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
225
|
+
const shutdownTimeout = envInt("PO_SHUTDOWN_TIMEOUT", process.env["PO_SHUTDOWN_TIMEOUT"], { min: 0 });
|
|
226
|
+
if (shutdownTimeout !== undefined) {
|
|
205
227
|
overrides["orchestrator"] = {
|
|
206
228
|
...((overrides["orchestrator"] as PlainObject | undefined) ?? {}),
|
|
207
|
-
shutdownTimeout
|
|
229
|
+
shutdownTimeout,
|
|
208
230
|
};
|
|
209
231
|
}
|
|
210
232
|
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
const maxConcurrentJobs = Number(maxRunningJobsRaw);
|
|
214
|
-
if (!Number.isInteger(maxConcurrentJobs) || maxConcurrentJobs < 1) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`orchestrator.maxConcurrentJobs must be a positive integer (PO_MAX_RUNNING_JOBS), got ${maxRunningJobsRaw}`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
233
|
+
const maxRunningJobs = envInt("PO_MAX_RUNNING_JOBS", process.env["PO_MAX_RUNNING_JOBS"], { min: 1 });
|
|
234
|
+
if (maxRunningJobs !== undefined) {
|
|
219
235
|
overrides["orchestrator"] = {
|
|
220
236
|
...((overrides["orchestrator"] as PlainObject | undefined) ?? {}),
|
|
221
|
-
maxConcurrentJobs,
|
|
237
|
+
maxConcurrentJobs: maxRunningJobs,
|
|
222
238
|
};
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
const staleRunningGraceMs = envInt(
|
|
242
|
+
"PO_STALE_RUNNING_GRACE_MS",
|
|
243
|
+
process.env["PO_STALE_RUNNING_GRACE_MS"],
|
|
244
|
+
{ min: 1 }
|
|
245
|
+
);
|
|
246
|
+
if (staleRunningGraceMs !== undefined) {
|
|
247
|
+
overrides["orchestrator"] = {
|
|
248
|
+
...((overrides["orchestrator"] as PlainObject | undefined) ?? {}),
|
|
249
|
+
staleRunningGraceMs,
|
|
250
|
+
};
|
|
228
251
|
}
|
|
229
252
|
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
const staleLeaseTimeoutMs = envInt(
|
|
254
|
+
"PO_STALE_LEASE_TIMEOUT_MS",
|
|
255
|
+
process.env["PO_STALE_LEASE_TIMEOUT_MS"],
|
|
256
|
+
{ min: 1 }
|
|
257
|
+
);
|
|
258
|
+
if (staleLeaseTimeoutMs !== undefined) {
|
|
259
|
+
overrides["orchestrator"] = {
|
|
260
|
+
...((overrides["orchestrator"] as PlainObject | undefined) ?? {}),
|
|
261
|
+
staleLeaseTimeoutMs,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
232
264
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
239
|
-
return null;
|
|
265
|
+
const logLevel = process.env["PO_LOG_LEVEL"];
|
|
266
|
+
if (logLevel !== undefined) {
|
|
267
|
+
if (!(VALID_LOG_LEVELS as readonly string[]).includes(logLevel)) {
|
|
268
|
+
throw new Error(`PO_LOG_LEVEL must be one of ${VALID_LOG_LEVELS.join(", ")}, got ${logLevel}`);
|
|
240
269
|
}
|
|
241
|
-
|
|
270
|
+
overrides["logging"] = { level: logLevel as "debug" | "info" | "warn" | "error" };
|
|
242
271
|
}
|
|
243
272
|
|
|
244
|
-
|
|
245
|
-
return JSON.parse(text) as Record<string, unknown>;
|
|
246
|
-
} catch (err) {
|
|
247
|
-
throw new Error(`Failed to load config file: ${filePath}: ${String(err)}`);
|
|
248
|
-
}
|
|
273
|
+
return deepMerge(config as unknown as PlainObject, overrides) as unknown as AppConfig;
|
|
249
274
|
}
|
|
250
275
|
|
|
251
276
|
function validateConfig(config: AppConfig): void {
|
|
252
|
-
const errors: string[] = [];
|
|
253
|
-
|
|
254
|
-
if (config.ui.port < 1 || config.ui.port > 65535) {
|
|
255
|
-
errors.push(`ui.port must be between 1 and 65535, got ${config.ui.port}`);
|
|
256
|
-
}
|
|
257
|
-
if (config.llm.maxConcurrency < 1) {
|
|
258
|
-
errors.push(`llm.maxConcurrency must be >= 1, got ${config.llm.maxConcurrency}`);
|
|
259
|
-
}
|
|
260
277
|
if (config.taskRunner.maxRefinementAttempts < 1) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
errors.push(`taskRunner.maxAttempts must be an integer >= 1, got ${config.taskRunner.maxAttempts}`);
|
|
265
|
-
}
|
|
266
|
-
if (!Number.isInteger(config.orchestrator.maxConcurrentJobs) || config.orchestrator.maxConcurrentJobs < 1) {
|
|
267
|
-
errors.push(`orchestrator.maxConcurrentJobs must be a positive integer, got ${config.orchestrator.maxConcurrentJobs}`);
|
|
268
|
-
}
|
|
269
|
-
if (!(VALID_LOG_LEVELS as readonly string[]).includes(config.logging.level)) {
|
|
270
|
-
errors.push(`logging.level must be one of ${VALID_LOG_LEVELS.join(", ")}, got ${config.logging.level}`);
|
|
278
|
+
throw new Error(
|
|
279
|
+
`taskRunner.maxRefinementAttempts must be >= 1, got ${config.taskRunner.maxRefinementAttempts}`
|
|
280
|
+
);
|
|
271
281
|
}
|
|
282
|
+
}
|
|
272
283
|
|
|
273
|
-
|
|
274
|
-
|
|
284
|
+
function deepFreeze<T>(value: T): DeepReadonly<T> {
|
|
285
|
+
if (value === null || value === undefined) return value as DeepReadonly<T>;
|
|
286
|
+
if (typeof value !== "object") return value as DeepReadonly<T>;
|
|
287
|
+
Object.freeze(value);
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
for (const item of value) {
|
|
290
|
+
if (item !== null && typeof item === "object") {
|
|
291
|
+
deepFreeze(item);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return value as DeepReadonly<T>;
|
|
275
295
|
}
|
|
296
|
+
for (const key of Object.keys(value as object)) {
|
|
297
|
+
const v = (value as Record<string, unknown>)[key];
|
|
298
|
+
if (v !== null && typeof v === "object") {
|
|
299
|
+
deepFreeze(v);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return value as DeepReadonly<T>;
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
// ─── Module State ─────────────────────────────────────────────────────────────
|
|
279
306
|
|
|
280
|
-
let cachedConfig: AppConfig | null = null;
|
|
307
|
+
let cachedConfig: DeepReadonly<AppConfig> | null = null;
|
|
281
308
|
|
|
282
309
|
// ─── Registry Hydration ───────────────────────────────────────────────────────
|
|
283
310
|
|
|
284
|
-
|
|
285
|
-
pipelines
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
interface LegacyRegistry {
|
|
289
|
-
slugs: string[];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async function hydratePipelinesFromRegistry(config: AppConfig, registryPath: string): Promise<void> {
|
|
293
|
-
let raw: RegistryPipelines | LegacyRegistry;
|
|
294
|
-
try {
|
|
295
|
-
raw = JSON.parse(await readFile(registryPath, "utf8")) as RegistryPipelines | LegacyRegistry;
|
|
296
|
-
} catch (err) {
|
|
297
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
throw new Error(`Failed to read pipeline registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if ("slugs" in raw) {
|
|
304
|
-
console.warn(`[config] Legacy registry format detected at ${registryPath} — skipping`);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const root = config.paths.root;
|
|
309
|
-
for (const [slug, entry] of Object.entries(raw.pipelines)) {
|
|
310
|
-
const resolvedConfigDir = entry.configDir
|
|
311
|
-
? (path.isAbsolute(entry.configDir) ? entry.configDir : path.join(root, entry.configDir))
|
|
312
|
-
: path.join(root, "pipeline-config", slug);
|
|
313
|
-
const resolvedTasksDir = entry.tasksDir
|
|
314
|
-
? (path.isAbsolute(entry.tasksDir) ? entry.tasksDir : path.join(root, entry.tasksDir))
|
|
315
|
-
: path.join(resolvedConfigDir, "tasks");
|
|
316
|
-
config.pipelines[slug] = {
|
|
317
|
-
configDir: resolvedConfigDir,
|
|
318
|
-
tasksDir: resolvedTasksDir,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
311
|
+
function applyPipelineRegistryEntriesSync(config: AppConfig, poRoot: string): void {
|
|
312
|
+
Object.assign(config.pipelines, resolveAllPipelinesSync(poRoot));
|
|
321
313
|
}
|
|
322
314
|
|
|
323
315
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -326,52 +318,7 @@ export function resetConfig(): void {
|
|
|
326
318
|
cachedConfig = null;
|
|
327
319
|
}
|
|
328
320
|
|
|
329
|
-
export
|
|
330
|
-
let config = JSON.parse(JSON.stringify(defaultConfig)) as AppConfig;
|
|
331
|
-
|
|
332
|
-
if (options?.configPath) {
|
|
333
|
-
const fileData = await loadFromFile(options.configPath);
|
|
334
|
-
if (fileData) {
|
|
335
|
-
config = deepMerge(config as unknown as PlainObject, fileData) as unknown as AppConfig;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
config = loadFromEnvironment(config);
|
|
340
|
-
|
|
341
|
-
if (!process.env["PO_ROOT"]) {
|
|
342
|
-
throw new Error("PO_ROOT is required");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
await hydratePipelinesFromRegistry(config, path.join(config.paths.root, "pipeline-config", "registry.json"));
|
|
346
|
-
|
|
347
|
-
if (Object.keys(config.pipelines).length === 0) {
|
|
348
|
-
throw new Error("No pipelines are registered");
|
|
349
|
-
} else {
|
|
350
|
-
const errors: string[] = [];
|
|
351
|
-
for (const [slug, entry] of Object.entries(config.pipelines)) {
|
|
352
|
-
const configDirExists = existsSync(path.join(entry.configDir, "pipeline.json"));
|
|
353
|
-
const tasksDirExists = await stat(entry.tasksDir).then(() => true).catch(() => false);
|
|
354
|
-
if (!configDirExists) {
|
|
355
|
-
errors.push(`Pipeline '${slug}': pipeline.json not found`);
|
|
356
|
-
}
|
|
357
|
-
if (!tasksDirExists) {
|
|
358
|
-
errors.push(`Pipeline '${slug}': tasksDir not found`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (errors.length > 0) {
|
|
362
|
-
throw new Error(errors.join("; "));
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (options?.validate !== false) {
|
|
367
|
-
validateConfig(config);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
cachedConfig = config;
|
|
371
|
-
return config;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export function getConfig(): AppConfig {
|
|
321
|
+
export function getConfig(): DeepReadonly<AppConfig> {
|
|
375
322
|
if (cachedConfig) return cachedConfig;
|
|
376
323
|
|
|
377
324
|
let config = JSON.parse(JSON.stringify(defaultConfig)) as AppConfig;
|
|
@@ -380,37 +327,22 @@ export function getConfig(): AppConfig {
|
|
|
380
327
|
const rawPoRoot = process.env["PO_ROOT"];
|
|
381
328
|
if (!rawPoRoot) {
|
|
382
329
|
if (process.env["NODE_ENV"] === "test") {
|
|
383
|
-
|
|
330
|
+
validateConfig(config);
|
|
331
|
+
cachedConfig = deepFreeze(config);
|
|
384
332
|
return cachedConfig;
|
|
385
333
|
}
|
|
386
334
|
throw new Error("PO_ROOT is required");
|
|
387
335
|
}
|
|
388
336
|
const poRoot = path.resolve(rawPoRoot);
|
|
389
337
|
|
|
390
|
-
|
|
391
|
-
if (existsSync(registryPath)) {
|
|
392
|
-
const raw = JSON.parse(readFileSync(registryPath, "utf8")) as RegistryPipelines | LegacyRegistry;
|
|
393
|
-
if (!("slugs" in raw)) {
|
|
394
|
-
for (const [slug, entry] of Object.entries(raw.pipelines)) {
|
|
395
|
-
const resolvedConfigDir = entry.configDir
|
|
396
|
-
? (path.isAbsolute(entry.configDir) ? entry.configDir : path.join(poRoot, entry.configDir))
|
|
397
|
-
: path.join(poRoot, "pipeline-config", slug);
|
|
398
|
-
const resolvedTasksDir = entry.tasksDir
|
|
399
|
-
? (path.isAbsolute(entry.tasksDir) ? entry.tasksDir : path.join(poRoot, entry.tasksDir))
|
|
400
|
-
: path.join(resolvedConfigDir, "tasks");
|
|
401
|
-
config.pipelines[slug] = {
|
|
402
|
-
configDir: resolvedConfigDir,
|
|
403
|
-
tasksDir: resolvedTasksDir,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
338
|
+
applyPipelineRegistryEntriesSync(config, poRoot);
|
|
408
339
|
|
|
409
340
|
if (Object.keys(config.pipelines).length === 0 && process.env["NODE_ENV"] !== "test") {
|
|
410
341
|
console.warn("[config] No pipelines found in registry");
|
|
411
342
|
}
|
|
412
343
|
|
|
413
|
-
|
|
344
|
+
validateConfig(config);
|
|
345
|
+
cachedConfig = deepFreeze(config);
|
|
414
346
|
return cachedConfig;
|
|
415
347
|
}
|
|
416
348
|
|
|
@@ -435,27 +367,7 @@ export function getConfigValue(dotPath: string, defaultValue?: unknown): unknown
|
|
|
435
367
|
|
|
436
368
|
export function getPipelineConfig(slug: string, root?: string): PipelineConfigResult {
|
|
437
369
|
if (root) {
|
|
438
|
-
|
|
439
|
-
if (existsSync(registryPath)) {
|
|
440
|
-
const raw = JSON.parse(readFileSync(registryPath, "utf8")) as RegistryPipelines | LegacyRegistry;
|
|
441
|
-
if (!("slugs" in raw)) {
|
|
442
|
-
const entry = raw.pipelines[slug];
|
|
443
|
-
if (!entry) {
|
|
444
|
-
throw new Error(`Pipeline '${slug}' not found in registry`);
|
|
445
|
-
}
|
|
446
|
-
const resolvedConfigDir = entry.configDir
|
|
447
|
-
? (path.isAbsolute(entry.configDir) ? entry.configDir : path.join(root, entry.configDir))
|
|
448
|
-
: path.join(root, "pipeline-config", slug);
|
|
449
|
-
const resolvedTasksDir = entry.tasksDir
|
|
450
|
-
? (path.isAbsolute(entry.tasksDir) ? entry.tasksDir : path.join(root, entry.tasksDir))
|
|
451
|
-
: path.join(resolvedConfigDir, "tasks");
|
|
452
|
-
return {
|
|
453
|
-
pipelineJsonPath: path.join(resolvedConfigDir, "pipeline.json"),
|
|
454
|
-
tasksDir: resolvedTasksDir,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
throw new Error(`Pipeline '${slug}' not found in registry`);
|
|
370
|
+
return resolvePipelineSync(root, slug);
|
|
459
371
|
}
|
|
460
372
|
|
|
461
373
|
const config = getConfig();
|
package/src/core/control.ts
CHANGED
|
@@ -26,9 +26,12 @@ export const MAX_RUN_TASKS = 64;
|
|
|
26
26
|
|
|
27
27
|
export class ControlValidationError extends Error {
|
|
28
28
|
override name = "ControlValidationError";
|
|
29
|
+
readonly violations: string[];
|
|
29
30
|
|
|
30
31
|
constructor(violations: string[]) {
|
|
32
|
+
const normalizedViolations = [...violations];
|
|
31
33
|
super(violations.join("\n"));
|
|
34
|
+
this.violations = normalizedViolations;
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|