@geminix/gxpm 0.1.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/AGENTS.md +148 -0
- package/CANON.md +53 -0
- package/CLAUDE.md +60 -0
- package/CONTEXT.md +49 -0
- package/DEBUG.md +59 -0
- package/ISSUE_CONTEXT.md +25 -0
- package/README.md +143 -0
- package/VERSION +1 -0
- package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
- package/agents/grill-master.md +26 -0
- package/agents/implementer.md +32 -0
- package/agents/review-army/accessibility-reviewer.md +54 -0
- package/agents/review-army/code-quality-reviewer.md +54 -0
- package/agents/review-army/security-reviewer.md +56 -0
- package/agents/review-army/spec-compliance-reviewer.md +51 -0
- package/agents/review-army/test-reviewer.md +55 -0
- package/agents/reviewer.md +59 -0
- package/agents/ship-audit-army/docs-auditor.md +53 -0
- package/agents/ship-audit-army/performance-auditor.md +52 -0
- package/agents/ship-audit-army/security-auditor.md +52 -0
- package/agents/specifier.md +55 -0
- package/agents/triage-officer.md +27 -0
- package/bin/gxpm +17 -0
- package/bin/gxpm-browser +17 -0
- package/bin/gxpm-config +15 -0
- package/bin/gxpm-eval +13 -0
- package/bin/gxpm-global-discover +15 -0
- package/bin/gxpm-init +38 -0
- package/bin/gxpm-investigate +194 -0
- package/bin/gxpm-uninstall +15 -0
- package/bin/gxpm-update-check +165 -0
- package/commands/build.md +40 -0
- package/commands/help.md +53 -0
- package/commands/plan.md +34 -0
- package/commands/refine.md +46 -0
- package/commands/review.md +34 -0
- package/commands/ship.md +37 -0
- package/core/ac-check.ts +20 -0
- package/core/agent-runtime.ts +363 -0
- package/core/artifact-validator.ts +151 -0
- package/core/artifacts.ts +313 -0
- package/core/autopilot.ts +250 -0
- package/core/capabilities.ts +779 -0
- package/core/checkpoint.ts +370 -0
- package/core/cleanup.ts +32 -0
- package/core/command-probe.ts +82 -0
- package/core/config.ts +533 -0
- package/core/contracts/behavior-spec.schema.ts +38 -0
- package/core/contracts/converter.ts +61 -0
- package/core/contracts/host.ts +43 -0
- package/core/converters/converter.ts +93 -0
- package/core/converters/index.ts +8 -0
- package/core/converters/managed-artifact.ts +119 -0
- package/core/converters/parser.ts +159 -0
- package/core/converters/template-renderer.ts +35 -0
- package/core/converters/writer.ts +61 -0
- package/core/dag-executor.ts +426 -0
- package/core/dag-loader.ts +292 -0
- package/core/dag-schemas.ts +150 -0
- package/core/dispatch.ts +125 -0
- package/core/evidence.ts +148 -0
- package/core/gate.ts +269 -0
- package/core/hook-engine.ts +566 -0
- package/core/host-probe.ts +64 -0
- package/core/implement.ts +16 -0
- package/core/isolation-errors.ts +174 -0
- package/core/isolation-resolver.ts +921 -0
- package/core/issue-context.ts +381 -0
- package/core/issue-readiness.ts +457 -0
- package/core/issue-sync.ts +427 -0
- package/core/issues.ts +132 -0
- package/core/land.ts +108 -0
- package/core/orchestrator.ts +54 -0
- package/core/phase-artifact.ts +32 -0
- package/core/phase-gates.ts +130 -0
- package/core/phase-rewind.ts +94 -0
- package/core/plan-lint.ts +61 -0
- package/core/plan.ts +77 -0
- package/core/port-allocation.ts +50 -0
- package/core/pr-check.ts +15 -0
- package/core/preset-system/preset-resolver.ts +221 -0
- package/core/project-init-status.ts +127 -0
- package/core/qa.ts +15 -0
- package/core/resilience.ts +165 -0
- package/core/runs.ts +288 -0
- package/core/safe-path.test.ts +80 -0
- package/core/safe-path.ts +60 -0
- package/core/sdd-gate.test.ts +98 -0
- package/core/sdd-gate.ts +134 -0
- package/core/self-review.ts +62 -0
- package/core/session.ts +70 -0
- package/core/ship.ts +86 -0
- package/core/specify.ts +173 -0
- package/core/state.ts +1002 -0
- package/core/template-engine.ts +152 -0
- package/core/template-resolver.test.ts +70 -0
- package/core/template-resolver.ts +156 -0
- package/core/triage.ts +26 -0
- package/core/verify.ts +15 -0
- package/core/wiki-native.ts +2423 -0
- package/core/wiki.ts +27 -0
- package/core/workflow-event-emitter.ts +163 -0
- package/core/workflows/engine.ts +273 -0
- package/core/workflows/expressions.ts +76 -0
- package/core/workflows/index.ts +38 -0
- package/core/workflows/steps/command.ts +43 -0
- package/core/workflows/steps/gate.ts +47 -0
- package/core/workflows/steps/gxpm.ts +44 -0
- package/core/workflows/steps/linear.ts +31 -0
- package/core/workflows/steps/shell.ts +65 -0
- package/core/workflows/types.ts +62 -0
- package/core/workspace-runtime.ts +227 -0
- package/core/worktree-init-steps.ts +647 -0
- package/core/worktree-init.ts +330 -0
- package/core/worktree-owner.ts +143 -0
- package/docs/GXPM_VERIFY.md +98 -0
- package/docs/INSTALL_FOR_AGENTS.md +113 -0
- package/docs/README.md +57 -0
- package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
- package/docs/agents/domain.md +30 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +32 -0
- package/docs/architecture/gxpm-architecture-diagram.md +265 -0
- package/docs/architecture/gxpm-current-architecture.md +175 -0
- package/docs/architecture/gxpm-current-flow.md +278 -0
- package/docs/architecture/gxpm-replacement-architecture.md +211 -0
- package/docs/architecture/gxpm-target-architecture.md +449 -0
- package/docs/architecture/gxpm-v0-contract.md +311 -0
- package/docs/architecture/layered-workflow-boundaries.md +193 -0
- package/docs/architecture/preset-system.md +126 -0
- package/docs/architecture/scaffold-northstar.md +23 -0
- package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
- package/docs/brainstorms/README.md +22 -0
- package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
- package/docs/governance/beta-skill-promotion.md +39 -0
- package/docs/governance/development-contract.md +144 -0
- package/docs/governance/gherkin-style.md +90 -0
- package/docs/governance/host-adapter.md +56 -0
- package/docs/governance/skill-authoring.md +87 -0
- package/docs/governance/skill-testing.md +356 -0
- package/docs/governance/template-authoring.md +53 -0
- package/docs/migrations/v0.2.md +51 -0
- package/docs/plans/README.md +23 -0
- package/docs/plans/bdd-then-tdd-plan.md +1767 -0
- package/docs/plans/docs-knowledge-system-plan.md +31 -0
- package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
- package/docs/research/agents-md-best-practices.md +207 -0
- package/docs/research/archon-study.md +351 -0
- package/docs/research/claude-hooks-study.md +440 -0
- package/docs/research/codex-hooks-study.md +624 -0
- package/docs/research/everything-claude-code-study.md +252 -0
- package/docs/research/from-skills-to-layered-workflow.md +322 -0
- package/docs/research/gsd-study.md +69 -0
- package/docs/research/kimi-hooks-study.md +274 -0
- package/docs/research/mattpocock-skills-comparison.md +429 -0
- package/docs/research/mattpocock-skills-study.md +275 -0
- package/docs/research/oh-my-codex-study.md +279 -0
- package/docs/research/perplexity-agent-skills-design.md +168 -0
- package/docs/research/pmc-gstack-skill-study.md +122 -0
- package/docs/research/spec-kit-study.md +224 -0
- package/docs/research/superpowers-study.md +209 -0
- package/docs/roadmap/initial-roadmap.md +53 -0
- package/docs/solutions/README.md +45 -0
- package/docs/solutions/artifact-nesting-recovery.md +58 -0
- package/docs/solutions/session-context-restore-practice.md +67 -0
- package/docs/solutions/workflow/version-drift-recovery.md +49 -0
- package/docs/solutions/worktree-gate-recovery.md +62 -0
- package/docs/specs/README.md +28 -0
- package/docs/specs/claude.md +45 -0
- package/docs/specs/codex.md +44 -0
- package/docs/specs/cursor.md +44 -0
- package/hosts/adapters/claude.ts +29 -0
- package/hosts/adapters/codex.ts +27 -0
- package/hosts/adapters/cursor.ts +27 -0
- package/hosts/adapters/kimi.ts +27 -0
- package/hosts/claude.ts +23 -0
- package/hosts/codex.ts +26 -0
- package/hosts/cursor.ts +19 -0
- package/hosts/index.ts +33 -0
- package/hosts/registry.test.ts +52 -0
- package/hosts/registry.ts +57 -0
- package/hosts/schema.ts +58 -0
- package/package.json +52 -0
- package/scripts/browser.ts +185 -0
- package/scripts/cleanup.ts +142 -0
- package/scripts/commands/artifact.ts +115 -0
- package/scripts/commands/autopilot.ts +143 -0
- package/scripts/commands/capability.ts +57 -0
- package/scripts/commands/config.ts +69 -0
- package/scripts/commands/dag.ts +126 -0
- package/scripts/commands/feedback.ts +123 -0
- package/scripts/commands/gate.ts +291 -0
- package/scripts/commands/helpers.ts +126 -0
- package/scripts/commands/hook.ts +66 -0
- package/scripts/commands/init.ts +515 -0
- package/scripts/commands/issue.ts +825 -0
- package/scripts/commands/phase.ts +61 -0
- package/scripts/commands/preset.ts +159 -0
- package/scripts/commands/runtime.ts +199 -0
- package/scripts/commands/specify.ts +71 -0
- package/scripts/commands/upgrade.ts +243 -0
- package/scripts/commands/verify.ts +183 -0
- package/scripts/commands/wiki.ts +242 -0
- package/scripts/commands/workflow.ts +131 -0
- package/scripts/dev-skill.ts +55 -0
- package/scripts/discover-skills.ts +116 -0
- package/scripts/doctor.ts +410 -0
- package/scripts/dogfood-check.ts +125 -0
- package/scripts/eval-functional.ts +218 -0
- package/scripts/eval.ts +246 -0
- package/scripts/gen-skill-docs.ts +201 -0
- package/scripts/global-discover.ts +217 -0
- package/scripts/governance-check.ts +75 -0
- package/scripts/gxpm-check.ts +12 -0
- package/scripts/gxpm.ts +216 -0
- package/scripts/host-config.ts +62 -0
- package/scripts/install-claude-hooks.ts +138 -0
- package/scripts/install-codex-hooks.ts +271 -0
- package/scripts/install-hooks.ts +128 -0
- package/scripts/install-kimi-hooks.ts +92 -0
- package/scripts/install-skill.ts +184 -0
- package/scripts/phase-artifact-commands.ts +100 -0
- package/scripts/post-land-sync.ts +46 -0
- package/scripts/scaffold-check.ts +85 -0
- package/scripts/skill-naming-check.ts +78 -0
- package/scripts/skill-structure-check.ts +157 -0
- package/scripts/skills-lock-check.ts +60 -0
- package/scripts/sync-markdown-artifacts.ts +172 -0
- package/scripts/uninstall.ts +162 -0
- package/scripts/version.ts +47 -0
- package/scripts/wait-pr-ready.ts +407 -0
- package/skills/gxpm/SKILL.md +485 -0
- package/skills/gxpm/SKILL.md.tmpl +422 -0
- package/skills/gxpm/references/CANON.md +53 -0
- package/skills/gxpm/references/key-rules.md +130 -0
- package/skills/gxpm-architecture/SKILL.md +106 -0
- package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
- package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
- package/skills/gxpm-autopilot/SKILL.md +116 -0
- package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
- package/skills/gxpm-browser/SKILL.md +105 -0
- package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
- package/skills/gxpm-browser/references/commands.md +43 -0
- package/skills/gxpm-browser/references/evidence-path.md +20 -0
- package/skills/gxpm-build/SKILL.md +78 -0
- package/skills/gxpm-cleanup/SKILL.md +76 -0
- package/skills/gxpm-debug-issue/SKILL.md +39 -0
- package/skills/gxpm-diagnose/SKILL.md +220 -0
- package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
- package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
- package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
- package/skills/gxpm-diagnose/references/phases.md +60 -0
- package/skills/gxpm-eval/SKILL.md +78 -0
- package/skills/gxpm-explore-codebase/SKILL.md +36 -0
- package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
- package/skills/gxpm-feedback/SKILL.md +122 -0
- package/skills/gxpm-grill/SKILL.md +159 -0
- package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
- package/skills/gxpm-grill/references/documentation-templates.md +56 -0
- package/skills/gxpm-grill/references/process.md +25 -0
- package/skills/gxpm-handoff/SKILL.md +112 -0
- package/skills/gxpm-hygiene/SKILL.md +69 -0
- package/skills/gxpm-implementer/SKILL.md +142 -0
- package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
- package/skills/gxpm-linear/SKILL.md +282 -0
- package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
- package/skills/gxpm-linear/references/commands.md +75 -0
- package/skills/gxpm-linear/references/workflows.md +120 -0
- package/skills/gxpm-planning/SKILL.md +134 -0
- package/skills/gxpm-prototype/SKILL.md +64 -0
- package/skills/gxpm-refactor-safely/SKILL.md +62 -0
- package/skills/gxpm-review-army/SKILL.md +117 -0
- package/skills/gxpm-review-changes/SKILL.md +36 -0
- package/skills/gxpm-setup/SKILL.md +101 -0
- package/skills/gxpm-specifier/SKILL.md +135 -0
- package/skills/gxpm-tdd/SKILL.md +187 -0
- package/skills/gxpm-tdd/references/interface-design.md +23 -0
- package/skills/gxpm-tdd/references/mocking.md +27 -0
- package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
- package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
- package/skills/gxpm-tdd/references/workflow.md +50 -0
- package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
- package/skills/gxpm-triage/SKILL.md +160 -0
- package/skills/gxpm-verify/SKILL.md +107 -0
- package/skills/gxpm-write-skill/SKILL.md +131 -0
- package/skills/gxpm-zoom-out/SKILL.md +69 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
- package/templates/constitution-template.md +63 -0
- package/templates/hooks/gxpm-commit-msg +16 -0
- package/templates/hooks/gxpm-post-checkout +19 -0
- package/templates/hooks/gxpm-post-commit +7 -0
- package/templates/hooks/gxpm-post-merge +29 -0
- package/templates/hooks/gxpm-pre-commit +39 -0
- package/templates/hooks/gxpm-pre-push +33 -0
- package/templates/plan-template.md.tmpl +46 -0
- package/templates/spec-template.md.tmpl +63 -0
- package/templates/specify-stub.tmpl +22 -0
- package/templates/tasks-template.md.tmpl +32 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG Executor
|
|
3
|
+
*
|
|
4
|
+
* Executes a nodes-based workflow in topological order.
|
|
5
|
+
* Independent nodes within the same layer run concurrently via Promise.allSettled.
|
|
6
|
+
* Supports trigger rules, when conditions, and $node_id.output substitution.
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
DagNode,
|
|
10
|
+
TriggerRule,
|
|
11
|
+
NodeOutput,
|
|
12
|
+
NodeState,
|
|
13
|
+
DagExecutionResult,
|
|
14
|
+
} from "./dag-schemas";
|
|
15
|
+
import { isCancelNode } from "./dag-schemas";
|
|
16
|
+
|
|
17
|
+
export type NodeExecutor = (node: DagNode, context: DagContext) => Promise<NodeOutput>;
|
|
18
|
+
|
|
19
|
+
export interface DagContext {
|
|
20
|
+
workflowName: string;
|
|
21
|
+
nodeOutputs: Map<string, NodeOutput>;
|
|
22
|
+
cwd: string;
|
|
23
|
+
abortSignal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_NODE_MAX_RETRIES = 2;
|
|
27
|
+
const DEFAULT_NODE_RETRY_DELAY_MS = 3000;
|
|
28
|
+
|
|
29
|
+
function getEffectiveRetryConfig(node: DagNode): {
|
|
30
|
+
maxRetries: number;
|
|
31
|
+
delayMs: number;
|
|
32
|
+
onError: "transient" | "all";
|
|
33
|
+
} {
|
|
34
|
+
if (node.retry) {
|
|
35
|
+
return {
|
|
36
|
+
maxRetries: node.retry.max_attempts,
|
|
37
|
+
delayMs: node.retry.delay_ms ?? DEFAULT_NODE_RETRY_DELAY_MS,
|
|
38
|
+
onError: node.retry.on_error ?? "transient",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
maxRetries: DEFAULT_NODE_MAX_RETRIES,
|
|
43
|
+
delayMs: DEFAULT_NODE_RETRY_DELAY_MS,
|
|
44
|
+
onError: "transient",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function delay(ms: number): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build topological layers from DAG nodes using Kahn's algorithm.
|
|
54
|
+
* Layer 0: nodes with no dependencies.
|
|
55
|
+
* Layer N: nodes whose dependencies are all in layers 0..N-1.
|
|
56
|
+
*/
|
|
57
|
+
export function buildTopologicalLayers(nodes: readonly DagNode[]): DagNode[][] {
|
|
58
|
+
const inDegree = new Map<string, number>();
|
|
59
|
+
const dependents = new Map<string, string[]>();
|
|
60
|
+
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
inDegree.set(node.id, node.depends_on?.length ?? 0);
|
|
63
|
+
for (const dep of node.depends_on ?? []) {
|
|
64
|
+
const existing = dependents.get(dep) ?? [];
|
|
65
|
+
existing.push(node.id);
|
|
66
|
+
dependents.set(dep, existing);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const layers: DagNode[][] = [];
|
|
71
|
+
let ready = [...nodes].filter((n) => (inDegree.get(n.id) ?? 0) === 0);
|
|
72
|
+
|
|
73
|
+
while (ready.length > 0) {
|
|
74
|
+
layers.push(ready);
|
|
75
|
+
const nextIds: string[] = [];
|
|
76
|
+
for (const node of ready) {
|
|
77
|
+
for (const depId of dependents.get(node.id) ?? []) {
|
|
78
|
+
const newDegree = (inDegree.get(depId) ?? 0) - 1;
|
|
79
|
+
inDegree.set(depId, newDegree);
|
|
80
|
+
if (newDegree === 0) nextIds.push(depId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ready = nextIds
|
|
84
|
+
.map((id) => nodes.find((n) => n.id === id))
|
|
85
|
+
.filter((n): n is DagNode => n !== undefined);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const totalPlaced = layers.reduce((sum, l) => sum + l.length, 0);
|
|
89
|
+
if (totalPlaced < nodes.length) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"[DagExecutor] Cycle detected at runtime — was cycle detection skipped at load?"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return layers;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Evaluate trigger rule for a node given its upstream states.
|
|
100
|
+
*/
|
|
101
|
+
export function checkTriggerRule(
|
|
102
|
+
node: DagNode,
|
|
103
|
+
nodeOutputs: Map<string, NodeOutput>
|
|
104
|
+
): "run" | "skip" {
|
|
105
|
+
const nodeDeps = node.depends_on ?? [];
|
|
106
|
+
if (nodeDeps.length === 0) return "run";
|
|
107
|
+
|
|
108
|
+
const upstreams = nodeDeps.map(
|
|
109
|
+
(id) =>
|
|
110
|
+
nodeOutputs.get(id) ??
|
|
111
|
+
({
|
|
112
|
+
state: "failed",
|
|
113
|
+
output: "",
|
|
114
|
+
error: `upstream '${id}' missing from outputs`,
|
|
115
|
+
} as NodeOutput)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const rule: TriggerRule = node.trigger_rule ?? "all_success";
|
|
119
|
+
|
|
120
|
+
switch (rule) {
|
|
121
|
+
case "all_success":
|
|
122
|
+
return upstreams.every((u) => u.state === "completed") ? "run" : "skip";
|
|
123
|
+
case "one_success":
|
|
124
|
+
return upstreams.some((u) => u.state === "completed") ? "run" : "skip";
|
|
125
|
+
case "none_failed_min_one_success": {
|
|
126
|
+
const anyFailed = upstreams.some((u) => u.state === "failed");
|
|
127
|
+
const anySucceeded = upstreams.some((u) => u.state === "completed");
|
|
128
|
+
return !anyFailed && anySucceeded ? "run" : "skip";
|
|
129
|
+
}
|
|
130
|
+
case "all_done":
|
|
131
|
+
return upstreams.every((u) => u.state !== "pending" && u.state !== "running")
|
|
132
|
+
? "run"
|
|
133
|
+
: "skip";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Substitute $node_id.output and $node_id.output.field references in a string.
|
|
139
|
+
*/
|
|
140
|
+
export function substituteNodeOutputRefs(
|
|
141
|
+
text: string,
|
|
142
|
+
nodeOutputs: Map<string, NodeOutput>
|
|
143
|
+
): string {
|
|
144
|
+
return text.replace(
|
|
145
|
+
/\$([a-zA-Z_][a-zA-Z0-9_-]*)\.output(?:\.([a-zA-Z_][a-zA-Z0-9_]*))?/g,
|
|
146
|
+
(match, nodeId: string, field: string | undefined) => {
|
|
147
|
+
const nodeOutput = nodeOutputs.get(nodeId);
|
|
148
|
+
if (!nodeOutput) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
if (!field) {
|
|
152
|
+
return nodeOutput.output;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(nodeOutput.output) as Record<string, unknown>;
|
|
156
|
+
const value = parsed[field];
|
|
157
|
+
if (typeof value === "string") return value;
|
|
158
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
159
|
+
return "";
|
|
160
|
+
} catch {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Evaluate a when condition string against upstream node outputs.
|
|
169
|
+
* Supports simple expressions referencing $node_id.output and comparison operators.
|
|
170
|
+
* Returns true if the condition passes, false otherwise.
|
|
171
|
+
*/
|
|
172
|
+
export function evaluateWhenCondition(
|
|
173
|
+
condition: string,
|
|
174
|
+
nodeOutputs: Map<string, NodeOutput>
|
|
175
|
+
): boolean {
|
|
176
|
+
// Substitute output refs with JSON-stringified values for safe JS evaluation
|
|
177
|
+
const substituted = condition.replace(
|
|
178
|
+
/\$([a-zA-Z_][a-zA-Z0-9_-]*)\.output(?:\.([a-zA-Z_][a-zA-Z0-9_]*))?/g,
|
|
179
|
+
(match, nodeId: string, field: string | undefined) => {
|
|
180
|
+
const nodeOutput = nodeOutputs.get(nodeId);
|
|
181
|
+
if (!nodeOutput) {
|
|
182
|
+
return "null";
|
|
183
|
+
}
|
|
184
|
+
let value: unknown;
|
|
185
|
+
if (!field) {
|
|
186
|
+
value = nodeOutput.output;
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(nodeOutput.output) as Record<string, unknown>;
|
|
190
|
+
value = parsed[field];
|
|
191
|
+
} catch {
|
|
192
|
+
return "null";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (value === undefined || value === null) return "null";
|
|
196
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
197
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
198
|
+
return "null";
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Whitelist-only expression evaluator
|
|
203
|
+
// Allowed: literals (string, number, boolean), comparisons (==, !=, <, >, <=, >=),
|
|
204
|
+
// logical operators (&&, ||, !), and grouping parentheses.
|
|
205
|
+
const sanitized = substituted.trim();
|
|
206
|
+
|
|
207
|
+
if (!sanitized) return true;
|
|
208
|
+
|
|
209
|
+
// Security: reject any characters outside the whitelist
|
|
210
|
+
const whitelist = /^[\s\w\d_+\-*/%=<>!&|().,'"`?:\[\]]*$/;
|
|
211
|
+
if (!whitelist.test(sanitized)) {
|
|
212
|
+
throw new Error(`When condition contains disallowed characters: ${condition}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Reject dangerous patterns
|
|
216
|
+
const dangerous =
|
|
217
|
+
/\b(eval|Function|constructor|prototype|window|global|process|require|import|fetch| XMLHttpRequest)\b/i;
|
|
218
|
+
if (dangerous.test(sanitized)) {
|
|
219
|
+
throw new Error(`When condition contains disallowed keyword: ${condition}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Build a safe expression by wrapping in a function
|
|
223
|
+
try {
|
|
224
|
+
const fn = new Function(`return (${sanitized})`);
|
|
225
|
+
const result = fn();
|
|
226
|
+
return Boolean(result);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
229
|
+
throw new Error(`When condition evaluation failed: ${condition} — ${message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Execute a single node with retry logic.
|
|
235
|
+
*/
|
|
236
|
+
async function executeNodeWithRetry(
|
|
237
|
+
node: DagNode,
|
|
238
|
+
context: DagContext,
|
|
239
|
+
executor: NodeExecutor
|
|
240
|
+
): Promise<NodeOutput> {
|
|
241
|
+
const { maxRetries, delayMs, onError } = getEffectiveRetryConfig(node);
|
|
242
|
+
let lastError: NodeOutput | undefined;
|
|
243
|
+
|
|
244
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
245
|
+
if (context.abortSignal?.aborted) {
|
|
246
|
+
return {
|
|
247
|
+
state: "failed",
|
|
248
|
+
output: "",
|
|
249
|
+
error: "Aborted by abort signal",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const result = await executor(node, context);
|
|
255
|
+
if (result.state !== "failed") {
|
|
256
|
+
return { ...result, retries: attempt };
|
|
257
|
+
}
|
|
258
|
+
lastError = result;
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
261
|
+
lastError = {
|
|
262
|
+
state: "failed",
|
|
263
|
+
output: "",
|
|
264
|
+
error: errorMessage,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const shouldRetry =
|
|
269
|
+
onError === "all" || (lastError.error && isTransientError(lastError.error));
|
|
270
|
+
|
|
271
|
+
if (attempt < maxRetries && shouldRetry) {
|
|
272
|
+
await delay(delayMs * (attempt + 1)); // exponential-ish backoff
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { ...lastError!, retries: maxRetries };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isTransientError(errorMessage: string): boolean {
|
|
280
|
+
const transientPatterns = [
|
|
281
|
+
/timeout/i,
|
|
282
|
+
/econnreset/i,
|
|
283
|
+
/econnrefused/i,
|
|
284
|
+
/etimedout/i,
|
|
285
|
+
/network/i,
|
|
286
|
+
/temporary/i,
|
|
287
|
+
/rate.?limit/i,
|
|
288
|
+
/503/i,
|
|
289
|
+
/502/i,
|
|
290
|
+
/504/i,
|
|
291
|
+
];
|
|
292
|
+
return transientPatterns.some((p) => p.test(errorMessage));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Execute a DAG workflow.
|
|
297
|
+
*/
|
|
298
|
+
export async function executeDag(
|
|
299
|
+
nodes: readonly DagNode[],
|
|
300
|
+
executor: NodeExecutor,
|
|
301
|
+
options: {
|
|
302
|
+
workflowName?: string;
|
|
303
|
+
cwd?: string;
|
|
304
|
+
abortSignal?: AbortSignal;
|
|
305
|
+
onNodeStart?: (nodeId: string) => void;
|
|
306
|
+
onNodeComplete?: (nodeId: string, output: NodeOutput) => void;
|
|
307
|
+
} = {}
|
|
308
|
+
): Promise<DagExecutionResult> {
|
|
309
|
+
const startTime = Date.now();
|
|
310
|
+
const nodeOutputs = new Map<string, NodeOutput>();
|
|
311
|
+
const layers = buildTopologicalLayers(nodes);
|
|
312
|
+
const workflowName = options.workflowName ?? "unnamed-workflow";
|
|
313
|
+
const cwd = options.cwd ?? process.cwd();
|
|
314
|
+
|
|
315
|
+
for (const layer of layers) {
|
|
316
|
+
const layerResults = await Promise.allSettled(
|
|
317
|
+
layer.map(async (node) => {
|
|
318
|
+
if (options.abortSignal?.aborted) {
|
|
319
|
+
return {
|
|
320
|
+
node,
|
|
321
|
+
output: {
|
|
322
|
+
state: "failed" as NodeState,
|
|
323
|
+
output: "",
|
|
324
|
+
error: "Aborted by abort signal",
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check trigger rule
|
|
330
|
+
const triggerDecision = checkTriggerRule(node, nodeOutputs);
|
|
331
|
+
if (triggerDecision === "skip") {
|
|
332
|
+
const skippedOutput: NodeOutput = {
|
|
333
|
+
state: "skipped",
|
|
334
|
+
output: "",
|
|
335
|
+
};
|
|
336
|
+
nodeOutputs.set(node.id, skippedOutput);
|
|
337
|
+
options.onNodeComplete?.(node.id, skippedOutput);
|
|
338
|
+
return { node, output: skippedOutput };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check when condition
|
|
342
|
+
if (node.when) {
|
|
343
|
+
try {
|
|
344
|
+
const whenResult = evaluateWhenCondition(node.when, nodeOutputs);
|
|
345
|
+
if (!whenResult) {
|
|
346
|
+
const skippedOutput: NodeOutput = {
|
|
347
|
+
state: "skipped",
|
|
348
|
+
output: "",
|
|
349
|
+
};
|
|
350
|
+
nodeOutputs.set(node.id, skippedOutput);
|
|
351
|
+
options.onNodeComplete?.(node.id, skippedOutput);
|
|
352
|
+
return { node, output: skippedOutput };
|
|
353
|
+
}
|
|
354
|
+
} catch (whenErr) {
|
|
355
|
+
const errorMessage =
|
|
356
|
+
whenErr instanceof Error ? whenErr.message : String(whenErr);
|
|
357
|
+
const failedOutput: NodeOutput = {
|
|
358
|
+
state: "failed",
|
|
359
|
+
output: "",
|
|
360
|
+
error: errorMessage,
|
|
361
|
+
};
|
|
362
|
+
nodeOutputs.set(node.id, failedOutput);
|
|
363
|
+
options.onNodeComplete?.(node.id, failedOutput);
|
|
364
|
+
return { node, output: failedOutput };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
options.onNodeStart?.(node.id);
|
|
369
|
+
|
|
370
|
+
// Execute node
|
|
371
|
+
const context: DagContext = {
|
|
372
|
+
workflowName,
|
|
373
|
+
nodeOutputs,
|
|
374
|
+
cwd,
|
|
375
|
+
abortSignal: options.abortSignal,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const output = await executeNodeWithRetry(node, context, executor);
|
|
379
|
+
nodeOutputs.set(node.id, output);
|
|
380
|
+
options.onNodeComplete?.(node.id, output);
|
|
381
|
+
|
|
382
|
+
// Cancel node handling: if any node fails and there's a cancel dependency,
|
|
383
|
+
// downstream nodes will naturally fail via trigger_rule, but we also set
|
|
384
|
+
// a global abort if the failed node is a cancel node.
|
|
385
|
+
if (output.state === "failed" && isCancelNode(node)) {
|
|
386
|
+
// Cancel nodes immediately fail the workflow
|
|
387
|
+
throw new Error(`Cancel node '${node.id}' failed: ${output.error}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { node, output };
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Process layer results
|
|
395
|
+
for (const result of layerResults) {
|
|
396
|
+
if (result.status === "rejected") {
|
|
397
|
+
const errorMessage =
|
|
398
|
+
result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
399
|
+
return {
|
|
400
|
+
success: false,
|
|
401
|
+
nodeOutputs,
|
|
402
|
+
durationMs: Date.now() - startTime,
|
|
403
|
+
error: errorMessage,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Check if abort was requested
|
|
409
|
+
if (options.abortSignal?.aborted) {
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
nodeOutputs,
|
|
413
|
+
durationMs: Date.now() - startTime,
|
|
414
|
+
error: "Aborted by abort signal",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Determine overall success
|
|
420
|
+
const anyFailed = Array.from(nodeOutputs.values()).some((o) => o.state === "failed");
|
|
421
|
+
return {
|
|
422
|
+
success: !anyFailed,
|
|
423
|
+
nodeOutputs,
|
|
424
|
+
durationMs: Date.now() - startTime,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG Loader
|
|
3
|
+
*
|
|
4
|
+
* Validates DAG structure: unique IDs, dependency existence, cycle detection,
|
|
5
|
+
* and $node_id.output reference integrity.
|
|
6
|
+
*/
|
|
7
|
+
import type {
|
|
8
|
+
DagNode,
|
|
9
|
+
WorkflowDefinition,
|
|
10
|
+
WorkflowLoadError,
|
|
11
|
+
ParseResult,
|
|
12
|
+
} from "./dag-schemas";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate DAG structure and return an error string or null if valid.
|
|
16
|
+
*/
|
|
17
|
+
export function validateDagStructure(nodes: readonly DagNode[]): string | null {
|
|
18
|
+
// Check ID uniqueness
|
|
19
|
+
const ids = new Set<string>();
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
if (ids.has(node.id)) {
|
|
22
|
+
return `Duplicate node id: '${node.id}'`;
|
|
23
|
+
}
|
|
24
|
+
ids.add(node.id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check depends_on references exist
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
for (const dep of node.depends_on ?? []) {
|
|
30
|
+
if (!ids.has(dep)) {
|
|
31
|
+
return `Node '${node.id}' depends_on unknown node '${dep}'`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cycle detection via Kahn's algorithm
|
|
37
|
+
const inDegree = new Map<string, number>();
|
|
38
|
+
const dependents = new Map<string, string[]>();
|
|
39
|
+
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
inDegree.set(node.id, node.depends_on?.length ?? 0);
|
|
42
|
+
for (const dep of node.depends_on ?? []) {
|
|
43
|
+
const existing = dependents.get(dep) ?? [];
|
|
44
|
+
existing.push(node.id);
|
|
45
|
+
dependents.set(dep, existing);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const queue = nodes
|
|
50
|
+
.filter((n) => (inDegree.get(n.id) ?? 0) === 0)
|
|
51
|
+
.map((n) => n.id);
|
|
52
|
+
let visited = 0;
|
|
53
|
+
|
|
54
|
+
while (queue.length > 0) {
|
|
55
|
+
const nodeId = queue.shift();
|
|
56
|
+
if (nodeId === undefined) break;
|
|
57
|
+
visited++;
|
|
58
|
+
for (const dep of dependents.get(nodeId) ?? []) {
|
|
59
|
+
const newDegree = (inDegree.get(dep) ?? 0) - 1;
|
|
60
|
+
inDegree.set(dep, newDegree);
|
|
61
|
+
if (newDegree === 0) queue.push(dep);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (visited < nodes.length) {
|
|
66
|
+
const cycleNodes = nodes
|
|
67
|
+
.filter((n) => (inDegree.get(n.id) ?? 0) > 0)
|
|
68
|
+
.map((n) => n.id);
|
|
69
|
+
return `Cycle detected among nodes: ${cycleNodes.join(", ")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check $node_id.output references in when: and prompt: fields
|
|
73
|
+
const outputRefPattern = /\$([a-zA-Z_][a-zA-Z0-9_-]*)\.output/g;
|
|
74
|
+
const stripMarkdownCode = (s: string): string =>
|
|
75
|
+
s.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
76
|
+
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
const sources: string[] = [];
|
|
79
|
+
if (node.when) sources.push(node.when);
|
|
80
|
+
if ("prompt" in node && typeof node.prompt === "string") {
|
|
81
|
+
sources.push(stripMarkdownCode(node.prompt));
|
|
82
|
+
}
|
|
83
|
+
for (const source of sources) {
|
|
84
|
+
let m: RegExpExecArray | null;
|
|
85
|
+
outputRefPattern.lastIndex = 0;
|
|
86
|
+
while ((m = outputRefPattern.exec(source)) !== null) {
|
|
87
|
+
const refNodeId = m[1];
|
|
88
|
+
if (refNodeId !== undefined && !ids.has(refNodeId)) {
|
|
89
|
+
return `Node '${node.id}' references unknown node '$${refNodeId}.output'`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse a workflow definition from a plain object.
|
|
100
|
+
* Does NOT parse YAML — caller handles deserialization.
|
|
101
|
+
*/
|
|
102
|
+
export function parseWorkflow(raw: unknown, filename = "workflow.json"): ParseResult {
|
|
103
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
104
|
+
return {
|
|
105
|
+
workflow: null,
|
|
106
|
+
error: {
|
|
107
|
+
filename,
|
|
108
|
+
error: "Workflow must be a non-null object",
|
|
109
|
+
errorType: "validation_error",
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const obj = raw as Record<string, unknown>;
|
|
115
|
+
|
|
116
|
+
if (!obj.name || typeof obj.name !== "string") {
|
|
117
|
+
return {
|
|
118
|
+
workflow: null,
|
|
119
|
+
error: {
|
|
120
|
+
filename,
|
|
121
|
+
error: "Missing required field 'name'",
|
|
122
|
+
errorType: "validation_error",
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!obj.description || typeof obj.description !== "string") {
|
|
128
|
+
return {
|
|
129
|
+
workflow: null,
|
|
130
|
+
error: {
|
|
131
|
+
filename,
|
|
132
|
+
error: "Missing required field 'description'",
|
|
133
|
+
errorType: "validation_error",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!Array.isArray(obj.nodes)) {
|
|
139
|
+
return {
|
|
140
|
+
workflow: null,
|
|
141
|
+
error: {
|
|
142
|
+
filename,
|
|
143
|
+
error: "Workflow must have 'nodes' array",
|
|
144
|
+
errorType: "validation_error",
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const nodes: DagNode[] = [];
|
|
150
|
+
for (let i = 0; i < obj.nodes.length; i++) {
|
|
151
|
+
const n = obj.nodes[i];
|
|
152
|
+
if (!n || typeof n !== "object" || Array.isArray(n)) {
|
|
153
|
+
return {
|
|
154
|
+
workflow: null,
|
|
155
|
+
error: {
|
|
156
|
+
filename,
|
|
157
|
+
error: `Node at index ${i} is not an object`,
|
|
158
|
+
errorType: "validation_error",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const nodeObj = n as Record<string, unknown>;
|
|
163
|
+
const id =
|
|
164
|
+
typeof nodeObj.id === "string" && nodeObj.id.trim()
|
|
165
|
+
? nodeObj.id.trim()
|
|
166
|
+
: `#${i + 1}`;
|
|
167
|
+
|
|
168
|
+
const base: { id: string; depends_on?: string[]; trigger_rule?: string; when?: string } = {
|
|
169
|
+
id,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (Array.isArray(nodeObj.depends_on)) {
|
|
173
|
+
base.depends_on = nodeObj.depends_on.filter(
|
|
174
|
+
(d): d is string => typeof d === "string"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (typeof nodeObj.trigger_rule === "string") {
|
|
178
|
+
base.trigger_rule = nodeObj.trigger_rule;
|
|
179
|
+
}
|
|
180
|
+
if (typeof nodeObj.when === "string") {
|
|
181
|
+
base.when = nodeObj.when;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const type = nodeObj.type;
|
|
185
|
+
if (type === "prompt") {
|
|
186
|
+
if (typeof nodeObj.prompt !== "string") {
|
|
187
|
+
return {
|
|
188
|
+
workflow: null,
|
|
189
|
+
error: {
|
|
190
|
+
filename,
|
|
191
|
+
error: `Node '${id}': prompt node requires 'prompt' string`,
|
|
192
|
+
errorType: "validation_error",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
nodes.push({
|
|
197
|
+
...base,
|
|
198
|
+
type: "prompt",
|
|
199
|
+
prompt: nodeObj.prompt,
|
|
200
|
+
provider: typeof nodeObj.provider === "string" ? nodeObj.provider : undefined,
|
|
201
|
+
model: typeof nodeObj.model === "string" ? nodeObj.model : undefined,
|
|
202
|
+
systemPrompt: typeof nodeObj.systemPrompt === "string" ? nodeObj.systemPrompt : undefined,
|
|
203
|
+
output_format:
|
|
204
|
+
typeof nodeObj.output_format === "object" && nodeObj.output_format !== null
|
|
205
|
+
? (nodeObj.output_format as Record<string, unknown>)
|
|
206
|
+
: undefined,
|
|
207
|
+
maxBudgetUsd: typeof nodeObj.maxBudgetUsd === "number" ? nodeObj.maxBudgetUsd : undefined,
|
|
208
|
+
allowed_tools: Array.isArray(nodeObj.allowed_tools)
|
|
209
|
+
? nodeObj.allowed_tools.filter((t): t is string => typeof t === "string")
|
|
210
|
+
: undefined,
|
|
211
|
+
denied_tools: Array.isArray(nodeObj.denied_tools)
|
|
212
|
+
? nodeObj.denied_tools.filter((t): t is string => typeof t === "string")
|
|
213
|
+
: undefined,
|
|
214
|
+
});
|
|
215
|
+
} else if (type === "bash") {
|
|
216
|
+
if (typeof nodeObj.bash !== "string") {
|
|
217
|
+
return {
|
|
218
|
+
workflow: null,
|
|
219
|
+
error: {
|
|
220
|
+
filename,
|
|
221
|
+
error: `Node '${id}': bash node requires 'bash' string`,
|
|
222
|
+
errorType: "validation_error",
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
nodes.push({ ...base, type: "bash", bash: nodeObj.bash });
|
|
227
|
+
} else if (type === "script") {
|
|
228
|
+
if (typeof nodeObj.script !== "string") {
|
|
229
|
+
return {
|
|
230
|
+
workflow: null,
|
|
231
|
+
error: {
|
|
232
|
+
filename,
|
|
233
|
+
error: `Node '${id}': script node requires 'script' string`,
|
|
234
|
+
errorType: "validation_error",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
nodes.push({ ...base, type: "script", script: nodeObj.script });
|
|
239
|
+
} else if (type === "phase") {
|
|
240
|
+
if (typeof nodeObj.phase !== "string") {
|
|
241
|
+
return {
|
|
242
|
+
workflow: null,
|
|
243
|
+
error: {
|
|
244
|
+
filename,
|
|
245
|
+
error: `Node '${id}': phase node requires 'phase' string`,
|
|
246
|
+
errorType: "validation_error",
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
nodes.push({ ...base, type: "phase", phase: nodeObj.phase });
|
|
251
|
+
} else if (type === "approval") {
|
|
252
|
+
nodes.push({
|
|
253
|
+
...base,
|
|
254
|
+
type: "approval",
|
|
255
|
+
message: typeof nodeObj.message === "string" ? nodeObj.message : undefined,
|
|
256
|
+
});
|
|
257
|
+
} else if (type === "cancel") {
|
|
258
|
+
nodes.push({ ...base, type: "cancel" });
|
|
259
|
+
} else {
|
|
260
|
+
return {
|
|
261
|
+
workflow: null,
|
|
262
|
+
error: {
|
|
263
|
+
filename,
|
|
264
|
+
error: `Node '${id}': unknown or missing type '${type}'`,
|
|
265
|
+
errorType: "validation_error",
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const structureError = validateDagStructure(nodes);
|
|
272
|
+
if (structureError) {
|
|
273
|
+
return {
|
|
274
|
+
workflow: null,
|
|
275
|
+
error: {
|
|
276
|
+
filename,
|
|
277
|
+
error: structureError,
|
|
278
|
+
errorType: "validation_error",
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const workflow: WorkflowDefinition = {
|
|
284
|
+
name: obj.name,
|
|
285
|
+
description: obj.description,
|
|
286
|
+
provider: typeof obj.provider === "string" ? obj.provider : undefined,
|
|
287
|
+
model: typeof obj.model === "string" ? obj.model : undefined,
|
|
288
|
+
nodes,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return { workflow, error: null };
|
|
292
|
+
}
|