@synergenius/flowweaver-pack-weaver 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bot/ai-client.d.ts +1 -0
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +52 -1
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/audit-logger.d.ts +5 -0
- package/dist/bot/audit-logger.d.ts.map +1 -0
- package/dist/bot/audit-logger.js +42 -0
- package/dist/bot/audit-logger.js.map +1 -0
- package/dist/bot/audit-store.d.ts +13 -0
- package/dist/bot/audit-store.d.ts.map +1 -0
- package/dist/bot/audit-store.js +59 -0
- package/dist/bot/audit-store.js.map +1 -0
- package/dist/bot/cli-provider.d.ts +1 -0
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +86 -22
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/cli-stream-parser.d.ts +11 -0
- package/dist/bot/cli-stream-parser.d.ts.map +1 -0
- package/dist/bot/cli-stream-parser.js +53 -0
- package/dist/bot/cli-stream-parser.js.map +1 -0
- package/dist/bot/file-validator.d.ts +1 -1
- package/dist/bot/file-validator.d.ts.map +1 -1
- package/dist/bot/file-validator.js +13 -27
- package/dist/bot/file-validator.js.map +1 -1
- package/dist/bot/fw-api.d.ts +8 -0
- package/dist/bot/fw-api.d.ts.map +1 -0
- package/dist/bot/fw-api.js +12 -0
- package/dist/bot/fw-api.js.map +1 -0
- package/dist/bot/runner.d.ts +2 -1
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +8 -0
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts +3 -2
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +9 -30
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts +13 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +28 -22
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/types.d.ts +9 -1
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +2 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +2 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +69 -0
- package/dist/cli-handlers.js.map +1 -1
- package/dist/node-types/approval-gate.d.ts.map +1 -1
- package/dist/node-types/approval-gate.js +4 -0
- package/dist/node-types/approval-gate.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +10 -4
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/execute-plan.js +1 -1
- package/dist/node-types/execute-plan.js.map +1 -1
- package/dist/node-types/git-ops.d.ts.map +1 -1
- package/dist/node-types/git-ops.js +2 -0
- package/dist/node-types/git-ops.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +9 -1
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/send-notify.d.ts.map +1 -1
- package/dist/node-types/send-notify.js +4 -1
- package/dist/node-types/send-notify.js.map +1 -1
- package/dist/node-types/validate-result.d.ts +2 -2
- package/dist/node-types/validate-result.d.ts.map +1 -1
- package/dist/node-types/validate-result.js +2 -2
- package/dist/node-types/validate-result.js.map +1 -1
- package/dist/workflows/weaver-bot-batch.d.ts +4 -1
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +1 -1
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +4 -1
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +1 -1
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +3 -2
- package/src/bot/agent-provider.ts +273 -0
- package/src/bot/ai-client.ts +109 -0
- package/src/bot/approvals.ts +273 -0
- package/src/bot/audit-logger.ts +45 -0
- package/src/bot/audit-store.ts +69 -0
- package/src/bot/bot-agent-channel.ts +99 -0
- package/src/bot/cli-provider.ts +169 -0
- package/src/bot/cli-stream-parser.ts +59 -0
- package/src/bot/cost-store.ts +92 -0
- package/src/bot/cost-tracker.ts +72 -0
- package/src/bot/cron-parser.ts +153 -0
- package/src/bot/cron-scheduler.ts +48 -0
- package/src/bot/dashboard.ts +658 -0
- package/src/bot/design-checker.ts +327 -0
- package/src/bot/file-lock.ts +73 -0
- package/src/bot/file-validator.ts +41 -0
- package/src/bot/file-watcher.ts +103 -0
- package/src/bot/fw-api.ts +18 -0
- package/src/bot/genesis-prompt-context.ts +135 -0
- package/src/bot/genesis-store.ts +180 -0
- package/src/bot/index.ts +127 -0
- package/src/bot/notifications.ts +263 -0
- package/src/bot/pipeline-runner.ts +324 -0
- package/src/bot/provider-registry.ts +236 -0
- package/src/bot/run-store.ts +169 -0
- package/src/bot/runner.ts +311 -0
- package/src/bot/session-state.ts +73 -0
- package/src/bot/steering.ts +44 -0
- package/src/bot/step-executor.ts +34 -0
- package/src/bot/system-prompt.ts +280 -0
- package/src/bot/task-queue.ts +111 -0
- package/src/bot/types.ts +571 -0
- package/src/bot/utils.ts +17 -0
- package/src/bot/watch-daemon.ts +203 -0
- package/src/bot/web-approval.ts +240 -0
- package/src/cli-bridge.ts +41 -0
- package/src/cli-handlers.ts +1271 -0
- package/src/docs/weaver-config.md +135 -0
- package/src/index.ts +173 -0
- package/src/mcp-tools.ts +274 -0
- package/src/node-types/abort-task.ts +31 -0
- package/src/node-types/approval-gate.ts +75 -0
- package/src/node-types/bot-report.ts +82 -0
- package/src/node-types/build-context.ts +65 -0
- package/src/node-types/detect-provider.ts +75 -0
- package/src/node-types/exec-validate-retry.ts +175 -0
- package/src/node-types/execute-plan.ts +130 -0
- package/src/node-types/execute-target.ts +267 -0
- package/src/node-types/fix-errors.ts +68 -0
- package/src/node-types/genesis-apply-retry.ts +138 -0
- package/src/node-types/genesis-apply.ts +96 -0
- package/src/node-types/genesis-approve.ts +73 -0
- package/src/node-types/genesis-check-stabilize.ts +37 -0
- package/src/node-types/genesis-check-threshold.ts +34 -0
- package/src/node-types/genesis-commit.ts +71 -0
- package/src/node-types/genesis-compile-validate.ts +77 -0
- package/src/node-types/genesis-diff-fingerprint.ts +67 -0
- package/src/node-types/genesis-diff-workflow.ts +71 -0
- package/src/node-types/genesis-escrow-grace.ts +62 -0
- package/src/node-types/genesis-escrow-migrate.ts +138 -0
- package/src/node-types/genesis-escrow-recover.ts +99 -0
- package/src/node-types/genesis-escrow-stage.ts +104 -0
- package/src/node-types/genesis-escrow-validate.ts +120 -0
- package/src/node-types/genesis-load-config.ts +44 -0
- package/src/node-types/genesis-observe.ts +119 -0
- package/src/node-types/genesis-propose.ts +97 -0
- package/src/node-types/genesis-report.ts +95 -0
- package/src/node-types/genesis-snapshot.ts +30 -0
- package/src/node-types/genesis-try-apply.ts +165 -0
- package/src/node-types/genesis-update-history.ts +72 -0
- package/src/node-types/genesis-validate-proposal.ts +124 -0
- package/src/node-types/git-ops.ts +72 -0
- package/src/node-types/index.ts +36 -0
- package/src/node-types/load-config.ts +27 -0
- package/src/node-types/plan-task.ts +77 -0
- package/src/node-types/read-workflow.ts +68 -0
- package/src/node-types/receive-task.ts +92 -0
- package/src/node-types/report.ts +25 -0
- package/src/node-types/resolve-target.ts +64 -0
- package/src/node-types/route-task.ts +25 -0
- package/src/node-types/send-notify.ts +75 -0
- package/src/node-types/validate-result.ts +49 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/weaver-bot-template.ts +106 -0
- package/src/workflows/genesis-task.ts +91 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/weaver-bot-batch.ts +65 -0
- package/src/workflows/weaver-bot.ts +79 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type {
|
|
4
|
+
PipelineConfig,
|
|
5
|
+
PipelineResult,
|
|
6
|
+
PipelineStage,
|
|
7
|
+
StageCondition,
|
|
8
|
+
StageResult,
|
|
9
|
+
StageStatus,
|
|
10
|
+
WorkflowResult,
|
|
11
|
+
WeaverConfig,
|
|
12
|
+
ExecutionEvent,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import type { NotificationErrorHandler } from './notifications.js';
|
|
15
|
+
import { runWorkflow } from './runner.js';
|
|
16
|
+
|
|
17
|
+
export interface PipelineRunOptions {
|
|
18
|
+
verbose?: boolean;
|
|
19
|
+
dryRun?: boolean;
|
|
20
|
+
config?: WeaverConfig;
|
|
21
|
+
stage?: string;
|
|
22
|
+
onStageEvent?: (stageId: string, status: StageStatus, result?: WorkflowResult) => void;
|
|
23
|
+
onEvent?: (event: ExecutionEvent) => void;
|
|
24
|
+
onNotificationError?: NotificationErrorHandler;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class PipelineRunner {
|
|
28
|
+
static load(configPath: string): PipelineConfig {
|
|
29
|
+
const absPath = path.resolve(configPath);
|
|
30
|
+
if (!fs.existsSync(absPath)) {
|
|
31
|
+
throw new Error(`Pipeline config not found: ${absPath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const raw = JSON.parse(fs.readFileSync(absPath, 'utf-8')) as PipelineConfig;
|
|
35
|
+
const configDir = path.dirname(absPath);
|
|
36
|
+
|
|
37
|
+
// Resolve workflow paths relative to config file
|
|
38
|
+
for (const stage of raw.stages) {
|
|
39
|
+
if (!path.isAbsolute(stage.workflow)) {
|
|
40
|
+
stage.workflow = path.resolve(configDir, stage.workflow);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async run(config: PipelineConfig, options?: PipelineRunOptions): Promise<PipelineResult> {
|
|
48
|
+
this.validate(config);
|
|
49
|
+
|
|
50
|
+
const stageMap = new Map(config.stages.map((s) => [s.id, s]));
|
|
51
|
+
const waves = this.topologicalWaves(config.stages);
|
|
52
|
+
const results: Record<string, StageResult> = {};
|
|
53
|
+
const stageOrder: string[] = [];
|
|
54
|
+
let aborted = false;
|
|
55
|
+
const pipelineStart = Date.now();
|
|
56
|
+
|
|
57
|
+
// If filtering to a single stage, compute transitive deps
|
|
58
|
+
const activeIds = options?.stage
|
|
59
|
+
? this.transitiveDeps(options.stage, stageMap)
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
for (const wave of waves) {
|
|
63
|
+
const waveStages = activeIds
|
|
64
|
+
? wave.filter((id) => activeIds.has(id))
|
|
65
|
+
: wave;
|
|
66
|
+
|
|
67
|
+
if (waveStages.length === 0) continue;
|
|
68
|
+
|
|
69
|
+
const promises = waveStages.map(async (stageId) => {
|
|
70
|
+
const stage = stageMap.get(stageId)!;
|
|
71
|
+
const condition = stage.condition ?? 'on-success';
|
|
72
|
+
|
|
73
|
+
if (aborted && condition !== 'always') {
|
|
74
|
+
const sr: StageResult = { id: stageId, status: 'cancelled', workflowResult: null, durationMs: 0, wave: waves.indexOf(wave) };
|
|
75
|
+
results[stageId] = sr;
|
|
76
|
+
stageOrder.push(stageId);
|
|
77
|
+
options?.onStageEvent?.(stageId, 'cancelled');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!this.shouldRun(stage, results)) {
|
|
82
|
+
const sr: StageResult = { id: stageId, status: 'skipped', workflowResult: null, durationMs: 0, wave: waves.indexOf(wave) };
|
|
83
|
+
results[stageId] = sr;
|
|
84
|
+
stageOrder.push(stageId);
|
|
85
|
+
options?.onStageEvent?.(stageId, 'skipped');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
options?.onStageEvent?.(stageId, 'running');
|
|
90
|
+
const stageStart = Date.now();
|
|
91
|
+
|
|
92
|
+
// Merge params with upstream results
|
|
93
|
+
const params: Record<string, unknown> = {
|
|
94
|
+
...(stage.params ?? {}),
|
|
95
|
+
__stages: this.buildStageContext(stage, results),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const timeout = stage.timeoutSeconds ?? config.defaultTimeoutSeconds;
|
|
100
|
+
const workflowPromise = runWorkflow(stage.workflow, {
|
|
101
|
+
params,
|
|
102
|
+
verbose: options?.verbose,
|
|
103
|
+
dryRun: options?.dryRun,
|
|
104
|
+
config: config.config ?? options?.config,
|
|
105
|
+
onEvent: options?.onEvent,
|
|
106
|
+
onNotificationError: options?.onNotificationError,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let workflowResult: WorkflowResult;
|
|
110
|
+
if (timeout) {
|
|
111
|
+
workflowResult = await Promise.race([
|
|
112
|
+
workflowPromise,
|
|
113
|
+
new Promise<never>((_, reject) =>
|
|
114
|
+
setTimeout(() => reject(new Error(`Stage "${stageId}" timed out after ${timeout}s`)), timeout * 1000),
|
|
115
|
+
),
|
|
116
|
+
]);
|
|
117
|
+
} else {
|
|
118
|
+
workflowResult = await workflowPromise;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const status: StageStatus = workflowResult.success ? 'completed' : 'failed';
|
|
122
|
+
const sr: StageResult = {
|
|
123
|
+
id: stageId,
|
|
124
|
+
status,
|
|
125
|
+
workflowResult,
|
|
126
|
+
durationMs: Date.now() - stageStart,
|
|
127
|
+
wave: waves.indexOf(wave),
|
|
128
|
+
};
|
|
129
|
+
results[stageId] = sr;
|
|
130
|
+
stageOrder.push(stageId);
|
|
131
|
+
options?.onStageEvent?.(stageId, status, workflowResult);
|
|
132
|
+
|
|
133
|
+
if (!workflowResult.success && config.failFast !== false) {
|
|
134
|
+
aborted = true;
|
|
135
|
+
}
|
|
136
|
+
} catch (err: unknown) {
|
|
137
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
138
|
+
const sr: StageResult = {
|
|
139
|
+
id: stageId,
|
|
140
|
+
status: 'failed',
|
|
141
|
+
workflowResult: null,
|
|
142
|
+
durationMs: Date.now() - stageStart,
|
|
143
|
+
error: msg,
|
|
144
|
+
wave: waves.indexOf(wave),
|
|
145
|
+
};
|
|
146
|
+
results[stageId] = sr;
|
|
147
|
+
stageOrder.push(stageId);
|
|
148
|
+
options?.onStageEvent?.(stageId, 'failed');
|
|
149
|
+
|
|
150
|
+
if (config.failFast !== false) {
|
|
151
|
+
aborted = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await Promise.allSettled(promises);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const allResults = Object.values(results);
|
|
160
|
+
const anyFailed = allResults.some((r) => r.status === 'failed');
|
|
161
|
+
const anyCancelled = allResults.some((r) => r.status === 'cancelled');
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: !anyFailed && !anyCancelled,
|
|
165
|
+
outcome: anyFailed ? 'failed' : anyCancelled ? 'cancelled' : 'completed',
|
|
166
|
+
durationMs: Date.now() - pipelineStart,
|
|
167
|
+
stages: results,
|
|
168
|
+
stageOrder,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private validate(config: PipelineConfig): void {
|
|
173
|
+
if (!config.stages || config.stages.length === 0) {
|
|
174
|
+
throw new Error('Pipeline must have at least one stage');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ids = new Set<string>();
|
|
178
|
+
for (const stage of config.stages) {
|
|
179
|
+
if (ids.has(stage.id)) {
|
|
180
|
+
throw new Error(`Duplicate stage id: "${stage.id}"`);
|
|
181
|
+
}
|
|
182
|
+
ids.add(stage.id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const stage of config.stages) {
|
|
186
|
+
for (const dep of stage.dependsOn ?? []) {
|
|
187
|
+
if (!ids.has(dep)) {
|
|
188
|
+
throw new Error(`Stage "${stage.id}" depends on unknown stage "${dep}"`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.detectCycle(config.stages);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private detectCycle(stages: PipelineStage[]): void {
|
|
197
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
198
|
+
const color = new Map<string, number>();
|
|
199
|
+
const parent = new Map<string, string>();
|
|
200
|
+
const adj = new Map<string, string[]>();
|
|
201
|
+
|
|
202
|
+
for (const s of stages) {
|
|
203
|
+
color.set(s.id, WHITE);
|
|
204
|
+
adj.set(s.id, s.dependsOn ?? []);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const dfs = (id: string): string | null => {
|
|
208
|
+
color.set(id, GRAY);
|
|
209
|
+
for (const dep of adj.get(id) ?? []) {
|
|
210
|
+
if (color.get(dep) === GRAY) {
|
|
211
|
+
// Reconstruct cycle
|
|
212
|
+
const cycle = [dep, id];
|
|
213
|
+
let cur = id;
|
|
214
|
+
while (parent.has(cur) && parent.get(cur) !== dep) {
|
|
215
|
+
cur = parent.get(cur)!;
|
|
216
|
+
cycle.push(cur);
|
|
217
|
+
}
|
|
218
|
+
return cycle.reverse().join(' -> ');
|
|
219
|
+
}
|
|
220
|
+
if (color.get(dep) === WHITE) {
|
|
221
|
+
parent.set(dep, id);
|
|
222
|
+
const result = dfs(dep);
|
|
223
|
+
if (result) return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
color.set(id, BLACK);
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
for (const s of stages) {
|
|
231
|
+
if (color.get(s.id) === WHITE) {
|
|
232
|
+
const cycle = dfs(s.id);
|
|
233
|
+
if (cycle) {
|
|
234
|
+
throw new Error(`Circular dependency detected: ${cycle}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private topologicalWaves(stages: PipelineStage[]): string[][] {
|
|
241
|
+
const inDegree = new Map<string, number>();
|
|
242
|
+
const dependents = new Map<string, string[]>();
|
|
243
|
+
|
|
244
|
+
for (const s of stages) {
|
|
245
|
+
inDegree.set(s.id, (s.dependsOn ?? []).length);
|
|
246
|
+
for (const dep of s.dependsOn ?? []) {
|
|
247
|
+
const list = dependents.get(dep) ?? [];
|
|
248
|
+
list.push(s.id);
|
|
249
|
+
dependents.set(dep, list);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const waves: string[][] = [];
|
|
254
|
+
let remaining = stages.length;
|
|
255
|
+
|
|
256
|
+
while (remaining > 0) {
|
|
257
|
+
const wave: string[] = [];
|
|
258
|
+
for (const [id, deg] of inDegree) {
|
|
259
|
+
if (deg === 0) wave.push(id);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (wave.length === 0) break; // should not happen after cycle check
|
|
263
|
+
|
|
264
|
+
for (const id of wave) {
|
|
265
|
+
inDegree.delete(id);
|
|
266
|
+
for (const dep of dependents.get(id) ?? []) {
|
|
267
|
+
inDegree.set(dep, (inDegree.get(dep) ?? 1) - 1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
waves.push(wave);
|
|
272
|
+
remaining -= wave.length;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return waves;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private transitiveDeps(stageId: string, stageMap: Map<string, PipelineStage>): Set<string> {
|
|
279
|
+
const result = new Set<string>();
|
|
280
|
+
const visit = (id: string) => {
|
|
281
|
+
if (result.has(id)) return;
|
|
282
|
+
result.add(id);
|
|
283
|
+
const stage = stageMap.get(id);
|
|
284
|
+
if (!stage) throw new Error(`Unknown stage: "${id}"`);
|
|
285
|
+
for (const dep of stage.dependsOn ?? []) {
|
|
286
|
+
visit(dep);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
visit(stageId);
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private shouldRun(stage: PipelineStage, results: Record<string, StageResult>): boolean {
|
|
294
|
+
const condition: StageCondition = stage.condition ?? 'on-success';
|
|
295
|
+
const deps = stage.dependsOn ?? [];
|
|
296
|
+
|
|
297
|
+
if (deps.length === 0) return true;
|
|
298
|
+
|
|
299
|
+
const allSucceeded = deps.every((d) => results[d]?.status === 'completed');
|
|
300
|
+
const someFailed = deps.some((d) => results[d]?.status === 'failed');
|
|
301
|
+
|
|
302
|
+
switch (condition) {
|
|
303
|
+
case 'on-success':
|
|
304
|
+
return allSucceeded;
|
|
305
|
+
case 'on-failure':
|
|
306
|
+
return someFailed;
|
|
307
|
+
case 'always':
|
|
308
|
+
return true;
|
|
309
|
+
default:
|
|
310
|
+
return allSucceeded;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private buildStageContext(
|
|
315
|
+
stage: PipelineStage,
|
|
316
|
+
results: Record<string, StageResult>,
|
|
317
|
+
): Record<string, WorkflowResult | null> {
|
|
318
|
+
const ctx: Record<string, WorkflowResult | null> = {};
|
|
319
|
+
for (const dep of stage.dependsOn ?? []) {
|
|
320
|
+
ctx[dep] = results[dep]?.workflowResult ?? null;
|
|
321
|
+
}
|
|
322
|
+
return ctx;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import type {
|
|
5
|
+
BotAgentProvider,
|
|
6
|
+
BotProviderConfig,
|
|
7
|
+
ProviderFactory,
|
|
8
|
+
ProviderFactoryConfig,
|
|
9
|
+
ProviderMetadata,
|
|
10
|
+
ProviderModule,
|
|
11
|
+
} from './types.js';
|
|
12
|
+
|
|
13
|
+
interface RegistryEntry {
|
|
14
|
+
factory: ProviderFactory;
|
|
15
|
+
metadata: ProviderMetadata;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ProviderRegistry {
|
|
19
|
+
private factories = new Map<string, RegistryEntry>();
|
|
20
|
+
|
|
21
|
+
register(name: string, factory: ProviderFactory, metadata: ProviderMetadata): void {
|
|
22
|
+
this.factories.set(name, { factory, metadata });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resolve(name: string): RegistryEntry | undefined {
|
|
26
|
+
return this.factories.get(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
has(name: string): boolean {
|
|
30
|
+
return this.factories.has(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
list(): Array<{ name: string; metadata: ProviderMetadata }> {
|
|
34
|
+
return Array.from(this.factories.entries()).map(([name, entry]) => ({
|
|
35
|
+
name,
|
|
36
|
+
metadata: entry.metadata,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const defaultRegistry = new ProviderRegistry();
|
|
42
|
+
|
|
43
|
+
defaultRegistry.register(
|
|
44
|
+
'anthropic',
|
|
45
|
+
async (config) => {
|
|
46
|
+
const { AnthropicAgentProvider } = await import('./agent-provider.js');
|
|
47
|
+
return new AnthropicAgentProvider({ name: 'anthropic', model: config.model, maxTokens: config.maxTokens });
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
displayName: 'Anthropic API',
|
|
51
|
+
description: 'Direct Anthropic API calls via @anthropic-ai/sdk',
|
|
52
|
+
source: 'built-in',
|
|
53
|
+
requiredEnvVars: ['ANTHROPIC_API_KEY'],
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
defaultRegistry.register(
|
|
58
|
+
'claude-cli',
|
|
59
|
+
async (config) => {
|
|
60
|
+
const { CliAgentProvider } = await import('./cli-provider.js');
|
|
61
|
+
return new CliAgentProvider('claude-cli', config.model);
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
displayName: 'Claude CLI',
|
|
65
|
+
description: 'Claude Code CLI (claude -p)',
|
|
66
|
+
source: 'built-in',
|
|
67
|
+
detectCliCommand: 'claude',
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
defaultRegistry.register(
|
|
72
|
+
'copilot-cli',
|
|
73
|
+
async (config) => {
|
|
74
|
+
const { CliAgentProvider } = await import('./cli-provider.js');
|
|
75
|
+
return new CliAgentProvider('copilot-cli', config.model);
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
displayName: 'GitHub Copilot CLI',
|
|
79
|
+
description: 'GitHub Copilot CLI (copilot -p)',
|
|
80
|
+
source: 'built-in',
|
|
81
|
+
detectCliCommand: 'copilot',
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export async function loadExternalProvider(
|
|
86
|
+
moduleSpec: string,
|
|
87
|
+
): Promise<{ factory: ProviderFactory; metadata: ProviderMetadata }> {
|
|
88
|
+
let mod: Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (moduleSpec.startsWith('.') || moduleSpec.startsWith('/')) {
|
|
92
|
+
const absPath = path.resolve(moduleSpec);
|
|
93
|
+
const fileUrl = pathToFileURL(absPath).href;
|
|
94
|
+
mod = await import(fileUrl);
|
|
95
|
+
} else {
|
|
96
|
+
mod = await import(moduleSpec);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
const isLocal = moduleSpec.startsWith('.') || moduleSpec.startsWith('/');
|
|
101
|
+
const hint = isLocal
|
|
102
|
+
? `Check the path exists: ${path.resolve(moduleSpec)}`
|
|
103
|
+
: `Install it with: npm install ${moduleSpec}`;
|
|
104
|
+
throw new Error(`Failed to load provider from "${moduleSpec}": ${msg}\n ${hint}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Accept: default export object with createProvider, named createProvider, or default function
|
|
108
|
+
const defaultExport = (mod.default ?? mod) as Record<string, unknown>;
|
|
109
|
+
let factory: ProviderFactory;
|
|
110
|
+
let metadata: ProviderMetadata;
|
|
111
|
+
|
|
112
|
+
if (typeof defaultExport === 'function') {
|
|
113
|
+
factory = defaultExport as ProviderFactory;
|
|
114
|
+
metadata = { displayName: moduleSpec, source: moduleSpec.startsWith('.') || moduleSpec.startsWith('/') ? 'local' : 'npm' };
|
|
115
|
+
} else if (typeof defaultExport.createProvider === 'function') {
|
|
116
|
+
const providerModule = defaultExport as unknown as ProviderModule;
|
|
117
|
+
factory = providerModule.createProvider;
|
|
118
|
+
metadata = providerModule.metadata ?? {
|
|
119
|
+
displayName: moduleSpec,
|
|
120
|
+
source: moduleSpec.startsWith('.') || moduleSpec.startsWith('/') ? 'local' : 'npm',
|
|
121
|
+
};
|
|
122
|
+
} else if (typeof mod.createProvider === 'function') {
|
|
123
|
+
factory = mod.createProvider as ProviderFactory;
|
|
124
|
+
metadata = (mod.metadata as ProviderMetadata) ?? {
|
|
125
|
+
displayName: moduleSpec,
|
|
126
|
+
source: moduleSpec.startsWith('.') || moduleSpec.startsWith('/') ? 'local' : 'npm',
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Provider module "${moduleSpec}" must export a createProvider function ` +
|
|
131
|
+
`(as default export, default.createProvider, or named export)`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { factory, metadata };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let discoveredProviders: Array<{ name: string; metadata: ProviderMetadata }> | null = null;
|
|
139
|
+
|
|
140
|
+
export async function discoverProviders(
|
|
141
|
+
registry: ProviderRegistry = defaultRegistry,
|
|
142
|
+
): Promise<Array<{ name: string; metadata: ProviderMetadata }>> {
|
|
143
|
+
if (discoveredProviders) return discoveredProviders;
|
|
144
|
+
|
|
145
|
+
const discovered: Array<{ name: string; metadata: ProviderMetadata }> = [];
|
|
146
|
+
let dir = process.cwd();
|
|
147
|
+
|
|
148
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
149
|
+
const nodeModules = path.join(dir, 'node_modules');
|
|
150
|
+
if (fs.existsSync(nodeModules)) {
|
|
151
|
+
await scanNodeModules(nodeModules, registry, discovered);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
const parent = path.dirname(dir);
|
|
155
|
+
if (parent === dir) break;
|
|
156
|
+
dir = parent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
discoveredProviders = discovered;
|
|
160
|
+
return discovered;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function scanNodeModules(
|
|
164
|
+
nodeModulesDir: string,
|
|
165
|
+
registry: ProviderRegistry,
|
|
166
|
+
results: Array<{ name: string; metadata: ProviderMetadata }>,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
let entries: fs.Dirent[];
|
|
169
|
+
try {
|
|
170
|
+
entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (!entry.isDirectory()) continue;
|
|
177
|
+
|
|
178
|
+
if (entry.name.startsWith('@')) {
|
|
179
|
+
// Scoped packages
|
|
180
|
+
const scopeDir = path.join(nodeModulesDir, entry.name);
|
|
181
|
+
let scopeEntries: fs.Dirent[];
|
|
182
|
+
try {
|
|
183
|
+
scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
for (const scopeEntry of scopeEntries) {
|
|
188
|
+
if (!scopeEntry.isDirectory()) continue;
|
|
189
|
+
await checkProviderPackage(
|
|
190
|
+
path.join(scopeDir, scopeEntry.name),
|
|
191
|
+
`${entry.name}/${scopeEntry.name}`,
|
|
192
|
+
registry,
|
|
193
|
+
results,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
await checkProviderPackage(
|
|
198
|
+
path.join(nodeModulesDir, entry.name),
|
|
199
|
+
entry.name,
|
|
200
|
+
registry,
|
|
201
|
+
results,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function checkProviderPackage(
|
|
208
|
+
pkgDir: string,
|
|
209
|
+
pkgName: string,
|
|
210
|
+
registry: ProviderRegistry,
|
|
211
|
+
results: Array<{ name: string; metadata: ProviderMetadata }>,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
|
214
|
+
if (!fs.existsSync(pkgJsonPath)) return;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
218
|
+
const keywords: string[] = pkgJson.keywords ?? [];
|
|
219
|
+
if (!keywords.includes('flowweaver-provider')) return;
|
|
220
|
+
|
|
221
|
+
// Derive provider name
|
|
222
|
+
const providerName: string =
|
|
223
|
+
pkgJson.flowWeaver?.providerName ??
|
|
224
|
+
pkgName
|
|
225
|
+
.replace(/^@[^/]+\//, '')
|
|
226
|
+
.replace(/^flowweaver-provider-/, '');
|
|
227
|
+
|
|
228
|
+
if (registry.has(providerName)) return;
|
|
229
|
+
|
|
230
|
+
const { factory, metadata } = await loadExternalProvider(pkgName);
|
|
231
|
+
registry.register(providerName, factory, metadata);
|
|
232
|
+
results.push({ name: providerName, metadata });
|
|
233
|
+
} catch {
|
|
234
|
+
// Skip packages that fail to load
|
|
235
|
+
}
|
|
236
|
+
}
|