@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.
Files changed (299) hide show
  1. package/AGENTS.md +148 -0
  2. package/CANON.md +53 -0
  3. package/CLAUDE.md +60 -0
  4. package/CONTEXT.md +49 -0
  5. package/DEBUG.md +59 -0
  6. package/ISSUE_CONTEXT.md +25 -0
  7. package/README.md +143 -0
  8. package/VERSION +1 -0
  9. package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
  10. package/agents/grill-master.md +26 -0
  11. package/agents/implementer.md +32 -0
  12. package/agents/review-army/accessibility-reviewer.md +54 -0
  13. package/agents/review-army/code-quality-reviewer.md +54 -0
  14. package/agents/review-army/security-reviewer.md +56 -0
  15. package/agents/review-army/spec-compliance-reviewer.md +51 -0
  16. package/agents/review-army/test-reviewer.md +55 -0
  17. package/agents/reviewer.md +59 -0
  18. package/agents/ship-audit-army/docs-auditor.md +53 -0
  19. package/agents/ship-audit-army/performance-auditor.md +52 -0
  20. package/agents/ship-audit-army/security-auditor.md +52 -0
  21. package/agents/specifier.md +55 -0
  22. package/agents/triage-officer.md +27 -0
  23. package/bin/gxpm +17 -0
  24. package/bin/gxpm-browser +17 -0
  25. package/bin/gxpm-config +15 -0
  26. package/bin/gxpm-eval +13 -0
  27. package/bin/gxpm-global-discover +15 -0
  28. package/bin/gxpm-init +38 -0
  29. package/bin/gxpm-investigate +194 -0
  30. package/bin/gxpm-uninstall +15 -0
  31. package/bin/gxpm-update-check +165 -0
  32. package/commands/build.md +40 -0
  33. package/commands/help.md +53 -0
  34. package/commands/plan.md +34 -0
  35. package/commands/refine.md +46 -0
  36. package/commands/review.md +34 -0
  37. package/commands/ship.md +37 -0
  38. package/core/ac-check.ts +20 -0
  39. package/core/agent-runtime.ts +363 -0
  40. package/core/artifact-validator.ts +151 -0
  41. package/core/artifacts.ts +313 -0
  42. package/core/autopilot.ts +250 -0
  43. package/core/capabilities.ts +779 -0
  44. package/core/checkpoint.ts +370 -0
  45. package/core/cleanup.ts +32 -0
  46. package/core/command-probe.ts +82 -0
  47. package/core/config.ts +533 -0
  48. package/core/contracts/behavior-spec.schema.ts +38 -0
  49. package/core/contracts/converter.ts +61 -0
  50. package/core/contracts/host.ts +43 -0
  51. package/core/converters/converter.ts +93 -0
  52. package/core/converters/index.ts +8 -0
  53. package/core/converters/managed-artifact.ts +119 -0
  54. package/core/converters/parser.ts +159 -0
  55. package/core/converters/template-renderer.ts +35 -0
  56. package/core/converters/writer.ts +61 -0
  57. package/core/dag-executor.ts +426 -0
  58. package/core/dag-loader.ts +292 -0
  59. package/core/dag-schemas.ts +150 -0
  60. package/core/dispatch.ts +125 -0
  61. package/core/evidence.ts +148 -0
  62. package/core/gate.ts +269 -0
  63. package/core/hook-engine.ts +566 -0
  64. package/core/host-probe.ts +64 -0
  65. package/core/implement.ts +16 -0
  66. package/core/isolation-errors.ts +174 -0
  67. package/core/isolation-resolver.ts +921 -0
  68. package/core/issue-context.ts +381 -0
  69. package/core/issue-readiness.ts +457 -0
  70. package/core/issue-sync.ts +427 -0
  71. package/core/issues.ts +132 -0
  72. package/core/land.ts +108 -0
  73. package/core/orchestrator.ts +54 -0
  74. package/core/phase-artifact.ts +32 -0
  75. package/core/phase-gates.ts +130 -0
  76. package/core/phase-rewind.ts +94 -0
  77. package/core/plan-lint.ts +61 -0
  78. package/core/plan.ts +77 -0
  79. package/core/port-allocation.ts +50 -0
  80. package/core/pr-check.ts +15 -0
  81. package/core/preset-system/preset-resolver.ts +221 -0
  82. package/core/project-init-status.ts +127 -0
  83. package/core/qa.ts +15 -0
  84. package/core/resilience.ts +165 -0
  85. package/core/runs.ts +288 -0
  86. package/core/safe-path.test.ts +80 -0
  87. package/core/safe-path.ts +60 -0
  88. package/core/sdd-gate.test.ts +98 -0
  89. package/core/sdd-gate.ts +134 -0
  90. package/core/self-review.ts +62 -0
  91. package/core/session.ts +70 -0
  92. package/core/ship.ts +86 -0
  93. package/core/specify.ts +173 -0
  94. package/core/state.ts +1002 -0
  95. package/core/template-engine.ts +152 -0
  96. package/core/template-resolver.test.ts +70 -0
  97. package/core/template-resolver.ts +156 -0
  98. package/core/triage.ts +26 -0
  99. package/core/verify.ts +15 -0
  100. package/core/wiki-native.ts +2423 -0
  101. package/core/wiki.ts +27 -0
  102. package/core/workflow-event-emitter.ts +163 -0
  103. package/core/workflows/engine.ts +273 -0
  104. package/core/workflows/expressions.ts +76 -0
  105. package/core/workflows/index.ts +38 -0
  106. package/core/workflows/steps/command.ts +43 -0
  107. package/core/workflows/steps/gate.ts +47 -0
  108. package/core/workflows/steps/gxpm.ts +44 -0
  109. package/core/workflows/steps/linear.ts +31 -0
  110. package/core/workflows/steps/shell.ts +65 -0
  111. package/core/workflows/types.ts +62 -0
  112. package/core/workspace-runtime.ts +227 -0
  113. package/core/worktree-init-steps.ts +647 -0
  114. package/core/worktree-init.ts +330 -0
  115. package/core/worktree-owner.ts +143 -0
  116. package/docs/GXPM_VERIFY.md +98 -0
  117. package/docs/INSTALL_FOR_AGENTS.md +113 -0
  118. package/docs/README.md +57 -0
  119. package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
  120. package/docs/agents/domain.md +30 -0
  121. package/docs/agents/issue-tracker.md +30 -0
  122. package/docs/agents/triage-labels.md +32 -0
  123. package/docs/architecture/gxpm-architecture-diagram.md +265 -0
  124. package/docs/architecture/gxpm-current-architecture.md +175 -0
  125. package/docs/architecture/gxpm-current-flow.md +278 -0
  126. package/docs/architecture/gxpm-replacement-architecture.md +211 -0
  127. package/docs/architecture/gxpm-target-architecture.md +449 -0
  128. package/docs/architecture/gxpm-v0-contract.md +311 -0
  129. package/docs/architecture/layered-workflow-boundaries.md +193 -0
  130. package/docs/architecture/preset-system.md +126 -0
  131. package/docs/architecture/scaffold-northstar.md +23 -0
  132. package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
  133. package/docs/brainstorms/README.md +22 -0
  134. package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
  135. package/docs/governance/beta-skill-promotion.md +39 -0
  136. package/docs/governance/development-contract.md +144 -0
  137. package/docs/governance/gherkin-style.md +90 -0
  138. package/docs/governance/host-adapter.md +56 -0
  139. package/docs/governance/skill-authoring.md +87 -0
  140. package/docs/governance/skill-testing.md +356 -0
  141. package/docs/governance/template-authoring.md +53 -0
  142. package/docs/migrations/v0.2.md +51 -0
  143. package/docs/plans/README.md +23 -0
  144. package/docs/plans/bdd-then-tdd-plan.md +1767 -0
  145. package/docs/plans/docs-knowledge-system-plan.md +31 -0
  146. package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
  147. package/docs/research/agents-md-best-practices.md +207 -0
  148. package/docs/research/archon-study.md +351 -0
  149. package/docs/research/claude-hooks-study.md +440 -0
  150. package/docs/research/codex-hooks-study.md +624 -0
  151. package/docs/research/everything-claude-code-study.md +252 -0
  152. package/docs/research/from-skills-to-layered-workflow.md +322 -0
  153. package/docs/research/gsd-study.md +69 -0
  154. package/docs/research/kimi-hooks-study.md +274 -0
  155. package/docs/research/mattpocock-skills-comparison.md +429 -0
  156. package/docs/research/mattpocock-skills-study.md +275 -0
  157. package/docs/research/oh-my-codex-study.md +279 -0
  158. package/docs/research/perplexity-agent-skills-design.md +168 -0
  159. package/docs/research/pmc-gstack-skill-study.md +122 -0
  160. package/docs/research/spec-kit-study.md +224 -0
  161. package/docs/research/superpowers-study.md +209 -0
  162. package/docs/roadmap/initial-roadmap.md +53 -0
  163. package/docs/solutions/README.md +45 -0
  164. package/docs/solutions/artifact-nesting-recovery.md +58 -0
  165. package/docs/solutions/session-context-restore-practice.md +67 -0
  166. package/docs/solutions/workflow/version-drift-recovery.md +49 -0
  167. package/docs/solutions/worktree-gate-recovery.md +62 -0
  168. package/docs/specs/README.md +28 -0
  169. package/docs/specs/claude.md +45 -0
  170. package/docs/specs/codex.md +44 -0
  171. package/docs/specs/cursor.md +44 -0
  172. package/hosts/adapters/claude.ts +29 -0
  173. package/hosts/adapters/codex.ts +27 -0
  174. package/hosts/adapters/cursor.ts +27 -0
  175. package/hosts/adapters/kimi.ts +27 -0
  176. package/hosts/claude.ts +23 -0
  177. package/hosts/codex.ts +26 -0
  178. package/hosts/cursor.ts +19 -0
  179. package/hosts/index.ts +33 -0
  180. package/hosts/registry.test.ts +52 -0
  181. package/hosts/registry.ts +57 -0
  182. package/hosts/schema.ts +58 -0
  183. package/package.json +52 -0
  184. package/scripts/browser.ts +185 -0
  185. package/scripts/cleanup.ts +142 -0
  186. package/scripts/commands/artifact.ts +115 -0
  187. package/scripts/commands/autopilot.ts +143 -0
  188. package/scripts/commands/capability.ts +57 -0
  189. package/scripts/commands/config.ts +69 -0
  190. package/scripts/commands/dag.ts +126 -0
  191. package/scripts/commands/feedback.ts +123 -0
  192. package/scripts/commands/gate.ts +291 -0
  193. package/scripts/commands/helpers.ts +126 -0
  194. package/scripts/commands/hook.ts +66 -0
  195. package/scripts/commands/init.ts +515 -0
  196. package/scripts/commands/issue.ts +825 -0
  197. package/scripts/commands/phase.ts +61 -0
  198. package/scripts/commands/preset.ts +159 -0
  199. package/scripts/commands/runtime.ts +199 -0
  200. package/scripts/commands/specify.ts +71 -0
  201. package/scripts/commands/upgrade.ts +243 -0
  202. package/scripts/commands/verify.ts +183 -0
  203. package/scripts/commands/wiki.ts +242 -0
  204. package/scripts/commands/workflow.ts +131 -0
  205. package/scripts/dev-skill.ts +55 -0
  206. package/scripts/discover-skills.ts +116 -0
  207. package/scripts/doctor.ts +410 -0
  208. package/scripts/dogfood-check.ts +125 -0
  209. package/scripts/eval-functional.ts +218 -0
  210. package/scripts/eval.ts +246 -0
  211. package/scripts/gen-skill-docs.ts +201 -0
  212. package/scripts/global-discover.ts +217 -0
  213. package/scripts/governance-check.ts +75 -0
  214. package/scripts/gxpm-check.ts +12 -0
  215. package/scripts/gxpm.ts +216 -0
  216. package/scripts/host-config.ts +62 -0
  217. package/scripts/install-claude-hooks.ts +138 -0
  218. package/scripts/install-codex-hooks.ts +271 -0
  219. package/scripts/install-hooks.ts +128 -0
  220. package/scripts/install-kimi-hooks.ts +92 -0
  221. package/scripts/install-skill.ts +184 -0
  222. package/scripts/phase-artifact-commands.ts +100 -0
  223. package/scripts/post-land-sync.ts +46 -0
  224. package/scripts/scaffold-check.ts +85 -0
  225. package/scripts/skill-naming-check.ts +78 -0
  226. package/scripts/skill-structure-check.ts +157 -0
  227. package/scripts/skills-lock-check.ts +60 -0
  228. package/scripts/sync-markdown-artifacts.ts +172 -0
  229. package/scripts/uninstall.ts +162 -0
  230. package/scripts/version.ts +47 -0
  231. package/scripts/wait-pr-ready.ts +407 -0
  232. package/skills/gxpm/SKILL.md +485 -0
  233. package/skills/gxpm/SKILL.md.tmpl +422 -0
  234. package/skills/gxpm/references/CANON.md +53 -0
  235. package/skills/gxpm/references/key-rules.md +130 -0
  236. package/skills/gxpm-architecture/SKILL.md +106 -0
  237. package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
  238. package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
  239. package/skills/gxpm-autopilot/SKILL.md +116 -0
  240. package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
  241. package/skills/gxpm-browser/SKILL.md +105 -0
  242. package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
  243. package/skills/gxpm-browser/references/commands.md +43 -0
  244. package/skills/gxpm-browser/references/evidence-path.md +20 -0
  245. package/skills/gxpm-build/SKILL.md +78 -0
  246. package/skills/gxpm-cleanup/SKILL.md +76 -0
  247. package/skills/gxpm-debug-issue/SKILL.md +39 -0
  248. package/skills/gxpm-diagnose/SKILL.md +220 -0
  249. package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
  250. package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
  251. package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
  252. package/skills/gxpm-diagnose/references/phases.md +60 -0
  253. package/skills/gxpm-eval/SKILL.md +78 -0
  254. package/skills/gxpm-explore-codebase/SKILL.md +36 -0
  255. package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
  256. package/skills/gxpm-feedback/SKILL.md +122 -0
  257. package/skills/gxpm-grill/SKILL.md +159 -0
  258. package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
  259. package/skills/gxpm-grill/references/documentation-templates.md +56 -0
  260. package/skills/gxpm-grill/references/process.md +25 -0
  261. package/skills/gxpm-handoff/SKILL.md +112 -0
  262. package/skills/gxpm-hygiene/SKILL.md +69 -0
  263. package/skills/gxpm-implementer/SKILL.md +142 -0
  264. package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
  265. package/skills/gxpm-linear/SKILL.md +282 -0
  266. package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
  267. package/skills/gxpm-linear/references/commands.md +75 -0
  268. package/skills/gxpm-linear/references/workflows.md +120 -0
  269. package/skills/gxpm-planning/SKILL.md +134 -0
  270. package/skills/gxpm-prototype/SKILL.md +64 -0
  271. package/skills/gxpm-refactor-safely/SKILL.md +62 -0
  272. package/skills/gxpm-review-army/SKILL.md +117 -0
  273. package/skills/gxpm-review-changes/SKILL.md +36 -0
  274. package/skills/gxpm-setup/SKILL.md +101 -0
  275. package/skills/gxpm-specifier/SKILL.md +135 -0
  276. package/skills/gxpm-tdd/SKILL.md +187 -0
  277. package/skills/gxpm-tdd/references/interface-design.md +23 -0
  278. package/skills/gxpm-tdd/references/mocking.md +27 -0
  279. package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
  280. package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
  281. package/skills/gxpm-tdd/references/workflow.md +50 -0
  282. package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
  283. package/skills/gxpm-triage/SKILL.md +160 -0
  284. package/skills/gxpm-verify/SKILL.md +107 -0
  285. package/skills/gxpm-write-skill/SKILL.md +131 -0
  286. package/skills/gxpm-zoom-out/SKILL.md +69 -0
  287. package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
  288. package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
  289. package/templates/constitution-template.md +63 -0
  290. package/templates/hooks/gxpm-commit-msg +16 -0
  291. package/templates/hooks/gxpm-post-checkout +19 -0
  292. package/templates/hooks/gxpm-post-commit +7 -0
  293. package/templates/hooks/gxpm-post-merge +29 -0
  294. package/templates/hooks/gxpm-pre-commit +39 -0
  295. package/templates/hooks/gxpm-pre-push +33 -0
  296. package/templates/plan-template.md.tmpl +46 -0
  297. package/templates/spec-template.md.tmpl +63 -0
  298. package/templates/specify-stub.tmpl +22 -0
  299. 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
+ }