@jsleekr/graft 5.7.2 → 6.0.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/README.md CHANGED
@@ -4,104 +4,54 @@
4
4
 
5
5
  # Graft
6
6
 
7
- **Define multi-agent pipelines in 10 lines. Compile to Claude Code in 1 second.**
7
+ **Infrastructure as Code for Claude Code multi-agent pipelines.**
8
8
 
9
- Graft is a graph-native language that compiles `.gft` files into [Claude Code](https://docs.anthropic.com/en/docs/claude-code) harness structures — agents, hooks, orchestration, and settings — with compile-time token budget analysis.
9
+ Write `.gft` files to define multi-agent pipelines. The compiler generates `.claude/` harness structures — agents, hooks, orchestration plans, settings — with compile-time token budget analysis.
10
10
 
11
- ## 10-Minute Getting Started
11
+ **[Documentation](https://jsleekr.github.io/graft/)** | **[User Guide](docs/guide.md)** | **[Examples](examples/)**
12
12
 
13
- ### 1. Install
14
-
15
- ```bash
16
- npm install -g @jsleekr/graft
17
- ```
18
-
19
- Requires Node.js 20+.
13
+ ---
20
14
 
21
- ### 2. Create a project
15
+ ## Getting Started (5 minutes)
22
16
 
23
- ```bash
24
- graft init my-pipeline
25
- cd my-pipeline
26
- ```
17
+ ### Prerequisites
27
18
 
28
- This creates `pipeline.gft` — a simple two-node pipeline ready to compile.
19
+ - [Node.js 20+](https://nodejs.org)
20
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
29
21
 
30
- ### 3. Compile
22
+ ### Step 1: Install Graft
31
23
 
32
24
  ```bash
33
- graft compile pipeline.gft
34
- ```
35
-
36
- Output:
37
-
38
- ```
39
- ✓ Parse OK
40
- ✓ Scope check OK
41
- ✓ Type check OK
42
- ✓ Token analysis:
43
- Analyst in ~ 500 out ~ 2,000
44
- Reviewer in ~ 840 out ~ 1,000
45
- Best path: 4,340 tokens ✓ within budget (10,000)
46
-
47
- Generated:
48
- .claude/agents/analyst.md ← agent definition
49
- .claude/agents/reviewer.md ← agent definition
50
- .claude/hooks/analyst-to-reviewer.js ← edge transform
51
- .claude/CLAUDE.md ← orchestration plan
52
- .claude/settings.json ← model routing + hooks
25
+ npm install -g @jsleekr/graft
53
26
  ```
54
27
 
55
- ### 4. Run in Claude Code
28
+ ### Step 2: Create a project
56
29
 
57
30
  ```bash
58
- # Create input
59
- echo '{"question": "What is Graft?"}' > .graft/session/input.json
60
-
61
- # Open in Claude Code
62
- claude
31
+ graft init my-pipeline
32
+ cd my-pipeline
63
33
  ```
64
34
 
65
- Then tell Claude Code:
66
-
67
- > `.claude/CLAUDE.md`의 실행 계획을 따라서 파이프라인을 실행해줘. 입력은 `.graft/session/input.json`에 있어.
68
-
69
- Claude Code reads the generated `.claude/` structure and runs the pipeline automatically.
35
+ This creates:
36
+ - `pipeline.gft` — a starter two-node pipeline
37
+ - `.claude/CLAUDE.md` the .gft language spec, so Claude Code natively understands Graft
70
38
 
71
- ### 5. Check the results
39
+ ### Step 3: Open Claude Code and just talk
72
40
 
73
41
  ```bash
74
- cat .graft/session/node_outputs/reviewer.json
75
- ```
76
-
77
- That's it. You have a working multi-agent pipeline.
78
-
79
- ---
80
-
81
- ## How It Works
82
-
83
- ```
84
- .gft Source → Graft Compiler → .claude/ output → Claude Code runs it
42
+ claude
85
43
  ```
86
44
 
87
- | Graft Source | Generated Output | Purpose |
88
- |-------------|-----------------|---------|
89
- | `node` | `.claude/agents/*.md` | Agent with model, tools, output schema |
90
- | `edge \| transform` | `.claude/hooks/*.js` | Node.js data transform between nodes |
91
- | `graph` | `.claude/CLAUDE.md` | Step-by-step orchestration plan |
92
- | `memory` | `.graft/memory/*.json` | Persistent state across runs |
93
- | config | `.claude/settings.json` | Model routing, budget, hook registration |
94
-
95
- ## Why Graft?
45
+ Then say:
96
46
 
97
- Multi-agent systems waste tokens passing full context between agents. Graft fixes this:
47
+ > "I want a code review pipeline where security, logic, and performance reviewers run in parallel, then a senior reviewer synthesizes everything."
98
48
 
99
- - **Edge transforms** extract only what the next agent needs (`select`, `drop`, `compact`, `filter`)
100
- - **Compile-time token analysis** catches budget overruns before you spend API credits
101
- - **Typed output schemas** enforce structured JSON between agents
102
- - **Explicit `reads`** declarations prevent context leaks — the compiler verifies scope
49
+ **Claude Code already knows .gft syntax** (from `.claude/CLAUDE.md`). It will:
50
+ 1. Write a `.gft` file for you
51
+ 2. Run `graft compile` to generate the harness
52
+ 3. You're done
103
53
 
104
- ## Example: Code Review Pipeline
54
+ ### Step 4: Or write .gft yourself
105
55
 
106
56
  ```graft
107
57
  context PullRequest(max_tokens: 2k) {
@@ -113,7 +63,7 @@ node SecurityReviewer(model: sonnet, budget: 4k/2k) {
113
63
  reads: [PullRequest]
114
64
  produces SecurityAnalysis {
115
65
  vulnerabilities: List<String>
116
- risk_level: enum(low, medium, high, critical)
66
+ risk_level: String
117
67
  }
118
68
  }
119
69
 
@@ -143,9 +93,62 @@ graph CodeReview(input: PullRequest, output: FinalReview, budget: 25k) {
143
93
  }
144
94
  ```
145
95
 
146
- This compiles to 3 agents running in parallel, with edge transforms that strip unnecessary data before the senior review.
96
+ Compile it:
147
97
 
148
- ## Language Features
98
+ ```bash
99
+ graft compile code-review.gft
100
+ ```
101
+
102
+ The compiler generates agents, hooks, orchestration plan, and settings — ready for Claude Code.
103
+
104
+ ---
105
+
106
+ ## How It Works
107
+
108
+ ```
109
+ You describe what you want (natural language or .gft)
110
+
111
+ Claude Code writes/edits .gft files (it knows the syntax from CLAUDE.md)
112
+
113
+ graft compile → .claude/ output (agents, hooks, settings)
114
+
115
+ Claude Code reads the .claude/ structure and runs the pipeline
116
+ ```
117
+
118
+ | Graft Source | Generated Output | Purpose |
119
+ |-------------|-----------------|---------|
120
+ | `node` | `.claude/agents/*.md` | Agent with model, tools, output schema |
121
+ | `edge \| transform` | `.claude/hooks/*.js` | Data transform between nodes |
122
+ | `graph` | `.claude/CLAUDE.md` | Step-by-step orchestration plan |
123
+ | `memory` | `.graft/memory/*.json` | Persistent state across runs |
124
+ | config | `.claude/settings.json` | Model routing, budget, hook registration |
125
+
126
+ ## Why Graft?
127
+
128
+ **For humans**: Write 72 lines of `.gft` instead of manually maintaining 9 generated files (13KB+). ~8x compression ratio.
129
+
130
+ **For LLMs**: Claude Code reads 400 tokens of `.gft` instead of 3,300 tokens of scattered config. Modifications are single-file edits with compiler-guaranteed consistency.
131
+
132
+ - **Edge transforms** — extract only what the next agent needs (`select`, `drop`, `compact`, `filter`)
133
+ - **Compile-time token analysis** — catches budget overruns before you spend API credits
134
+ - **Typed output schemas** — enforce structured JSON between agents
135
+ - **Scope checking** — the compiler verifies every `reads` reference at compile time
136
+
137
+ ## CLI
138
+
139
+ ```bash
140
+ graft init <name> # Scaffold project + inject CLAUDE.md spec
141
+ graft compile <file.gft> [--out-dir <dir>] # Compile to .claude/ harness
142
+ graft check <file.gft> # Parse + analyze only
143
+ graft run <file.gft> --input <json> # Compile and execute
144
+ graft test <file.gft> [--input <json>] # Test with mock data
145
+ graft fmt <file.gft> [-w] # Format .gft source
146
+ graft generate <desc> [--output <file>] # Generate .gft via Claude Code CLI
147
+ graft watch <file.gft> # Watch and recompile on changes
148
+ graft visualize <file.gft> # Pipeline DAG as Mermaid diagram
149
+ ```
150
+
151
+ ## Language Reference
149
152
 
150
153
  ### Contexts and Nodes
151
154
 
@@ -175,6 +178,16 @@ edge Analyzer -> Reviewer
175
178
  | compact
176
179
  ```
177
180
 
181
+ ### Conditional Routing
182
+
183
+ ```graft
184
+ edge RiskAssessor -> {
185
+ when risk_score > 0.7 -> DetailedReviewer
186
+ when risk_score > 0.3 -> StandardReviewer
187
+ else -> AutoApprove
188
+ }
189
+ ```
190
+
178
191
  ### Flow Control
179
192
 
180
193
  ```graft
@@ -185,70 +198,35 @@ graph Pipeline(input: TaskSpec, output: Report, budget: 35k) {
185
198
  }
186
199
  ```
187
200
 
188
- Also supports: `foreach`, `let` variables with expressions, parameterized sub-graphs, conditional edge routing.
201
+ Also supports: `foreach`, `let` variables with expressions, parameterized sub-graphs, `import`.
189
202
 
190
- ### Imports and Memory
203
+ ### Memory
191
204
 
192
205
  ```graft
193
- import { UserMessage, SystemConfig } from "./shared.gft"
194
-
195
206
  memory ConversationLog(max_tokens: 2k, storage: file) {
196
207
  turns: List<Turn { role: String, content: String }>
197
208
  summary: Optional<String>
198
209
  }
199
-
200
- node Responder(model: sonnet, budget: 4k/2k) {
201
- reads: [UserMessage, SystemConfig, ConversationLog]
202
- writes: [ConversationLog.turns]
203
- produces Response { reply: String }
204
- }
205
- ```
206
-
207
- ### Expressions
208
-
209
- ```graft
210
- A -> let score = A.risk_score * 2
211
- -> let label = "Found ${len(A.items)} items"
212
- -> let safe = score >= 50 && score <= 100
213
- -> let fallback = A.name ?? "unknown"
214
- -> B -> done
215
210
  ```
216
211
 
217
- ### Type System
212
+ ## Execution Model
218
213
 
219
- ```
220
- String, Int, Float, Float(0..1), Bool // primitives
221
- List<T>, Map<K, V>, Optional<T> // collections
222
- TokenBounded<String, 100> // token-bounded types
223
- enum(low, medium, high) // inline enums
224
- Issue { file: FilePath, severity: ... } // inline structs
225
- ```
214
+ Graft is a **compiler**, not a runtime orchestrator.
226
215
 
227
- ## CLI
216
+ 1. **`.claude/CLAUDE.md`** — natural-language execution plan. Claude Code reads it as instructions.
217
+ 2. **`.claude/hooks/*.js`** — PostToolUse hooks that fire automatically. Edge transforms run deterministically.
218
+ 3. **`.claude/settings.json`** — model routing and hook registration.
228
219
 
229
- ```bash
230
- graft compile <file.gft> [--out-dir <dir>] # Compile to .claude/ structure
231
- graft check <file.gft> # Parse + analyze only
232
- graft run <file.gft> --input <json> [--dry-run] [--verbose] # Compile and execute
233
- graft init <name> # Scaffold a new project
234
- ```
235
-
236
- ## Programmatic API
237
-
238
- ```typescript
239
- import { compile } from '@jsleekr/graft/compiler';
240
- import { Executor } from '@jsleekr/graft/runtime';
241
- import type { Program } from '@jsleekr/graft/types';
220
+ `graft run` spawns Claude Code subprocesses per node. The orchestration depends on Claude Code's instruction-following — unlike LangGraph or CrewAI which use deterministic state machines.
242
221
 
243
- const result = compile(source, 'pipeline.gft');
244
- if (result.success) {
245
- console.log(`Parsed ${result.program.nodes.length} nodes`);
246
- }
247
- ```
222
+ ## Limitations
248
223
 
249
- ## Editor Support
224
+ - **Non-deterministic orchestration** — `CLAUDE.md` is an LLM prompt, not a state machine
225
+ - **Claude Code dependency** — generates `.claude/` structures only
226
+ - **Single provider** — Anthropic models only (multi-provider planned)
227
+ - **Memory** — JSON file storage only (other backends planned)
250
228
 
251
- The [Graft VS Code extension](editors/vscode/) provides syntax highlighting, real-time diagnostics, hover, go-to-definition, find references, rename, completions, and code actions.
229
+ See `SPECIFICATION.md` for planned features.
252
230
 
253
231
  ## Development
254
232
 
@@ -256,7 +234,7 @@ The [Graft VS Code extension](editors/vscode/) provides syntax highlighting, rea
256
234
  git clone https://github.com/JSLEEKR/graft.git
257
235
  cd graft && npm install
258
236
  npm run build # Compile TypeScript
259
- npm test # Run all 1,574 tests
237
+ npm test # Run all 1,712 tests
260
238
  ```
261
239
 
262
240
  ## License
@@ -1,4 +1,4 @@
1
- import { GraftError } from '../errors/diagnostics.js';
1
+ import { GraftError, didYouMean } from '../errors/diagnostics.js';
2
2
  import { ProgramIndex } from '../program-index.js';
3
3
  import { checkVarCollision, checkExprSources, checkGraphCallArgs, checkGraphRecursion, } from './graph-checker.js';
4
4
  export class ScopeChecker {
@@ -56,17 +56,28 @@ export class ScopeChecker {
56
56
  const isProduces = this.index.producesFieldsMap.has(ref.context);
57
57
  const isMemory = this.index.memoryMap.has(ref.context);
58
58
  if (!isContext && !isProduces && !isMemory) {
59
- errors.push(new GraftError(`'${ref.context}' is not declared as a context, produces output, or memory`, ref.location, 'error', 'SCOPE_UNDEFINED_REF'));
59
+ const allNames = [
60
+ ...this.index.contextMap.keys(),
61
+ ...this.index.producesFieldsMap.keys(),
62
+ ...this.index.memoryMap.keys(),
63
+ ];
64
+ const suggestion = didYouMean(ref.context, allNames);
65
+ const help = suggestion
66
+ ? `did you mean '${suggestion}'?`
67
+ : undefined;
68
+ errors.push(new GraftError(`'${ref.context}' is not declared as a context, produces output, or memory`, ref.location, 'error', 'SCOPE_UNDEFINED_REF', help));
60
69
  continue;
61
70
  }
62
71
  // Check partial reference fields
63
72
  if (ref.field) {
64
73
  if (isContext) {
65
74
  const ctx = this.index.contextMap.get(ref.context);
66
- const fieldNames = new Set(ctx.fields.map(f => f.name));
75
+ const fieldNames = ctx.fields.map(f => f.name);
76
+ const fieldSet = new Set(fieldNames);
67
77
  for (const f of ref.field) {
68
- if (!fieldNames.has(f)) {
69
- errors.push(new GraftError(`Field '${f}' does not exist in context '${ref.context}'`, ref.location, 'error', 'SCOPE_FIELD_NOT_FOUND'));
78
+ if (!fieldSet.has(f)) {
79
+ const suggestion = didYouMean(f, fieldNames);
80
+ errors.push(new GraftError(`Field '${f}' does not exist in context '${ref.context}'`, ref.location, 'error', 'SCOPE_FIELD_NOT_FOUND', suggestion ? `did you mean '${suggestion}'?` : undefined));
70
81
  }
71
82
  }
72
83
  }
@@ -74,7 +85,8 @@ export class ScopeChecker {
74
85
  const fields = this.index.producesFieldsMap.get(ref.context);
75
86
  for (const f of ref.field) {
76
87
  if (!fields.has(f)) {
77
- errors.push(new GraftError(`Field '${f}' does not exist in produces '${ref.context}'`, ref.location, 'error', 'SCOPE_FIELD_NOT_FOUND'));
88
+ const suggestion = didYouMean(f, [...fields.keys()]);
89
+ errors.push(new GraftError(`Field '${f}' does not exist in produces '${ref.context}'`, ref.location, 'error', 'SCOPE_FIELD_NOT_FOUND', suggestion ? `did you mean '${suggestion}'?` : undefined));
78
90
  }
79
91
  }
80
92
  }
@@ -11,6 +11,7 @@ export interface CodegenBackend {
11
11
  readonly name: string;
12
12
  generateAgent(node: NodeDecl, memoryNames: Set<string>, ctx: CodegenContext): string;
13
13
  generateHook(edge: EdgeDecl, ctx: CodegenContext): string | null;
14
+ generateConditionalHook?(edge: EdgeDecl, ctx: CodegenContext): string | null;
14
15
  generateOrchestration(ctx: CodegenContext): string;
15
16
  generateSettings(ctx: CodegenContext): Record<string, unknown>;
16
17
  }
@@ -4,6 +4,7 @@ export declare class ClaudeCodeBackend implements CodegenBackend {
4
4
  readonly name = "claude";
5
5
  generateAgent(node: NodeDecl, memoryNames: Set<string>, ctx: CodegenContext): string;
6
6
  generateHook(edge: EdgeDecl, _ctx: CodegenContext): string | null;
7
+ generateConditionalHook(edge: EdgeDecl, _ctx: CodegenContext): string | null;
7
8
  generateOrchestration(ctx: CodegenContext): string;
8
9
  generateSettings(ctx: CodegenContext): Record<string, unknown>;
9
10
  }
@@ -1,5 +1,5 @@
1
1
  import { generateAgent } from './agents.js';
2
- import { generateHook } from './hooks.js';
2
+ import { generateHook, generateConditionalHook } from './hooks.js';
3
3
  import { generateOrchestration } from './orchestration.js';
4
4
  import { generateSettings } from './settings.js';
5
5
  export class ClaudeCodeBackend {
@@ -7,8 +7,9 @@ export class ClaudeCodeBackend {
7
7
  generateAgent(node, memoryNames, ctx) {
8
8
  // Compute input overrides: map produces names to actual file paths
9
9
  const inputOverrides = new Map();
10
- // 1. For edges with transforms: use the transformed output path
11
- // 2. For edges without transforms: use the source's raw output path
10
+ // 1. For direct edges with transforms: use the transformed output path
11
+ // 2. For direct edges without transforms: use the source's raw output path
12
+ // 3. For conditional edges targeting this node: use the source's raw output path
12
13
  for (const edge of ctx.program.edges) {
13
14
  if (edge.target.kind === 'direct' && edge.target.node === node.name) {
14
15
  const sourceNode = ctx.program.nodes.find(n => n.name === edge.source);
@@ -22,6 +23,18 @@ export class ClaudeCodeBackend {
22
23
  }
23
24
  }
24
25
  }
26
+ else if (edge.target.kind === 'conditional') {
27
+ const isTarget = edge.target.branches.some(b => b.target === node.name);
28
+ if (isTarget) {
29
+ const sourceNode = ctx.program.nodes.find(n => n.name === edge.source);
30
+ if (sourceNode) {
31
+ const producesName = sourceNode.produces.name;
32
+ if (!inputOverrides.has(producesName)) {
33
+ inputOverrides.set(producesName, `.graft/session/node_outputs/${edge.source.toLowerCase()}.json`);
34
+ }
35
+ }
36
+ }
37
+ }
25
38
  }
26
39
  // 3. For produces reads with no corresponding edge: resolve to producer's raw output
27
40
  for (const ref of node.reads) {
@@ -38,6 +51,9 @@ export class ClaudeCodeBackend {
38
51
  generateHook(edge, _ctx) {
39
52
  return generateHook(edge);
40
53
  }
54
+ generateConditionalHook(edge, _ctx) {
55
+ return generateConditionalHook(edge);
56
+ }
41
57
  generateOrchestration(ctx) {
42
58
  return generateOrchestration(ctx.program, ctx.report);
43
59
  }
@@ -18,14 +18,27 @@ export function generate(program, report, sourceFile, index, backend) {
18
18
  }
19
19
  // Hooks
20
20
  for (const edge of program.edges) {
21
- const hook = be.generateHook(edge, ctx);
22
- if (hook && edge.target.kind === 'direct') {
23
- const source = edge.source.toLowerCase();
24
- const target = edge.target.node.toLowerCase();
25
- files.push({
26
- path: `.claude/hooks/${source}-to-${target}.js`,
27
- content: hook,
28
- });
21
+ if (edge.target.kind === 'conditional') {
22
+ // Conditional edge router hook
23
+ const hook = be.generateConditionalHook?.(edge, ctx);
24
+ if (hook) {
25
+ files.push({
26
+ path: `.claude/hooks/${edge.source.toLowerCase()}-router.js`,
27
+ content: hook,
28
+ });
29
+ }
30
+ }
31
+ else {
32
+ // Direct edge → transform hook
33
+ const hook = be.generateHook(edge, ctx);
34
+ if (hook && edge.target.kind === 'direct') {
35
+ const source = edge.source.toLowerCase();
36
+ const target = edge.target.node.toLowerCase();
37
+ files.push({
38
+ path: `.claude/hooks/${source}-to-${target}.js`,
39
+ content: hook,
40
+ });
41
+ }
29
42
  }
30
43
  }
31
44
  // Orchestration
@@ -42,6 +55,11 @@ export function generate(program, report, sourceFile, index, backend) {
42
55
  // Runtime scaffold
43
56
  files.push({ path: '.graft/session/node_outputs/.gitkeep', content: '' });
44
57
  files.push({ path: '.graft/token_log.txt', content: '' });
58
+ // Routing scaffold (only when conditional edges exist)
59
+ const hasConditionalEdges = program.edges.some(e => e.target.kind === 'conditional');
60
+ if (hasConditionalEdges) {
61
+ files.push({ path: '.graft/session/routing/.gitkeep', content: '' });
62
+ }
45
63
  // Memory scaffold — conditional
46
64
  if (program.memories.length > 0) {
47
65
  files.push({ path: '.graft/memory/.gitkeep', content: '' });
@@ -0,0 +1,17 @@
1
+ import { NodeDecl, EdgeDecl } from '../parser/ast.js';
2
+ import { CodegenBackend, CodegenContext } from './backend.js';
3
+ /**
4
+ * GenericBackend — a tool-agnostic codegen backend.
5
+ *
6
+ * Produces markdown agent files, JS hook stubs, and JSON settings
7
+ * that are not tied to any specific AI coding assistant.
8
+ * This is groundwork for future Cursor / Windsurf / Copilot support.
9
+ */
10
+ export declare class GenericBackend implements CodegenBackend {
11
+ readonly name = "generic";
12
+ generateAgent(node: NodeDecl, memoryNames: Set<string>, ctx: CodegenContext): string;
13
+ generateHook(edge: EdgeDecl, _ctx: CodegenContext): string | null;
14
+ generateConditionalHook(edge: EdgeDecl, _ctx: CodegenContext): string | null;
15
+ generateOrchestration(ctx: CodegenContext): string;
16
+ generateSettings(ctx: CodegenContext): Record<string, unknown>;
17
+ }
@@ -0,0 +1,156 @@
1
+ import { fieldsToJsonExample } from '../utils.js';
2
+ import { MODEL_MAP } from '../constants.js';
3
+ import { VERSION } from '../version.js';
4
+ /**
5
+ * GenericBackend — a tool-agnostic codegen backend.
6
+ *
7
+ * Produces markdown agent files, JS hook stubs, and JSON settings
8
+ * that are not tied to any specific AI coding assistant.
9
+ * This is groundwork for future Cursor / Windsurf / Copilot support.
10
+ */
11
+ export class GenericBackend {
12
+ name = 'generic';
13
+ generateAgent(node, memoryNames, ctx) {
14
+ const name = node.name.toLowerCase();
15
+ const resolvedModel = MODEL_MAP[node.model] || node.model;
16
+ const jsonSchema = fieldsToJsonExample(node.produces.fields);
17
+ const readsList = node.reads
18
+ .map(r => `- ${r.context}${r.field && r.field.length > 0 ? ` (fields: ${r.field.join(', ')})` : ''}`)
19
+ .join('\n');
20
+ const toolsList = node.tools.length > 0
21
+ ? `\n## Tools\n${node.tools.map(t => `- ${t}`).join('\n')}\n`
22
+ : '';
23
+ return `# ${node.name} Agent
24
+
25
+ ## Metadata
26
+ - **name**: ${name}
27
+ - **model**: ${resolvedModel}
28
+ - **produces**: ${node.produces.name}
29
+
30
+ ## Reads
31
+ ${readsList || '(none)'}
32
+ ${toolsList}
33
+ ## Output Schema
34
+ \`\`\`json
35
+ ${JSON.stringify(jsonSchema, null, 2)}
36
+ \`\`\`
37
+
38
+ ## Instructions
39
+ Execute the ${node.name} step of the pipeline.
40
+ Read the specified inputs, perform your task, and produce output matching the schema above.
41
+ Write your output as JSON to: .graft/session/node_outputs/${name}.json
42
+ `;
43
+ }
44
+ generateHook(edge, _ctx) {
45
+ if (edge.transforms.length === 0)
46
+ return null;
47
+ if (edge.target.kind !== 'direct')
48
+ return null;
49
+ const source = edge.source.toLowerCase();
50
+ const target = edge.target.node.toLowerCase();
51
+ const transformNames = edge.transforms.map(t => t.type).join(', ');
52
+ return `// Hook: ${edge.source} -> ${edge.target.node}
53
+ // Transforms: ${transformNames}
54
+ // Generated by Graft (generic backend)
55
+
56
+ import * as fs from 'node:fs';
57
+
58
+ const inputPath = '.graft/session/node_outputs/${source}.json';
59
+ const outputPath = '.graft/session/node_outputs/${source}_to_${target}.json';
60
+
61
+ const raw = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
62
+
63
+ // TODO: Implement transforms: ${transformNames}
64
+ const result = raw;
65
+
66
+ fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
67
+ `;
68
+ }
69
+ generateConditionalHook(edge, _ctx) {
70
+ if (edge.target.kind !== 'conditional')
71
+ return null;
72
+ const source = edge.source.toLowerCase();
73
+ const branches = edge.target.branches;
74
+ const branchLines = branches.map(b => {
75
+ const condition = b.condition ? `/* condition */` : `/* else */`;
76
+ return ` ${condition} '${b.target}'`;
77
+ }).join(',\n');
78
+ return `// Router: ${edge.source} -> conditional
79
+ // Generated by Graft (generic backend)
80
+
81
+ import * as fs from 'node:fs';
82
+
83
+ const inputPath = '.graft/session/node_outputs/${source}.json';
84
+ const routingPath = '.graft/session/routing/${source}_route.json';
85
+
86
+ const data = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
87
+
88
+ // TODO: Implement conditional routing logic
89
+ const routes = [
90
+ ${branchLines}
91
+ ];
92
+
93
+ const selectedRoute = routes[routes.length - 1]; // default: last (else) branch
94
+ fs.writeFileSync(routingPath, JSON.stringify({ route: selectedRoute }, null, 2));
95
+ `;
96
+ }
97
+ generateOrchestration(ctx) {
98
+ const graph = ctx.program.graphs[0];
99
+ const graphName = graph?.name ?? 'pipeline';
100
+ const nodes = ctx.program.nodes.map(n => n.name);
101
+ const flowSteps = graph?.flow.map(step => {
102
+ if (step.kind === 'node')
103
+ return `1. Run **${step.name}**`;
104
+ if (step.kind === 'parallel')
105
+ return `1. Run in parallel: ${step.branches.join(', ')}`;
106
+ return '';
107
+ }).filter(Boolean).join('\n') ?? '';
108
+ return `# ${graphName} — Orchestration
109
+
110
+ > Generated by Graft v${VERSION} (generic backend)
111
+ > Source: ${ctx.sourceFile}
112
+
113
+ ## Pipeline Flow
114
+ ${flowSteps}
115
+
116
+ ## Nodes
117
+ ${nodes.map(n => `- ${n}`).join('\n')}
118
+
119
+ ## Runtime
120
+ This pipeline was compiled with the generic backend.
121
+ To execute, run each node in order according to the flow above,
122
+ passing outputs between nodes via .graft/session/node_outputs/.
123
+ `;
124
+ }
125
+ generateSettings(ctx) {
126
+ const graph = ctx.program.graphs[0];
127
+ const firstNode = ctx.program.nodes[0];
128
+ const defaultModel = firstNode
129
+ ? (MODEL_MAP[firstNode.model] || firstNode.model)
130
+ : 'default';
131
+ const overrides = {};
132
+ for (const node of ctx.program.nodes) {
133
+ overrides[node.name] = MODEL_MAP[node.model] || node.model;
134
+ }
135
+ return {
136
+ graft: {
137
+ version: VERSION,
138
+ backend: 'generic',
139
+ source: ctx.sourceFile,
140
+ compiled_at: new Date().toISOString(),
141
+ budget: {
142
+ total: graph?.budget ?? 0,
143
+ },
144
+ model_routing: {
145
+ default: defaultModel,
146
+ overrides,
147
+ },
148
+ nodes: ctx.program.nodes.map(n => n.name),
149
+ edges: ctx.program.edges.map(e => ({
150
+ source: e.source,
151
+ target: e.target.kind === 'direct' ? e.target.node : 'conditional',
152
+ })),
153
+ },
154
+ };
155
+ }
156
+ }
@@ -1,2 +1,7 @@
1
1
  import { EdgeDecl } from '../parser/ast.js';
2
2
  export declare function generateHook(edge: EdgeDecl): string | null;
3
+ /**
4
+ * Generate a router hook for conditional edges.
5
+ * Evaluates branch conditions and writes routing decision.
6
+ */
7
+ export declare function generateConditionalHook(edge: EdgeDecl): string | null;