diff --git a/README.md b/README.md index 83f080448..a6918f27a 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ digraph PlanImplement { graph [ goal="Plan, approve, implement, and simplify a change" model_stylesheet=" - * { llm_model: claude-haiku-4-5; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; } + * { model: claude-haiku-4-5; reasoning_effort: low; } + .coding { model: claude-sonnet-4-5; reasoning_effort: high; } " ] diff --git a/arc/workflows/golden-gate/workflow.dot b/arc/workflows/golden-gate/workflow.dot index 20f1f4b2d..362395bf3 100644 --- a/arc/workflows/golden-gate/workflow.dot +++ b/arc/workflows/golden-gate/workflow.dot @@ -1,7 +1,7 @@ digraph GoldenGate { graph [ goal="Create a hyperrealistic interactive 3D Golden Gate Bridge flight experience", - model_stylesheet="* { llm_model: gpt-5.4; llm_provider: openai }" + model_stylesheet="* { model: gpt-5.4;}" ] rankdir=LR diff --git a/arc/workflows/implement/workflow.dot b/arc/workflows/implement/workflow.dot index e53f9cba3..4ab945c04 100644 --- a/arc/workflows/implement/workflow.dot +++ b/arc/workflows/implement/workflow.dot @@ -2,7 +2,7 @@ digraph ImplementAndSimplify { graph [ goal="Implement and simplify", model_stylesheet=" - * { backend: api; llm_model: claude-opus-4-6; llm_provider: anthropic; } + * { backend: api; model: claude-opus-4-6;} " ] rankdir=LR diff --git a/arc/workflows/playwright-demo/workflow.dot b/arc/workflows/playwright-demo/workflow.dot index 0908162fa..3b31c25f4 100644 --- a/arc/workflows/playwright-demo/workflow.dot +++ b/arc/workflows/playwright-demo/workflow.dot @@ -1,5 +1,5 @@ digraph PlaywrightDemo { - graph [goal="Use Playwright MCP to browse Hacker News and take screenshots", model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }"] + graph [goal="Use Playwright MCP to browse Hacker News and take screenshots", model_stylesheet="* { model: claude-sonnet-4-6;}"] rankdir=LR start [shape=Mdiamond, label="Start"] diff --git a/arc/workflows/solitaire/workflow.dot b/arc/workflows/solitaire/workflow.dot index 7b31e6407..d30e3cbc7 100644 --- a/arc/workflows/solitaire/workflow.dot +++ b/arc/workflows/solitaire/workflow.dot @@ -6,9 +6,9 @@ digraph BuildSolitaire { retry_target="impl_setup", fallback_retry_target="impl_logic", model_stylesheet=" - * { llm_model: claude-sonnet; llm_provider: anthropic; } - .hard { llm_model: claude-opus; llm_provider: anthropic; } - .verify { llm_model: claude-haiku; llm_provider: anthropic; } + * { model: claude-sonnet;} + .hard { model: claude-opus; } + .verify { model: claude-haiku; } " ] diff --git a/arc/workflows/spec/spec-dod-multimodel.dot b/arc/workflows/spec/spec-dod-multimodel.dot index 3adcfde80..ac4fbbca9 100644 --- a/arc/workflows/spec/spec-dod-multimodel.dot +++ b/arc/workflows/spec/spec-dod-multimodel.dot @@ -5,11 +5,11 @@ digraph SpecDoDMultiModel { retry_target="triage_merge", default_fidelity="full", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .opus { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } - .gpt { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } - .codex { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } - .merge { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-opus-4-6;} + .opus { model: claude-opus-4-6;reasoning_effort: high; } + .gpt { model: gpt-5.2; reasoning_effort: high; } + .codex { model: gpt-5.2-codex; reasoning_effort: high; } + .merge { model: claude-opus-4-6; reasoning_effort: high; } " ] diff --git a/arc/workflows/spec/spec-dod.dot b/arc/workflows/spec/spec-dod.dot index 66d2a32ab..d3311e20c 100644 --- a/arc/workflows/spec/spec-dod.dot +++ b/arc/workflows/spec/spec-dod.dot @@ -4,9 +4,9 @@ digraph SpecDoD { default_max_retry="3", retry_target="triage", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } + * { model: claude-opus-4-6;} .audit { reasoning_effort: high; } - .fix { llm_model: claude-opus-4-6; reasoning_effort: high; } + .fix { model: claude-opus-4-6; reasoning_effort: high; } #final_audit { reasoning_effort: high; } " ] diff --git a/arc/workflows/vnc-demo/workflow.dot b/arc/workflows/vnc-demo/workflow.dot index 2103c44e5..03a2950ac 100644 --- a/arc/workflows/vnc-demo/workflow.dot +++ b/arc/workflows/vnc-demo/workflow.dot @@ -1,7 +1,7 @@ digraph VNCDemo { graph [ goal="Browse the web with Playwright while user watches via VNC", - model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }" + model_stylesheet="* { model: claude-sonnet-4-6;}" ] rankdir=LR diff --git a/arc/workflows/web-game-demo/workflow.dot b/arc/workflows/web-game-demo/workflow.dot index c0db80cf4..c5e18beff 100644 --- a/arc/workflows/web-game-demo/workflow.dot +++ b/arc/workflows/web-game-demo/workflow.dot @@ -1,7 +1,7 @@ digraph WebGameDemo { graph [ goal="Verify develop-web-game skill setup with asset files", - model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }" + model_stylesheet="* { model: claude-sonnet-4-6;}" ] rankdir=LR diff --git a/docs/core-concepts/agents.mdx b/docs/core-concepts/agents.mdx index 77f33dead..e3d4abf7e 100644 --- a/docs/core-concepts/agents.mdx +++ b/docs/core-concepts/agents.mdx @@ -44,7 +44,7 @@ The CLI is selected automatically based on the node's provider: Set the CLI backend on a node with `backend="cli"` or via a [model stylesheet](/workflows/stylesheets): ```dot -implement [label="Implement", backend="cli", llm_provider="anthropic"] +implement [label="Implement", backend="cli"] ``` ``` diff --git a/docs/core-concepts/models.mdx b/docs/core-concepts/models.mdx index ac4d28074..89adb0a82 100644 --- a/docs/core-concepts/models.mdx +++ b/docs/core-concepts/models.mdx @@ -56,9 +56,9 @@ Assign models to workflow nodes using [model stylesheets](/workflows/stylesheets digraph Example { graph [ model_stylesheet=" - * { llm_model: claude-haiku-4-5; } - .coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; } - #review { llm_model: gemini-3.1-pro-preview; } + * { model: claude-haiku-4-5; } + .coding { model: claude-sonnet-4-5; reasoning_effort: high; } + #review { model: gemini-3.1-pro-preview; } " ] diff --git a/docs/examples/clone-substack.mdx b/docs/examples/clone-substack.mdx index e2fa4a15f..1343ce5ad 100644 --- a/docs/examples/clone-substack.mdx +++ b/docs/examples/clone-substack.mdx @@ -47,11 +47,11 @@ GitHub to Railway validated by code review only — no live deployment execution retry_target="plan_fanout", fallback_retry_target="plan_fanout", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .hard { llm_model: gpt-5.3-codex; llm_provider: openai; } - .verify { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .branch-a { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .branch-b { llm_model: gemini-3-flash-preview; llm_provider: gemini; } + * { model: claude-opus-4-6; } + .hard { model: gpt-5.3-codex; } + .verify { model: claude-opus-4-6; } + .branch-a { model: claude-opus-4-6; } + .branch-b { model: gemini-3-flash-preview;} " ] @@ -693,11 +693,11 @@ check_toolchain -> postmortem [condition="outcome=fail && context.failure_ The stylesheet assigns models based on task difficulty: ``` -* { llm_model: claude-opus-4-6; } // Default: spec expansion, debate, postmortem -.hard { llm_model: gpt-5.3-codex; } // Implementation: optimized for code generation -.verify { llm_model: claude-opus-4-6; } // Fidelity verification: careful analysis -.branch-a { llm_model: claude-opus-4-6; } // Plan A, Review A: Anthropic perspective -.branch-b { llm_model: gemini-3-flash-preview; } // Plan B, Review B: Google perspective +* { model: claude-opus-4-6; } // Default: spec expansion, debate, postmortem +.hard { model: gpt-5.3-codex; } // Implementation: optimized for code generation +.verify { model: claude-opus-4-6; } // Fidelity verification: careful analysis +.branch-a { model: claude-opus-4-6; } // Plan A, Review A: Anthropic perspective +.branch-b { model: gemini-3-flash-preview; } // Plan B, Review B: Google perspective ``` The `.branch-b` class uses a different provider for both planning and review. This ensures the second opinion is genuinely independent — not just a second run of the same model. The `.hard` class routes implementation to OpenAI's Codex, which is optimized for high-throughput code generation. diff --git a/docs/examples/definition-of-done.mdx b/docs/examples/definition-of-done.mdx index ce9a96dd3..9fc8975e5 100644 --- a/docs/examples/definition-of-done.mdx +++ b/docs/examples/definition-of-done.mdx @@ -29,9 +29,9 @@ digraph SpecDoD { default_max_retry="3", retry_target="triage", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } + * { model: claude-opus-4-6;} .audit { reasoning_effort: high; } - .fix { llm_model: claude-opus-4-6; reasoning_effort: high; } + .fix { model: claude-opus-4-6; reasoning_effort: high; } #final_audit { reasoning_effort: high; } " ] @@ -286,11 +286,11 @@ digraph SpecDoDMultiModel { retry_target="triage_merge", default_fidelity="full", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .opus { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } - .gpt { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } - .codex { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } - .merge { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-opus-4-6;} + .opus { model: claude-opus-4-6;reasoning_effort: high; } + .gpt { model: gpt-5.2; reasoning_effort: high; } + .codex { model: gpt-5.2-codex; reasoning_effort: high; } + .merge { model: claude-opus-4-6; reasoning_effort: high; } " ] diff --git a/docs/examples/nlspec-conformance.mdx b/docs/examples/nlspec-conformance.mdx index 788e03b4f..e14a5805d 100644 --- a/docs/examples/nlspec-conformance.mdx +++ b/docs/examples/nlspec-conformance.mdx @@ -22,8 +22,8 @@ digraph NLSpecConformance { graph [ goal="Implement a conformant system from a natural language specification", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; } - .impl { llm_model: claude-sonnet-4-5; reasoning_effort: high; } + * { model: claude-haiku-4-5;} + .impl { model: claude-sonnet-4-5; reasoning_effort: high; } " ] rankdir=LR @@ -135,8 +135,8 @@ The `model_stylesheet` assigns a cheaper model as the default and routes impleme ```dot graph [model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; } - .impl { llm_model: claude-sonnet-4-5; reasoning_effort: high; } + * { model: claude-haiku-4-5;} + .impl { model: claude-sonnet-4-5; reasoning_effort: high; } "] ``` diff --git a/docs/examples/semantic-port.mdx b/docs/examples/semantic-port.mdx index 65285be8e..2c12d1fcb 100644 --- a/docs/examples/semantic-port.mdx +++ b/docs/examples/semantic-port.mdx @@ -20,9 +20,9 @@ digraph SemanticPort { rankdir=LR, default_max_retry=3, model_stylesheet=" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .hard { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .analyze { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } + * { model: claude-sonnet-4-5;} + .hard { model: claude-opus-4-6; } + .analyze { model: gemini-3.1-pro-preview;} " ] @@ -170,7 +170,7 @@ The `analyze` node is the decision point. It examines each upstream commit for * This distinction is critical — routing a different model (Gemini) to the analysis node via the `.analyze` class brings a fresh perspective to the port/skip decision: ``` -.analyze { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } +.analyze { model: gemini-3.1-pro-preview;} ``` ### The fix loop @@ -200,9 +200,9 @@ When the agent decides a commit is irrelevant, it updates the ledger, commits th The stylesheet assigns three tiers of models: ``` -* { llm_model: claude-sonnet-4-5; } // Default: plan, finalize -.hard { llm_model: claude-opus-4-6; } // Implementation, fixing -.analyze { llm_model: gemini-3.1-pro-preview; } // Analysis: fresh eyes +* { model: claude-sonnet-4-5; } // Default: plan, finalize +.hard { model: claude-opus-4-6; } // Implementation, fixing +.analyze { model: gemini-3.1-pro-preview; } // Analysis: fresh eyes ``` - **Sonnet** handles routine tasks: fetching commits, finalizing plans, updating the ledger diff --git a/docs/examples/solitaire.mdx b/docs/examples/solitaire.mdx index 7ee0e30e7..bcda3ef5f 100644 --- a/docs/examples/solitaire.mdx +++ b/docs/examples/solitaire.mdx @@ -22,9 +22,9 @@ digraph BuildSolitaire { retry_target="impl_setup", fallback_retry_target="impl_logic", model_stylesheet=" - * { llm_model: claude-sonnet; llm_provider: anthropic; } - .hard { llm_model: claude-opus; llm_provider: anthropic; } - .verify { llm_model: claude-haiku; llm_provider: anthropic; } + * { model: claude-sonnet;} + .hard { model: claude-opus; } + .verify { model: claude-haiku; } " ] @@ -218,9 +218,9 @@ If a node fails and has no local retry target, Fabro jumps back to `impl_setup` The stylesheet assigns models by role: ``` -* { llm_model: claude-sonnet-4-5; } // Default: spec, setup, integration -.hard { llm_model: claude-opus-4-6; } // Hard work: game logic, UI, review -.verify { llm_model: claude-haiku-4-5; } // Verification: fast, cheap checks +* { model: claude-sonnet-4-5; } // Default: spec, setup, integration +.hard { model: claude-opus-4-6; } // Hard work: game logic, UI, review +.verify { model: claude-haiku-4-5; } // Verification: fast, cheap checks ``` - **Sonnet** handles routine phases: expanding the spec, setting up the project, wiring integration diff --git a/docs/reference/dot-language.mdx b/docs/reference/dot-language.mdx index f859d053e..97a954737 100644 --- a/docs/reference/dot-language.mdx +++ b/docs/reference/dot-language.mdx @@ -65,7 +65,7 @@ Set workflow-level configuration: ```dot // Block syntax -graph [goal="Build a feature", model_stylesheet="* { llm_model: claude-haiku-4-5; }"] +graph [goal="Build a feature", model_stylesheet="* { model: claude-haiku-4-5; }"] // Declaration syntax rankdir=LR @@ -199,8 +199,8 @@ Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be | `max_tokens` | Integer | Maximum output tokens | | `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate` | | `thread_id` | String | Groups nodes into a shared conversation thread | -| `llm_model` | String | Explicit model ID (overrides stylesheet) | -| `llm_provider` | String | Explicit provider name (overrides stylesheet) | +| `model` | String | Explicit model ID (overrides stylesheet) | +| `provider` | String | Explicit provider name (overrides stylesheet) | | `project_memory` | Boolean | When `true` (default), prompt nodes discover and include project docs (`AGENTS.md`, `CLAUDE.md`, etc.) as a system prompt. Set to `false` to disable. | | `backend` | String | Agent execution backend. `api` (default): Fabro calls the LLM API directly and runs its own tool loop. `cli`: Fabro delegates to an external CLI tool (`claude`, `codex`, or `gemini` based on provider). See [Agents — Backends](/core-concepts/agents#backends). | @@ -350,9 +350,9 @@ digraph ImplementFeature { graph [ goal="Implement a feature with tests and code review", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } - #review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-haiku-4-5;reasoning_effort: low; } + .coding { model: claude-sonnet-4-5;reasoning_effort: high; } + #review { model: claude-sonnet-4-5;reasoning_effort: high; } " ] rankdir=LR diff --git a/docs/tutorials/ensemble.mdx b/docs/tutorials/ensemble.mdx index 54b45a50b..e7033a1af 100644 --- a/docs/tutorials/ensemble.mdx +++ b/docs/tutorials/ensemble.mdx @@ -16,11 +16,11 @@ digraph Ensemble { graph [ goal="Get independent opinions from multiple providers, then synthesize", model_stylesheet=" - #opus { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } - #codex { llm_model: gpt-5.3-codex; llm_provider: openai; } - #mercury { llm_model: mercury-2; llm_provider: inception; } - #synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + #opus { model: claude-opus-4-6; } + #gemini { model: gemini-3.1-pro-preview;} + #codex { model: gpt-5.3-codex; } + #mercury { model: mercury-2; provider: inception; } + #synth { model: claude-opus-4-6; reasoning_effort: high; } " ] rankdir=LR @@ -68,10 +68,10 @@ The workflow has three phases: The `fork` node spawns four parallel branches, each assigned to a different provider via the stylesheet: ``` -#opus { llm_model: claude-opus-4-6; llm_provider: anthropic; } -#gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } -#codex { llm_model: gpt-5.3-codex; llm_provider: openai; } -#mercury { llm_model: mercury-2; llm_provider: inception; } +#opus { model: claude-opus-4-6; } +#gemini { model: gemini-3.1-pro-preview;} +#codex { model: gpt-5.3-codex; } +#mercury { model: mercury-2; provider: inception; } ``` Each branch receives the same prompt but runs on a completely different model. The branches execute concurrently and have no knowledge of each other's responses. diff --git a/docs/tutorials/multi-model.mdx b/docs/tutorials/multi-model.mdx index 652fb0b8f..8d11c7727 100644 --- a/docs/tutorials/multi-model.mdx +++ b/docs/tutorials/multi-model.mdx @@ -16,9 +16,9 @@ digraph MultiModel { graph [ goal="Build and review a utility function using multiple models", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } - #review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-haiku-4-5;reasoning_effort: low; } + .coding { model: claude-sonnet-4-5;reasoning_effort: high; } + #review { model: claude-sonnet-4-5;reasoning_effort: high; } " ] rankdir=LR @@ -44,9 +44,9 @@ fabro run files-internal/demo/08-multi-model.dot The `model_stylesheet` graph attribute contains CSS-like rules that assign models to nodes: ``` -* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } -.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } -#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } +* { model: claude-haiku-4-5;reasoning_effort: low; } +.coding { model: claude-sonnet-4-5;reasoning_effort: high; } +#review { model: claude-sonnet-4-5;reasoning_effort: high; } ``` ### Selectors @@ -85,8 +85,8 @@ Stylesheets support four properties: | Property | Description | |---|---| -| `llm_model` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`) | -| `llm_provider` | Provider name (`anthropic`, `openai`, `gemini`, etc.) | +| `model` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`) | +| `provider` | Provider name (`anthropic`, `openai`, `gemini`, etc.) | | `reasoning_effort` | `low`, `medium`, or `high` | | `backend` | `api` (default) or `cli` | @@ -105,7 +105,7 @@ Model routing lets you optimize cost and latency without changing the workflow s A model set directly on a node attribute always beats the stylesheet: ```dot -implement [label="Implement", class="coding", llm_model="claude-opus-4-6"] +implement [label="Implement", class="coding", model="claude-opus-4-6"] ``` This node uses Opus regardless of what `.coding` says. diff --git a/docs/workflows/stylesheets.mdx b/docs/workflows/stylesheets.mdx index 8f9469eec..f6db0489d 100644 --- a/docs/workflows/stylesheets.mdx +++ b/docs/workflows/stylesheets.mdx @@ -14,9 +14,9 @@ digraph Example { graph [ goal="Build and review a utility function", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; } - .coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; } - #review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } + * { model: claude-haiku-4-5;} + .coding { model: claude-sonnet-4-5; reasoning_effort: high; } + #review { model: gemini-3.1-pro-preview;} " ] @@ -64,8 +64,8 @@ Stylesheets support four properties: | Property | Description | Example | |---|---|---| -| `llm_model` | Model ID or alias | `claude-sonnet-4-5`, `opus`, `gemini-pro` | -| `llm_provider` | Provider name | `anthropic`, `openai`, `gemini` | +| `model` | Model ID or alias | `claude-sonnet-4-5`, `opus`, `gemini-pro` | +| `provider` | Provider name | `anthropic`, `openai`, `gemini` | | `reasoning_effort` | Reasoning effort level | `low`, `medium`, `high` | | `backend` | Agent execution backend — `api` (default) runs Fabro's own tool loop, `cli` delegates to an external CLI tool. See [Backends](/core-concepts/agents#backends). | `cli`, `api` | @@ -82,9 +82,9 @@ When multiple rules match the same node, the rule with the **highest specificity For example: ``` -* { llm_model: claude-haiku-4-5; } -.coding { llm_model: claude-sonnet-4-5; } -#review { llm_model: gpt-5.2; } +* { model: claude-haiku-4-5; } +.coding { model: claude-sonnet-4-5; } +#review { model: gpt-5.2; } ``` A node with `id="review"` and `class="coding"` gets `gpt-5.2` because `#id` (specificity 3) beats `.class` (specificity 2). @@ -96,10 +96,10 @@ If two rules have the same specificity, the **last one** in the stylesheet wins. A model set directly on a node attribute always takes precedence over stylesheets, regardless of specificity: ```dot -implement [label="Implement", class="coding", llm_model="claude-opus-4-6"] +implement [label="Implement", class="coding", model="claude-opus-4-6"] ``` -Even if `.coding` sets `llm_model: claude-sonnet-4-5`, this node uses Opus because the explicit attribute wins. +Even if `.coding` sets `model: claude-sonnet-4-5`, this node uses Opus because the explicit attribute wins. ## Syntax reference @@ -118,12 +118,12 @@ selector { property: value; property: value; } ### Full example ``` -* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } +* { model: claude-haiku-4-5;reasoning_effort: low; } box { reasoning_effort: high; } tab { reasoning_effort: low; } -.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } -.review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } -#final_check { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } +.coding { model: claude-sonnet-4-5;reasoning_effort: high; } +.review { model: gemini-3.1-pro-preview;} +#final_check { model: claude-opus-4-6;reasoning_effort: high; } ``` This stylesheet: diff --git a/files-internal/demo/08-multi-model.dot b/files-internal/demo/08-multi-model.dot index c96c89394..b8d2c6a82 100644 --- a/files-internal/demo/08-multi-model.dot +++ b/files-internal/demo/08-multi-model.dot @@ -2,9 +2,9 @@ digraph MultiModel { graph [ goal="Build and review a utility function using multiple models", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } - #review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-haiku-4-5;reasoning_effort: low; } + .coding { model: claude-sonnet-4-5;reasoning_effort: high; } + #review { model: claude-sonnet-4-5;reasoning_effort: high; } " ] rankdir=LR diff --git a/files-internal/demo/11-ensemble.dot b/files-internal/demo/11-ensemble.dot index efd9ab99f..8af88cb36 100644 --- a/files-internal/demo/11-ensemble.dot +++ b/files-internal/demo/11-ensemble.dot @@ -2,11 +2,11 @@ digraph Ensemble { graph [ goal="Get independent opinions from multiple providers, then synthesize", model_stylesheet=" - #opus { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } - #codex { llm_model: gpt-5.3-codex; llm_provider: openai; } - #mercury { llm_model: mercury-2; llm_provider: inception; } - #synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + #opus { model: claude-opus-4-6; } + #gemini { model: gemini-3.1-pro-preview;} + #codex { model: gpt-5.3-codex; } + #mercury { model: mercury-2; provider: inception; } + #synth { model: claude-opus-4-6; reasoning_effort: high; } " ] rankdir=LR diff --git a/lib/crates/fabro-workflows/README.md b/lib/crates/fabro-workflows/README.md index a53113a54..0f7fcc70f 100644 --- a/lib/crates/fabro-workflows/README.md +++ b/lib/crates/fabro-workflows/README.md @@ -124,9 +124,9 @@ digraph Styled { graph [ goal="Build feature", model_stylesheet=" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .code { llm_model: claude-opus-4-6; } - #critical_review { llm_model: gpt-5.2; llm_provider: openai; } + * { model: claude-sonnet-4-5;} + .code { model: claude-opus-4-6; } + #critical_review { model: gpt-5.2;} " ] // ... diff --git a/lib/crates/fabro-workflows/src/cli/backend.rs b/lib/crates/fabro-workflows/src/cli/backend.rs index c77f5ab48..4f39300fa 100644 --- a/lib/crates/fabro-workflows/src/cli/backend.rs +++ b/lib/crates/fabro-workflows/src/cli/backend.rs @@ -242,9 +242,9 @@ impl CodergenBackend for AgentApiBackend { .await .map_err(|e| FabroError::handler(format!("Failed to create LLM client: {e}")))?; - let model = node.llm_model().unwrap_or(&self.model); + let model = node.model().unwrap_or(&self.model); let provider = node - .llm_provider() + .provider() .map(String::from) .or_else(|| Some(self.provider.as_str().to_string())); @@ -281,7 +281,7 @@ impl CodergenBackend for AgentApiBackend { // Build per-request fallback chain: if the node overrides the provider, // no failover is available; otherwise use the backend's. - let fallback_chain: &[FallbackTarget] = if node.llm_provider().is_some() { + let fallback_chain: &[FallbackTarget] = if node.provider().is_some() { &[] } else { &self.fallback_chain diff --git a/lib/crates/fabro-workflows/src/cli/cli_backend.rs b/lib/crates/fabro-workflows/src/cli/cli_backend.rs index 2be2374eb..d3afa43b9 100644 --- a/lib/crates/fabro-workflows/src/cli/cli_backend.rs +++ b/lib/crates/fabro-workflows/src/cli/cli_backend.rs @@ -463,9 +463,9 @@ impl CodergenBackend for AgentCliBackend { .map_err(|e| FabroError::handler(format!("Failed to write prompt file: {e}")))?; // 3. Build CLI command - let model = node.llm_model().unwrap_or(&self.model); + let model = node.model().unwrap_or(&self.model); let provider = node - .llm_provider() + .provider() .and_then(|s| s.parse::().ok()) .unwrap_or(self.provider); @@ -733,7 +733,7 @@ impl BackendRouter { } // CLI-only model on the node - if let Some(model) = node.llm_model() { + if let Some(model) = node.model() { if is_cli_only_model(model) { return true; } @@ -1164,7 +1164,7 @@ mod tests { fn router_uses_api_for_non_cli_model() { let mut node = Node::new("test"); node.attrs.insert( - "llm_model".to_string(), + "model".to_string(), AttrValue::String("claude-opus-4-6".to_string()), ); diff --git a/lib/crates/fabro-workflows/src/cli/run.rs b/lib/crates/fabro-workflows/src/cli/run.rs index 4c2de51fe..2d1235eb8 100644 --- a/lib/crates/fabro-workflows/src/cli/run.rs +++ b/lib/crates/fabro-workflows/src/cli/run.rs @@ -2094,8 +2094,8 @@ async fn run_preflight( if !crate::graph::types::is_llm_handler_type(node.handler_type()) { continue; } - let node_model = node.llm_model().unwrap_or(&model); - let node_provider = node.llm_provider().unwrap_or(default_provider); + let node_model = node.model().unwrap_or(&model); + let node_provider = node.provider().unwrap_or(default_provider); // Resolve through catalog to get canonical model ID and provider let (resolved_model, resolved_provider) = @@ -2106,7 +2106,7 @@ async fn run_preflight( }; // Use node-level provider override if explicitly set, otherwise catalog provider - let final_provider = if node.llm_provider().is_some() { + let final_provider = if node.provider().is_some() { node_provider.to_string() } else { resolved_provider diff --git a/lib/crates/fabro-workflows/src/graph/types.rs b/lib/crates/fabro-workflows/src/graph/types.rs index 4304a0cf3..f1a3bdce2 100644 --- a/lib/crates/fabro-workflows/src/graph/types.rs +++ b/lib/crates/fabro-workflows/src/graph/types.rs @@ -191,13 +191,13 @@ impl Node { } #[must_use] - pub fn llm_model(&self) -> Option<&str> { - self.str_attr("llm_model") + pub fn model(&self) -> Option<&str> { + self.str_attr("model") } #[must_use] - pub fn llm_provider(&self) -> Option<&str> { - self.str_attr("llm_provider") + pub fn provider(&self) -> Option<&str> { + self.str_attr("provider") } #[must_use] @@ -526,8 +526,8 @@ mod tests { assert_eq!(node.thread_id(), None); assert_eq!(node.class(), None); assert_eq!(node.timeout(), None); - assert_eq!(node.llm_model(), None); - assert_eq!(node.llm_provider(), None); + assert_eq!(node.model(), None); + assert_eq!(node.provider(), None); assert_eq!(node.reasoning_effort(), "high"); assert!(!node.auto_status()); assert!(!node.allow_partial()); diff --git a/lib/crates/fabro-workflows/src/handler/prompt.rs b/lib/crates/fabro-workflows/src/handler/prompt.rs index 30a3033dc..b622bc4bf 100644 --- a/lib/crates/fabro-workflows/src/handler/prompt.rs +++ b/lib/crates/fabro-workflows/src/handler/prompt.rs @@ -54,7 +54,7 @@ impl Handler for PromptHandler { let system_prompt = if node.project_memory() { let working_dir = services.sandbox.working_directory(); let provider = node - .llm_provider() + .provider() .and_then(|s| s.parse::().ok()) .unwrap_or(Provider::Anthropic); let docs = fabro_agent::discover_project_docs( diff --git a/lib/crates/fabro-workflows/src/stylesheet.rs b/lib/crates/fabro-workflows/src/stylesheet.rs index ed31c7f97..fa8819111 100644 --- a/lib/crates/fabro-workflows/src/stylesheet.rs +++ b/lib/crates/fabro-workflows/src/stylesheet.rs @@ -177,8 +177,7 @@ fn parse_declarations(remaining: &mut &str) -> Result, FabroErr } /// Recognized stylesheet properties. -const STYLESHEET_PROPERTIES: &[&str] = - &["llm_model", "llm_provider", "reasoning_effort", "backend"]; +const STYLESHEET_PROPERTIES: &[&str] = &["model", "provider", "reasoning_effort", "backend"]; /// Apply a stylesheet to a graph. Rules are applied by specificity order; /// higher specificity wins. Explicit node attributes are never overridden. @@ -246,26 +245,24 @@ mod tests { #[test] fn parse_universal_rule() { - let ss = parse_stylesheet("* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }") - .unwrap(); + let ss = parse_stylesheet("* { model: claude-sonnet-4-5; provider: anthropic; }").unwrap(); assert_eq!(ss.rules.len(), 1); assert_eq!(ss.rules[0].selector, Selector::Universal); assert_eq!(ss.rules[0].declarations.len(), 2); - assert_eq!(ss.rules[0].declarations[0].property, "llm_model"); + assert_eq!(ss.rules[0].declarations[0].property, "model"); assert_eq!(ss.rules[0].declarations[0].value, "claude-sonnet-4-5"); } #[test] fn parse_class_rule() { - let ss = parse_stylesheet(".code { llm_model: claude-opus-4-6; }").unwrap(); + let ss = parse_stylesheet(".code { model: claude-opus-4-6; }").unwrap(); assert_eq!(ss.rules[0].selector, Selector::Class("code".into())); } #[test] fn parse_id_rule() { - let ss = - parse_stylesheet("#critical_review { llm_model: gpt-5.2; reasoning_effort: high; }") - .unwrap(); + let ss = parse_stylesheet("#critical_review { model: gpt-5.2; reasoning_effort: high; }") + .unwrap(); assert_eq!(ss.rules[0].selector, Selector::Id("critical_review".into())); assert_eq!(ss.rules[0].declarations.len(), 2); } @@ -273,9 +270,9 @@ mod tests { #[test] fn parse_multiple_rules() { let input = r" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .code { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } + * { model: claude-sonnet-4-5; provider: anthropic; } + .code { model: claude-opus-4-6; provider: anthropic; } + #critical_review { model: gpt-5.2; provider: openai; reasoning_effort: high; } "; let ss = parse_stylesheet(input).unwrap(); assert_eq!(ss.rules.len(), 3); @@ -283,37 +280,37 @@ mod tests { #[test] fn parse_error_missing_brace() { - let result = parse_stylesheet("* llm_model: test; }"); + let result = parse_stylesheet("* model: test; }"); assert!(result.is_err()); } #[test] fn parse_error_missing_selector() { - let result = parse_stylesheet("{ llm_model: test; }"); + let result = parse_stylesheet("{ model: test; }"); assert!(result.is_err()); } #[test] fn apply_universal_to_all_nodes() { - let ss = parse_stylesheet("* { llm_model: sonnet; }").unwrap(); + let ss = parse_stylesheet("* { model: sonnet; }").unwrap(); let mut graph = Graph::new("test"); graph.nodes.insert("a".into(), Node::new("a")); graph.nodes.insert("b".into(), Node::new("b")); apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("sonnet".into())) ); assert_eq!( - graph.nodes["b"].attrs.get("llm_model"), + graph.nodes["b"].attrs.get("model"), Some(&AttrValue::String("sonnet".into())) ); } #[test] fn apply_class_overrides_universal() { - let ss = parse_stylesheet("* { llm_model: sonnet; } .code { llm_model: opus; }").unwrap(); + let ss = parse_stylesheet("* { model: sonnet; } .code { model: opus; }").unwrap(); let mut graph = Graph::new("test"); let mut code_node = Node::new("impl"); @@ -326,19 +323,18 @@ mod tests { apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["impl"].attrs.get("llm_model"), + graph.nodes["impl"].attrs.get("model"), Some(&AttrValue::String("opus".into())) ); assert_eq!( - graph.nodes["plan"].attrs.get("llm_model"), + graph.nodes["plan"].attrs.get("model"), Some(&AttrValue::String("sonnet".into())) ); } #[test] fn apply_id_overrides_class() { - let ss = - parse_stylesheet(".code { llm_model: opus; } #special { llm_model: gpt; }").unwrap(); + let ss = parse_stylesheet(".code { model: opus; } #special { model: gpt; }").unwrap(); let mut graph = Graph::new("test"); let mut node = Node::new("special"); @@ -348,25 +344,25 @@ mod tests { apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["special"].attrs.get("llm_model"), + graph.nodes["special"].attrs.get("model"), Some(&AttrValue::String("gpt".into())) ); } #[test] fn explicit_attrs_not_overridden() { - let ss = parse_stylesheet("* { llm_model: sonnet; }").unwrap(); + let ss = parse_stylesheet("* { model: sonnet; }").unwrap(); let mut graph = Graph::new("test"); let mut node = Node::new("a"); node.attrs - .insert("llm_model".into(), AttrValue::String("explicit".into())); + .insert("model".into(), AttrValue::String("explicit".into())); graph.nodes.insert("a".into(), node); apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("explicit".into())) ); } @@ -382,9 +378,9 @@ mod tests { #[test] fn spec_section_86_example() { let input = r" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .code { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } + * { model: claude-sonnet-4-5; provider: anthropic; } + .code { model: claude-opus-4-6; provider: anthropic; } + #critical_review { model: gpt-5.2; provider: openai; reasoning_effort: high; } "; let ss = parse_stylesheet(input).unwrap(); let mut graph = Graph::new("test"); @@ -404,21 +400,21 @@ mod tests { apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["plan"].attrs.get("llm_model"), + graph.nodes["plan"].attrs.get("model"), Some(&AttrValue::String("claude-sonnet-4-5".into())) ); assert_eq!( - graph.nodes["implement"].attrs.get("llm_model"), + graph.nodes["implement"].attrs.get("model"), Some(&AttrValue::String("claude-opus-4-6".into())) ); assert_eq!( - graph.nodes["critical_review"].attrs.get("llm_model"), + graph.nodes["critical_review"].attrs.get("model"), Some(&AttrValue::String("gpt-5.2".into())) ); assert_eq!( - graph.nodes["critical_review"].attrs.get("llm_provider"), + graph.nodes["critical_review"].attrs.get("provider"), Some(&AttrValue::String("openai".into())) ); assert_eq!( @@ -429,7 +425,7 @@ mod tests { #[test] fn parse_shape_selector() { - let ss = parse_stylesheet("box { llm_model: opus; }").unwrap(); + let ss = parse_stylesheet("box { model: opus; }").unwrap(); assert_eq!(ss.rules.len(), 1); assert_eq!(ss.rules[0].selector, Selector::Shape("box".into())); assert_eq!(ss.rules[0].declarations[0].value, "opus"); @@ -437,7 +433,7 @@ mod tests { #[test] fn apply_shape_selector_to_matching_nodes() { - let ss = parse_stylesheet("box { llm_model: opus; }").unwrap(); + let ss = parse_stylesheet("box { model: opus; }").unwrap(); let mut graph = Graph::new("test"); // Default shape is "box" @@ -453,42 +449,42 @@ mod tests { apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("opus".into())) ); // Mdiamond node should NOT get the box rule - assert_eq!(graph.nodes["b"].attrs.get("llm_model"), None); + assert_eq!(graph.nodes["b"].attrs.get("model"), None); } #[test] fn shape_overrides_universal_specificity() { - let ss = parse_stylesheet("* { llm_model: sonnet; } box { llm_model: opus; }").unwrap(); + let ss = parse_stylesheet("* { model: sonnet; } box { model: opus; }").unwrap(); let mut graph = Graph::new("test"); graph.nodes.insert("a".into(), Node::new("a")); // default shape = box apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("opus".into())) ); } #[test] fn class_overrides_shape_specificity() { - let ss = parse_stylesheet("box { llm_model: opus; } .fast { llm_model: flash; }").unwrap(); + let ss = parse_stylesheet("box { model: opus; } .fast { model: flash; }").unwrap(); let mut graph = Graph::new("test"); let mut node = Node::new("a"); node.classes.push("fast".into()); graph.nodes.insert("a".into(), node); apply_stylesheet(&ss, &mut graph); assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("flash".into())) ); } #[test] fn class_overrides_universal_specificity() { - let ss = parse_stylesheet("* { llm_model: sonnet; } .special { llm_model: gpt; }").unwrap(); + let ss = parse_stylesheet("* { model: sonnet; } .special { model: gpt; }").unwrap(); let mut graph = Graph::new("test"); let mut node_a = Node::new("a"); @@ -502,12 +498,12 @@ mod tests { // .special (specificity 1) overrides * (specificity 0) assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("gpt".into())) ); // No class, gets universal assert_eq!( - graph.nodes["b"].attrs.get("llm_model"), + graph.nodes["b"].attrs.get("model"), Some(&AttrValue::String("sonnet".into())) ); } diff --git a/lib/crates/fabro-workflows/src/transform.rs b/lib/crates/fabro-workflows/src/transform.rs index 4e8279d08..edda439ef 100644 --- a/lib/crates/fabro-workflows/src/transform.rs +++ b/lib/crates/fabro-workflows/src/transform.rs @@ -112,6 +112,29 @@ impl Transform for StylesheetApplicationTransform { } } +/// For nodes with `model` but no `provider`, infer the provider from the model catalog. +pub struct ProviderInferenceTransform; + +impl Transform for ProviderInferenceTransform { + fn apply(&self, graph: &mut Graph) { + for node in graph.nodes.values_mut() { + let model = node + .attrs + .get("model") + .and_then(AttrValue::as_str) + .map(String::from); + if let Some(model) = model { + if !node.attrs.contains_key("provider") { + if let Some(info) = fabro_llm::catalog::get_model_info(&model) { + node.attrs + .insert("provider".to_string(), AttrValue::String(info.provider)); + } + } + } + } + } +} + /// Resolve a potential `@path` file reference. /// /// If `value` starts with `@` and the referenced file exists locally, the file @@ -479,7 +502,7 @@ mod tests { ); primary.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String("* { llm_model: sonnet; }".to_string()), + AttrValue::String("* { model: sonnet; }".to_string()), ); let mut secondary = Graph::new("sub"); @@ -493,7 +516,7 @@ mod tests { transform.apply(&mut primary); assert_eq!(primary.goal(), "Build feature"); - assert_eq!(primary.model_stylesheet(), "* { llm_model: sonnet; }"); + assert_eq!(primary.model_stylesheet(), "* { model: sonnet; }"); } #[test] @@ -592,6 +615,82 @@ mod tests { ); } + // ----------------------------------------------------------------------- + // ProviderInferenceTransform tests + // ----------------------------------------------------------------------- + + #[test] + fn provider_inference_sets_provider_from_catalog() { + let mut graph = Graph::new("test"); + let mut node = Node::new("a"); + node.attrs.insert( + "model".to_string(), + AttrValue::String("claude-sonnet-4-5".to_string()), + ); + graph.nodes.insert("a".to_string(), node); + + ProviderInferenceTransform.apply(&mut graph); + + assert_eq!( + graph.nodes["a"] + .attrs + .get("provider") + .and_then(AttrValue::as_str), + Some("anthropic") + ); + } + + #[test] + fn provider_inference_does_not_override_explicit_provider() { + let mut graph = Graph::new("test"); + let mut node = Node::new("a"); + node.attrs.insert( + "model".to_string(), + AttrValue::String("claude-sonnet-4-5".to_string()), + ); + node.attrs.insert( + "provider".to_string(), + AttrValue::String("custom".to_string()), + ); + graph.nodes.insert("a".to_string(), node); + + ProviderInferenceTransform.apply(&mut graph); + + assert_eq!( + graph.nodes["a"] + .attrs + .get("provider") + .and_then(AttrValue::as_str), + Some("custom") + ); + } + + #[test] + fn provider_inference_unknown_model_leaves_no_provider() { + let mut graph = Graph::new("test"); + let mut node = Node::new("a"); + node.attrs.insert( + "model".to_string(), + AttrValue::String("unknown-model-xyz".to_string()), + ); + graph.nodes.insert("a".to_string(), node); + + ProviderInferenceTransform.apply(&mut graph); + + assert_eq!(graph.nodes["a"].attrs.get("provider"), None); + } + + #[test] + fn provider_inference_no_model_no_change() { + let mut graph = Graph::new("test"); + let node = Node::new("a"); + graph.nodes.insert("a".to_string(), node); + + ProviderInferenceTransform.apply(&mut graph); + + assert_eq!(graph.nodes["a"].attrs.get("provider"), None); + } + // ----------------------------------------------------------------------- // resolve_file_ref tests // ----------------------------------------------------------------------- diff --git a/lib/crates/fabro-workflows/src/validation/rules.rs b/lib/crates/fabro-workflows/src/validation/rules.rs index ff092cbc3..7f247e8cc 100644 --- a/lib/crates/fabro-workflows/src/validation/rules.rs +++ b/lib/crates/fabro-workflows/src/validation/rules.rs @@ -929,7 +929,7 @@ impl LintRule for StylesheetModelKnownRule { let label = Self::selector_label(&rule.selector); for decl in &rule.declarations { match decl.property.as_str() { - "llm_model" => { + "model" => { if fabro_llm::catalog::get_model_info(&decl.value).is_none() { diagnostics.push(Diagnostic { rule: self.name().to_string(), @@ -944,7 +944,7 @@ impl LintRule for StylesheetModelKnownRule { }); } } - "llm_provider" => { + "provider" => { if fabro_llm::Provider::from_str(&decl.value).is_err() { let valid: Vec<&str> = fabro_llm::Provider::ALL .iter() @@ -1432,7 +1432,7 @@ mod tests { let mut g = minimal_graph(); g.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String("* { llm_model: foo;".to_string()), + AttrValue::String("* { model: foo;".to_string()), ); let rule = StylesheetSyntaxRule; let d = rule.apply(&g); @@ -1445,7 +1445,7 @@ mod tests { let mut g = minimal_graph(); g.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String("* { llm_model: foo; }".to_string()), + AttrValue::String("* { model: foo; }".to_string()), ); let rule = StylesheetSyntaxRule; let d = rule.apply(&g); @@ -2228,8 +2228,7 @@ mod tests { g.attrs.insert( "model_stylesheet".to_string(), AttrValue::String( - "* { llm_model: gpt-4; } .fast { llm_model: gpt-3.5; reasoning_effort: low; }" - .to_string(), + "* { model: gpt-4; } .fast { model: gpt-3.5; reasoning_effort: low; }".to_string(), ), ); let rule = StylesheetSyntaxRule; @@ -2836,9 +2835,7 @@ mod tests { let mut g = minimal_graph(); g.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String( - "* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }".to_string(), - ), + AttrValue::String("* { model: claude-sonnet-4-5; provider: anthropic; }".to_string()), ); let rule = StylesheetModelKnownRule; let d = rule.apply(&g); @@ -2850,7 +2847,7 @@ mod tests { let mut g = minimal_graph(); g.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String("#opus { llm_model: claude-opus-4-5; }".to_string()), + AttrValue::String("#opus { model: claude-opus-4-5; }".to_string()), ); let rule = StylesheetModelKnownRule; let d = rule.apply(&g); @@ -2865,7 +2862,7 @@ mod tests { let mut g = minimal_graph(); g.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String("* { llm_provider: google; }".to_string()), + AttrValue::String("* { provider: google; }".to_string()), ); let rule = StylesheetModelKnownRule; let d = rule.apply(&g); @@ -2879,7 +2876,7 @@ mod tests { let mut g = minimal_graph(); g.attrs.insert( "model_stylesheet".to_string(), - AttrValue::String("* { llm_model: opus; }".to_string()), + AttrValue::String("* { model: opus; }".to_string()), ); let rule = StylesheetModelKnownRule; let d = rule.apply(&g); diff --git a/lib/crates/fabro-workflows/src/workflow.rs b/lib/crates/fabro-workflows/src/workflow.rs index 3b7de052b..bfd9d85d9 100644 --- a/lib/crates/fabro-workflows/src/workflow.rs +++ b/lib/crates/fabro-workflows/src/workflow.rs @@ -3,7 +3,8 @@ use std::path::Path; use crate::error::FabroError; use crate::graph::Graph; use crate::transform::{ - FileInliningTransform, StylesheetApplicationTransform, Transform, VariableExpansionTransform, + FileInliningTransform, ProviderInferenceTransform, StylesheetApplicationTransform, Transform, + VariableExpansionTransform, }; use crate::validation::{self, Diagnostic}; @@ -60,6 +61,7 @@ impl WorkflowBuilder { // Built-in transforms (PreambleTransform moved to engine execution time) VariableExpansionTransform.apply(&mut graph); StylesheetApplicationTransform.apply(&mut graph); + ProviderInferenceTransform.apply(&mut graph); // File inlining when base_dir is provided if let Some(dir) = base_dir { @@ -148,7 +150,7 @@ mod tests { #[test] fn prepare_from_source_applies_stylesheet() { let dot = r#"digraph Test { - graph [goal="Test", model_stylesheet="* { llm_model: sonnet; }"] + graph [goal="Test", model_stylesheet="* { model: sonnet; }"] start [shape=Mdiamond] work [label="Work"] exit [shape=Msquare] @@ -156,7 +158,7 @@ mod tests { }"#; let graph = prepare_from_source(dot).unwrap(); assert_eq!( - graph.nodes["work"].attrs.get("llm_model"), + graph.nodes["work"].attrs.get("model"), Some(&AttrValue::String("sonnet".into())) ); } diff --git a/lib/crates/fabro-workflows/tests/attractor_compat.rs b/lib/crates/fabro-workflows/tests/attractor_compat.rs index 171eb9a11..d157c5e1a 100644 --- a/lib/crates/fabro-workflows/tests/attractor_compat.rs +++ b/lib/crates/fabro-workflows/tests/attractor_compat.rs @@ -37,7 +37,7 @@ fn parse_attractor_batch_clean() { #[test] fn parse_attractor_batch_has_errors() { - // This file is intentionally missing llm_provider on the work node. + // This file is intentionally missing provider on the work node. // It should still parse successfully — validation is separate from parsing. let graph = parse_attractor_dot("batch_has_errors.dot").unwrap(); assert_eq!(graph.nodes.len(), 3); diff --git a/lib/crates/fabro-workflows/tests/integration.rs b/lib/crates/fabro-workflows/tests/integration.rs index ea193a4af..081e364bd 100644 --- a/lib/crates/fabro-workflows/tests/integration.rs +++ b/lib/crates/fabro-workflows/tests/integration.rs @@ -809,9 +809,9 @@ fn variable_expansion_replaces_goal_in_prompts() { #[test] fn stylesheet_application_by_specificity() { let stylesheet_text = r" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .code { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } + * { model: claude-sonnet-4-5; provider: anthropic; } + .code { model: claude-opus-4-6; provider: anthropic; } + #critical_review { model: gpt-5.2; provider: openai; reasoning_effort: high; } "; let mut graph = Graph::new("test"); @@ -834,10 +834,10 @@ fn stylesheet_application_by_specificity() { critical.classes.push("code".to_string()); graph.nodes.insert("critical_review".to_string(), critical); - // explicit node: has explicit llm_model, should NOT be overridden + // explicit node: has explicit model, should NOT be overridden let mut explicit = Node::new("explicit_node"); explicit.attrs.insert( - "llm_model".to_string(), + "model".to_string(), AttrValue::String("my-custom-model".to_string()), ); graph.nodes.insert("explicit_node".to_string(), explicit); @@ -847,31 +847,31 @@ fn stylesheet_application_by_specificity() { // plan: universal -> claude-sonnet-4-5 assert_eq!( - graph.nodes["plan"].attrs.get("llm_model"), + graph.nodes["plan"].attrs.get("model"), Some(&AttrValue::String("claude-sonnet-4-5".to_string())) ); assert_eq!( - graph.nodes["plan"].attrs.get("llm_provider"), + graph.nodes["plan"].attrs.get("provider"), Some(&AttrValue::String("anthropic".to_string())) ); // implement: .code -> claude-opus-4-6 assert_eq!( - graph.nodes["implement"].attrs.get("llm_model"), + graph.nodes["implement"].attrs.get("model"), Some(&AttrValue::String("claude-opus-4-6".to_string())) ); assert_eq!( - graph.nodes["implement"].attrs.get("llm_provider"), + graph.nodes["implement"].attrs.get("provider"), Some(&AttrValue::String("anthropic".to_string())) ); // critical_review: #critical_review -> gpt-5.2 (id overrides class) assert_eq!( - graph.nodes["critical_review"].attrs.get("llm_model"), + graph.nodes["critical_review"].attrs.get("model"), Some(&AttrValue::String("gpt-5.2".to_string())) ); assert_eq!( - graph.nodes["critical_review"].attrs.get("llm_provider"), + graph.nodes["critical_review"].attrs.get("provider"), Some(&AttrValue::String("openai".to_string())) ); assert_eq!( @@ -881,7 +881,7 @@ fn stylesheet_application_by_specificity() { // explicit_node: explicit attr NOT overridden by universal assert_eq!( - graph.nodes["explicit_node"].attrs.get("llm_model"), + graph.nodes["explicit_node"].attrs.get("model"), Some(&AttrValue::String("my-custom-model".to_string())) ); } @@ -891,7 +891,7 @@ fn stylesheet_application_via_parsed_graph() { let input = r#"digraph StyleTest { graph [ goal="Test stylesheet", - model_stylesheet="* { llm_model: sonnet; }" + model_stylesheet="* { model: sonnet; }" ] start [shape=Mdiamond] exit [shape=Msquare] @@ -905,24 +905,24 @@ fn stylesheet_application_via_parsed_graph() { let transform = StylesheetApplicationTransform; transform.apply(&mut graph); - // All nodes without explicit llm_model should get "sonnet" + // All nodes without explicit model should get "sonnet" assert_eq!( - graph.nodes["work"].attrs.get("llm_model"), + graph.nodes["work"].attrs.get("model"), Some(&AttrValue::String("sonnet".to_string())) ); assert_eq!( - graph.nodes["start"].attrs.get("llm_model"), + graph.nodes["start"].attrs.get("model"), Some(&AttrValue::String("sonnet".to_string())) ); assert_eq!( - graph.nodes["exit"].attrs.get("llm_model"), + graph.nodes["exit"].attrs.get("model"), Some(&AttrValue::String("sonnet".to_string())) ); } #[test] fn stylesheet_parse_and_apply_directly() { - let stylesheet_text = "* { llm_model: base; } .fast { llm_model: turbo; }"; + let stylesheet_text = "* { model: base; } .fast { model: turbo; }"; let stylesheet = parse_stylesheet(stylesheet_text).expect("stylesheet parse should succeed"); assert_eq!(stylesheet.rules.len(), 2); @@ -937,11 +937,11 @@ fn stylesheet_parse_and_apply_directly() { apply_stylesheet(&stylesheet, &mut graph); assert_eq!( - graph.nodes["a"].attrs.get("llm_model"), + graph.nodes["a"].attrs.get("model"), Some(&AttrValue::String("base".to_string())) ); assert_eq!( - graph.nodes["b"].attrs.get("llm_model"), + graph.nodes["b"].attrs.get("model"), Some(&AttrValue::String("turbo".to_string())) ); } @@ -3341,7 +3341,7 @@ async fn stylesheet_applies_model_override() { let input = r#"digraph StylesheetTest { graph [ goal="Test stylesheet", - model_stylesheet="* { llm_model: custom-model; }" + model_stylesheet="* { model: custom-model; }" ] start [shape=Mdiamond] exit [shape=Msquare] @@ -3351,7 +3351,7 @@ async fn stylesheet_applies_model_override() { let mut graph = parse(input).expect("parse"); validate_or_raise(&graph, &[]).expect("validate"); StylesheetApplicationTransform.apply(&mut graph); - assert_eq!(graph.nodes["work"].llm_model(), Some("custom-model")); + assert_eq!(graph.nodes["work"].model(), Some("custom-model")); let dir = tempfile::tempdir().unwrap(); let engine = WorkflowRunEngine::new( @@ -3453,7 +3453,7 @@ async fn integration_smoke_plan_implement_review_done() { let dot = r#"digraph SmokeIntegration { graph [ goal="Build the feature", - model_stylesheet="* { llm_model: test-model; }" + model_stylesheet="* { model: test-model; }" ] rankdir=LR start [shape=Mdiamond] @@ -3484,7 +3484,7 @@ async fn integration_smoke_plan_implement_review_done() { graph.nodes["plan"].prompt().unwrap(), "Plan: Build the feature" ); - assert_eq!(graph.nodes["plan"].llm_model(), Some("test-model")); + assert_eq!(graph.nodes["plan"].model(), Some("test-model")); // Run pipeline let interviewer = Arc::new(AutoApproveInterviewer); @@ -6530,7 +6530,7 @@ mod real_llm { ), ); classify.attrs.insert( - "llm_model".to_string(), + "model".to_string(), AttrValue::String("claude-haiku-4-5".to_string()), ); graph.nodes.insert("classify".to_string(), classify); @@ -9910,7 +9910,7 @@ async fn cli_backend_run_uses_node_model_override() { let mut node = Node::new("step"); node.attrs.insert( - "llm_model".to_string(), + "model".to_string(), AttrValue::String("claude-sonnet-4-5".to_string()), ); @@ -9956,11 +9956,11 @@ async fn cli_backend_run_uses_node_provider_override() { let mut node = Node::new("step"); node.attrs.insert( - "llm_provider".to_string(), + "provider".to_string(), AttrValue::String("openai".to_string()), ); node.attrs.insert( - "llm_model".to_string(), + "model".to_string(), AttrValue::String("gpt-5.3-codex".to_string()), ); @@ -10133,7 +10133,7 @@ async fn backend_router_delegates_to_cli_for_backend_attr() { node.attrs .insert("backend".to_string(), AttrValue::String("cli".to_string())); node.attrs.insert( - "llm_provider".to_string(), + "provider".to_string(), AttrValue::String("openai".to_string()), ); diff --git a/skills/fabro-create-workflow/SKILL.md b/skills/fabro-create-workflow/SKILL.md index 5dff80a80..aebdb9a7a 100644 --- a/skills/fabro-create-workflow/SKILL.md +++ b/skills/fabro-create-workflow/SKILL.md @@ -81,9 +81,9 @@ Use `model_stylesheet` for model assignment rather than per-node attributes: ```dot graph [model_stylesheet=" - * { llm_model: claude-sonnet-4-6; llm_provider: anthropic; } - .coding { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } + * { model: claude-sonnet-4-6;} + .coding { model: claude-opus-4-6;} + .review { model: gemini-3.1-pro-preview;} "] ``` diff --git a/skills/fabro-create-workflow/references/dot-language.md b/skills/fabro-create-workflow/references/dot-language.md index 1a8efe5a5..e4dc84c54 100644 --- a/skills/fabro-create-workflow/references/dot-language.md +++ b/skills/fabro-create-workflow/references/dot-language.md @@ -73,7 +73,7 @@ Comments: `//` line and `/* */` block. Multi-turn LLM with tools (shell, read_file, write_file, grep, glob, web_search, web_fetch, edit_file). -Key attributes: `prompt`, `reasoning_effort` (low/medium/high), `max_tokens`, `fidelity`, `thread_id`, `timeout`, `backend` (api or cli), `llm_model`, `llm_provider`, `project_memory`. +Key attributes: `prompt`, `reasoning_effort` (low/medium/high), `max_tokens`, `fidelity`, `thread_id`, `timeout`, `backend` (api or cli), `model`, `provider`, `project_memory`. ### Prompt Nodes (tab) @@ -156,17 +156,17 @@ CSS-like syntax for assigning models to nodes: ```dot graph [model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; } - .coding { llm_model: claude-sonnet-4-6; llm_provider: anthropic; } - #review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } + * { model: claude-haiku-4-5;} + .coding { model: claude-sonnet-4-6;} + #review { model: gemini-3.1-pro-preview;} "] ``` Selectors by specificity (low to high): `*` (universal, 0), shape name (1), `.class` (2), `#nodeid` (3). Higher specificity wins. Same specificity: last rule wins. Explicit node attributes override stylesheets. -Properties: `llm_model`, `llm_provider`, `reasoning_effort`, `backend`. +Properties: `model`, `provider`, `reasoning_effort`, `backend`. -**Critical:** Use semicolons between properties (e.g. `llm_model: foo; llm_provider: bar;`). +**Critical:** Use semicolons between properties (e.g. `model: foo; provider: bar;`). ## Variables diff --git a/skills/fabro-create-workflow/references/example-workflows.md b/skills/fabro-create-workflow/references/example-workflows.md index 457bf71f5..e5eacf328 100644 --- a/skills/fabro-create-workflow/references/example-workflows.md +++ b/skills/fabro-create-workflow/references/example-workflows.md @@ -151,9 +151,9 @@ digraph MultiModel { graph [ goal="Build and review a utility function using multiple models", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-6; llm_provider: anthropic; reasoning_effort: high; } - #review { llm_model: claude-sonnet-4-6; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-haiku-4-5;reasoning_effort: low; } + .coding { model: claude-sonnet-4-6;reasoning_effort: high; } + #review { model: claude-sonnet-4-6;reasoning_effort: high; } " ] rankdir=LR @@ -179,10 +179,10 @@ digraph Ensemble { graph [ goal="Get independent opinions from multiple providers, then synthesize", model_stylesheet=" - #opus { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } - #codex { llm_model: gpt-5.3-codex; llm_provider: openai; } - #synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + #opus { model: claude-opus-4-6; } + #gemini { model: gemini-3.1-pro-preview;} + #codex { model: gpt-5.3-codex; } + #synth { model: claude-opus-4-6; reasoning_effort: high; } " ] rankdir=LR @@ -219,7 +219,7 @@ digraph ImplementAndSimplify { graph [ goal="Implement and simplify", model_stylesheet=" - * { backend: api; llm_model: claude-opus-4-6; llm_provider: anthropic; } + * { backend: api; model: claude-opus-4-6;} " ] rankdir=LR diff --git a/test/attractor/batch_clean.dot b/test/attractor/batch_clean.dot index ff05b692b..1738218e9 100644 --- a/test/attractor/batch_clean.dot +++ b/test/attractor/batch_clean.dot @@ -1,6 +1,6 @@ digraph G { start [shape=Mdiamond] exit [shape=Msquare] - work [shape=box, llm_provider=openai, llm_model=gpt-5.2, prompt="Do the work."] + work [shape=box, model=gpt-5.2, prompt="Do the work."] start -> work -> exit } diff --git a/test/attractor/batch_has_errors.dot b/test/attractor/batch_has_errors.dot index 321ba89ad..1738218e9 100644 --- a/test/attractor/batch_has_errors.dot +++ b/test/attractor/batch_has_errors.dot @@ -1,6 +1,6 @@ digraph G { start [shape=Mdiamond] exit [shape=Msquare] - work [shape=box, llm_model=gpt-5.2, prompt="Do the work."] + work [shape=box, model=gpt-5.2, prompt="Do the work."] start -> work -> exit } diff --git a/test/attractor/batch_warnings_only.dot b/test/attractor/batch_warnings_only.dot index bccdb8e92..64b2fd21f 100644 --- a/test/attractor/batch_warnings_only.dot +++ b/test/attractor/batch_warnings_only.dot @@ -1,6 +1,6 @@ digraph G { start [shape=Mdiamond] exit [shape=Msquare] - work [shape=box, llm_provider=openai, llm_model=gpt-5.2] + work [shape=box, model=gpt-5.2] start -> work -> exit } diff --git a/test/attractor/consensus_task.dot b/test/attractor/consensus_task.dot index 47f473dc8..4a228268f 100644 --- a/test/attractor/consensus_task.dot +++ b/test/attractor/consensus_task.dot @@ -12,118 +12,118 @@ digraph Workflow { Start [ node_type="start", label="Start", shape="Mdiamond", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="4", timeout="300" ]; CheckDoD [ node_type="stack.steer", label="Check DoD", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="2", timeout="120", llm_prompt="Check if definition of done provided.\n\nTASK: $task\nDOD: $definition_of_done\n\nWrite status.json with outcome=needs_dod if DOD is empty or just a placeholder, else outcome=has_dod if a real DOD was provided." ]; DefineDoD_Gemini [ node_type="stack.observe", label="Define DoD (Gemini)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_gemini.md" ]; DefineDoD_GPT [ node_type="stack.observe", label="Define DoD (GPT)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", reasoning_effort="high", llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_gpt.md" ]; DefineDoD_Opus [ node_type="stack.observe", label="Define DoD (Opus)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_opus.md" ]; ConsolidateDoD [ node_type="stack.observe", label="Consolidate DoD", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="8", timeout="420", llm_prompt="Synthesize three DoD proposals.\n\nTASK: $task\n\nRead .ai/dod_*.md, write consensus to .ai/definition_of_done.md" ]; PlanGemini [ node_type="stack.observe", label="Plan (Gemini)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="8", timeout="420", llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_gemini.md" ]; PlanGPT [ node_type="stack.observe", label="Plan (GPT)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="8", timeout="420", reasoning_effort="high", llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_gpt.md" ]; PlanOpus [ node_type="stack.observe", label="Plan (Opus)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="8", timeout="420", llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_opus.md" ]; DebateConsolidate [ node_type="stack.observe", label="Debate & Consolidate", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="10", timeout="600", llm_prompt="Synthesize three plans.\n\nTASK: $task\n\nRead .ai/plan_*.md, write final to .ai/plan_final.md" ]; Implement [ node_type="stack.observe", label="Implement (Opus)", shape="box", style="rounded,filled", - is_codergen="true", allow_partial="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", allow_partial="true", model="gpt-5.2-codex", max_agent_turns="25", timeout="1200", llm_prompt="Execute plan.\n\nTASK: $task\n\nFollow .ai/plan_final.md. Log to .ai/implementation_log.md" ]; ReviewGemini [ node_type="stack.observe", label="Review (Gemini)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_gemini.md with PASS/FAIL" ]; ReviewGPT [ node_type="stack.observe", label="Review (GPT)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", reasoning_effort="high", llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_gpt.md with PASS/FAIL" ]; ReviewOpus [ node_type="stack.observe", label="Review (Opus)", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_opus.md with PASS/FAIL" ]; ReviewConsensus [ node_type="stack.steer", label="Review Consensus", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="6", timeout="300", llm_prompt="Reach consensus.\n\nRead .ai/review_*.md\nWrite to .ai/review_consensus.md\n\noutcome=yes if PASS, outcome=retry if FAIL. Set preferred_next_label to \"\" (empty) or to \"yes\"/\"retry\"; do not use \"No\"." ]; Postmortem [ node_type="stack.observe", label="Postmortem", shape="box", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="8", timeout="420", llm_prompt="Failure postmortem.\n\nTASK: $task\n\nWrite to .ai/postmortem_NN.md" ]; Exit [ node_type="exit", label="Exit", shape="Msquare", style="rounded,filled", - is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + is_codergen="true", model="gpt-5.2-codex", max_agent_turns="4", timeout="120" ]; diff --git a/test/attractor/green_test_complex.dot b/test/attractor/green_test_complex.dot index f6cc27b8f..a42757c87 100644 --- a/test/attractor/green_test_complex.dot +++ b/test/attractor/green_test_complex.dot @@ -5,10 +5,10 @@ digraph dttf { default_max_retry=3, retry_target="impl_setup", model_stylesheet=" - * { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } - .hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } - .verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } - .review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + * { model: gpt-5.2-codex;reasoning_effort: medium; } + .hard { model: gpt-5.2-codex;reasoning_effort: high; } + .verify { model: gpt-5.2-codex;reasoning_effort: medium; } + .review { model: gpt-5.2-codex;reasoning_effort: high; } " ] diff --git a/test/attractor/green_test_moderate.dot b/test/attractor/green_test_moderate.dot index 79134522c..d54fe3be3 100644 --- a/test/attractor/green_test_moderate.dot +++ b/test/attractor/green_test_moderate.dot @@ -5,10 +5,10 @@ digraph linkcheck { default_max_retry=3, retry_target="impl_setup", model_stylesheet=" - * { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } - .hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } - .verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } - .review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + * { model: gpt-5.2-codex;reasoning_effort: medium; } + .hard { model: gpt-5.2-codex;reasoning_effort: high; } + .verify { model: gpt-5.2-codex;reasoning_effort: medium; } + .review { model: gpt-5.2-codex;reasoning_effort: high; } " ] diff --git a/test/attractor/green_test_vague.dot b/test/attractor/green_test_vague.dot index 2730d53ab..aaf0747d4 100644 --- a/test/attractor/green_test_vague.dot +++ b/test/attractor/green_test_vague.dot @@ -5,10 +5,10 @@ digraph solitaire { default_max_retry=3, retry_target="impl_setup", model_stylesheet=" - * { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } - .hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } - .verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } - .review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + * { model: gpt-5.2-codex;reasoning_effort: medium; } + .hard { model: gpt-5.2-codex;reasoning_effort: high; } + .verify { model: gpt-5.2-codex;reasoning_effort: medium; } + .review { model: gpt-5.2-codex;reasoning_effort: high; } " ] diff --git a/test/attractor/refactor_test_complex.dot b/test/attractor/refactor_test_complex.dot index 6680b545e..e99f113bc 100644 --- a/test/attractor/refactor_test_complex.dot +++ b/test/attractor/refactor_test_complex.dot @@ -6,10 +6,10 @@ digraph dttf { retry_target="impl_setup", fallback_retry_target="impl_loader", model_stylesheet=" - * { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; } - .hard { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; } - .verify { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; } - .review { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; } + * { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; } + .hard { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; } + .verify { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; } + .review { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; } " ] diff --git a/test/attractor/refactor_test_moderate.dot b/test/attractor/refactor_test_moderate.dot index adc5a57d2..35380461b 100644 --- a/test/attractor/refactor_test_moderate.dot +++ b/test/attractor/refactor_test_moderate.dot @@ -6,10 +6,10 @@ digraph linkcheck { retry_target="impl_setup", fallback_retry_target="impl_crawler", model_stylesheet=" - * { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; } - .hard { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; } - .verify { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; } - .review { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; } + * { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; } + .hard { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; } + .verify { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; } + .review { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; } " ] diff --git a/test/attractor/refactor_test_vague.dot b/test/attractor/refactor_test_vague.dot index de0cd2945..2a00a559f 100644 --- a/test/attractor/refactor_test_vague.dot +++ b/test/attractor/refactor_test_vague.dot @@ -6,7 +6,7 @@ digraph solitaire { retry_target="impl_setup", fallback_retry_target="impl_game_logic", model_stylesheet=" - * { llm_model: gemini-3-flash-preview; llm_provider: google; } + * { model: gemini-3-flash-preview; provider: google; } " ] diff --git a/test/attractor/reference_template.dot b/test/attractor/reference_template.dot index b07f29c41..5186539cb 100644 --- a/test/attractor/reference_template.dot +++ b/test/attractor/reference_template.dot @@ -7,12 +7,12 @@ digraph reference_template { fallback_retry_target="debate_consolidate", provenance_version="1", model_stylesheet=" - * { llm_model: DEFAULT_MODEL; llm_provider: DEFAULT_PROVIDER; } - .hard { llm_model: HARD_MODEL; llm_provider: HARD_PROVIDER; } - .verify { llm_model: VERIFY_MODEL; llm_provider: VERIFY_PROVIDER; } - .branch-a { llm_model: BRANCH_A_MODEL; llm_provider: BRANCH_A_PROVIDER; } - .branch-b { llm_model: BRANCH_B_MODEL; llm_provider: BRANCH_B_PROVIDER; } - .branch-c { llm_model: BRANCH_C_MODEL; llm_provider: BRANCH_C_PROVIDER; } + * { model: DEFAULT_MODEL; provider: DEFAULT_PROVIDER; } + .hard { model: HARD_MODEL; provider: HARD_PROVIDER; } + .verify { model: VERIFY_MODEL; provider: VERIFY_PROVIDER; } + .branch-a { model: BRANCH_A_MODEL; provider: BRANCH_A_PROVIDER; } + .branch-b { model: BRANCH_B_MODEL; provider: BRANCH_B_PROVIDER; } + .branch-c { model: BRANCH_C_MODEL; provider: BRANCH_C_PROVIDER; } " ] diff --git a/test/attractor/semport.dot b/test/attractor/semport.dot index ec8f39be0..bb7137454 100644 --- a/test/attractor/semport.dot +++ b/test/attractor/semport.dot @@ -1,23 +1,23 @@ digraph Workflow { graph [ label="Semantic Port Tracking Loop", goal="We want to intelligently track and port semantic changes from the upstream openai-agents-python repository to our Go implementation.\n\nWe want to fetch the latest commits from inspiration/openai-agents-python, analyze each new commit for semantic changes (not just syntax), and intelligently coalesce/merge those changes into our Go codebase while respecting Go idioms and our existing architecture.\n\nWe want to track the disposition of each upstream commit in semport/ledger.tsv with three states: 'new' (unprocessed), 'implemented' (changes made), or 'acknowledged' (reviewed but no changes needed).\n\nWe want to make sure we are surgical in this monorepo and follow pre-existing coding conventions and standards.", rankdir="LR", context_fidelity_default="truncate", context_thread_default="semport-tracking", default_max_retry="4" ]; - FinalizeAndUpdateLedger [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6) Finalize & update ledger", llm_model="gpt-5.2-codex", llm_prompt="**Finalize implementation and update ledger in one step.**\\n\\n1. Synthesize the port plan from .ai/semport_plan_sonnet.md and implementation results into .ai/semport_implementation_summary.md. List which upstream commits were processed, what changes were made (with file:line references), and the disposition ('implemented').\\n\\n2. Update the ledger using:\\n```\\npython3 semport/ledger.py update implemented\\npython3 semport/ledger.py sort\\n```\\n\\n3. Verify with `python3 semport/ledger.py stats` to see progress.\\n\\n4. **Commit all changes** (implementation + ledger update) with a clear message:\\n ```\\n git add -A\\n git commit -m \"semport: implement - \"\\n ```\\n Example: `git commit -m \"semport: implement a776d80 - nest handoff history by default\"`\\n\\nKeep our goal $goal in mind. Then loop back to process the next commit.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; + FinalizeAndUpdateLedger [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6) Finalize & update ledger", model="gpt-5.2-codex", llm_prompt="**Finalize implementation and update ledger in one step.**\\n\\n1. Synthesize the port plan from .ai/semport_plan_sonnet.md and implementation results into .ai/semport_implementation_summary.md. List which upstream commits were processed, what changes were made (with file:line references), and the disposition ('implemented').\\n\\n2. Update the ledger using:\\n```\\npython3 semport/ledger.py update implemented\\npython3 semport/ledger.py sort\\n```\\n\\n3. Verify with `python3 semport/ledger.py stats` to see progress.\\n\\n4. **Commit all changes** (implementation + ledger update) with a clear message:\\n ```\\n git add -A\\n git commit -m \"semport: implement - \"\\n ```\\n Example: `git commit -m \"semport: implement a776d80 - nest handoff history by default\"`\\n\\nKeep our goal $goal in mind. Then loop back to process the next commit.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; - TestValidate [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="5) Test/Validate changes", llm_model="gpt-5.2-codex", llm_prompt="Keeping our goal in mind: $goal. From repo root, validate that all ported changes work correctly. Run relevant tests (go test ./...), ensure compilation succeeds, and verify the ported functionality matches the upstream semantic intent (not necessarily syntax). Write validation results to .ai/semport_validation_report_NN.md. Use outcome=yes if all tests pass and changes are semantically correct; otherwise use outcome=retry with concrete failure details.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1800"]; + TestValidate [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="5) Test/Validate changes", model="gpt-5.2-codex", llm_prompt="Keeping our goal in mind: $goal. From repo root, validate that all ported changes work correctly. Run relevant tests (go test ./...), ensure compilation succeeds, and verify the ported functionality matches the upstream semantic intent (not necessarily syntax). Write validation results to .ai/semport_validation_report_NN.md. Use outcome=yes if all tests pass and changes are semantically correct; otherwise use outcome=retry with concrete failure details.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1800"]; - AnalyzeFailureSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6a) Analyze failure (sonnet)", llm_model="gpt-5.2-codex", llm_prompt="When tests or validation fail, inspect .ai/semport_validation_report_*.md, logs, diffs, and error messages. Write .ai/semport_failure_sonnet.md summarizing root causes, impacted files (with line references), and what needs to be fixed. Clearly note where failure artifacts are located. Keep our goal $goal in mind and be subjective.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; + AnalyzeFailureSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6a) Analyze failure (sonnet)", model="gpt-5.2-codex", llm_prompt="When tests or validation fail, inspect .ai/semport_validation_report_*.md, logs, diffs, and error messages. Write .ai/semport_failure_sonnet.md summarizing root causes, impacted files (with line references), and what needs to be fixed. Clearly note where failure artifacts are located. Keep our goal $goal in mind and be subjective.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; - FetchUpstreamSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="1) Fetch upstream & identify next commit (sonnet)", llm_model="gpt-5.2-codex", llm_prompt="Our goal is: $goal\\n\\n---\\n\\n**CRITICAL: Use semport/ledger.py for all ledger operations to ensure proper chronological ordering. Ledger entries MUST always use short git hashes (7 characters, e.g. from `git rev-parse --short`).**\\n\\n1. Run `python3 semport/ledger.py earliest` to get the chronologically earliest commit with disposition='new'\\n2. If a commit is found, write ONLY that single commit (shortsha, iso8601, and full commit message from git show) to .ai/semport_new_commits.md and use outcome=process\\n3. If NO 'new' commits exist:\\n a. Ensure inspiration/openai-agents-python exists (clone if missing)\\n b. Run git fetch && git pull in that directory\\n c. Use git log to find commits newer than the latest in ledger.tsv, capturing a short hash for each commit (e.g. `git log --format='%h %cI' ...`)\\n d. Add new commits using `python3 semport/ledger.py add `\\n e. Run `python3 semport/ledger.py sort` to maintain chronological order\\n f. Then run `python3 semport/ledger.py earliest` to get the first new commit\\n g. If a new commit is found after fetching, write it to .ai/semport_new_commits.md and use outcome=process\\n h. If still no new commits after fetching, write a completion report to .ai/semport_completion.md and use outcome=done\\n\\n**IMPORTANT**: You MUST end with exactly one of these outcomes:\\n- outcome=process (when there is a commit to process)\\n- outcome=done (when fully caught up with no new commits)", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; + FetchUpstreamSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="1) Fetch upstream & identify next commit (sonnet)", model="gpt-5.2-codex", llm_prompt="Our goal is: $goal\\n\\n---\\n\\n**CRITICAL: Use semport/ledger.py for all ledger operations to ensure proper chronological ordering. Ledger entries MUST always use short git hashes (7 characters, e.g. from `git rev-parse --short`).**\\n\\n1. Run `python3 semport/ledger.py earliest` to get the chronologically earliest commit with disposition='new'\\n2. If a commit is found, write ONLY that single commit (shortsha, iso8601, and full commit message from git show) to .ai/semport_new_commits.md and use outcome=process\\n3. If NO 'new' commits exist:\\n a. Ensure inspiration/openai-agents-python exists (clone if missing)\\n b. Run git fetch && git pull in that directory\\n c. Use git log to find commits newer than the latest in ledger.tsv, capturing a short hash for each commit (e.g. `git log --format='%h %cI' ...`)\\n d. Add new commits using `python3 semport/ledger.py add `\\n e. Run `python3 semport/ledger.py sort` to maintain chronological order\\n f. Then run `python3 semport/ledger.py earliest` to get the first new commit\\n g. If a new commit is found after fetching, write it to .ai/semport_new_commits.md and use outcome=process\\n h. If still no new commits after fetching, write a completion report to .ai/semport_completion.md and use outcome=done\\n\\n**IMPORTANT**: You MUST end with exactly one of these outcomes:\\n- outcome=process (when there is a commit to process)\\n- outcome=done (when fully caught up with no new commits)", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; - ImplementPort [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="4) Implement port (gpt-5.1)", llm_model="gpt-5.2-codex", llm_prompt="Follow the port plan in .ai/semport_plan_finalized.md. For each upstream commit, port the semantic changes to the Go codebase. Focus on semantic equivalence, not literal translation. Use Go idioms, respect existing architecture, and reference specific files/line ranges. Log all changes and commands to .ai/semport_impl.log.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="2400"]; + ImplementPort [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="4) Implement port (gpt-5.1)", model="gpt-5.2-codex", llm_prompt="Follow the port plan in .ai/semport_plan_finalized.md. For each upstream commit, port the semantic changes to the Go codebase. Focus on semantic equivalence, not literal translation. Use Go idioms, respect existing architecture, and reference specific files/line ranges. Log all changes and commands to .ai/semport_impl.log.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="2400"]; - FinalizePlanGPT [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="3) Finalize port plan (gpt-5.1)", llm_model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Perform a final editorial pass over .ai/semport_plan_sonnet.md and write .ai/semport_plan_finalized.md. Ensure each port task has concrete file:line references, clear acceptance criteria, and is directly executable. Remove vague language and ensure the plan is actionable.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; + FinalizePlanGPT [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="3) Finalize port plan (gpt-5.1)", model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Perform a final editorial pass over .ai/semport_plan_sonnet.md and write .ai/semport_plan_finalized.md. Ensure each port task has concrete file:line references, clear acceptance criteria, and is directly executable. Remove vague language and ensure the plan is actionable.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; - Exit [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Exit", llm_model="gpt-5.2-codex", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="exit", penwidth="1.2", reasoning_effort="high", shape="doublecircle", style="rounded,filled", timeout="1200"]; + Exit [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Exit", model="gpt-5.2-codex", margin="0.1,0.08", max_agent_turns="8", node_type="exit", penwidth="1.2", reasoning_effort="high", shape="doublecircle", style="rounded,filled", timeout="1200"]; - Start [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Start", llm_model="gpt-5.2-codex", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="start", penwidth="1.2", reasoning_effort="high", shape="circle", style="rounded,filled", timeout="1200"]; + Start [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Start", model="gpt-5.2-codex", margin="0.1,0.08", max_agent_turns="8", node_type="start", penwidth="1.2", reasoning_effort="high", shape="circle", style="rounded,filled", timeout="1200"]; - AnalyzePlanSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="2) Analyze & plan port (sonnet)", llm_model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Read .ai/semport_new_commits.md which contains a SINGLE commit to process. Examine this one commit in inspiration/openai-agents-python using git show. Analyze the semantic changes (what functionality changed, not just syntax). Decide if this change is relevant to our Go implementation or if it's Python-specific/docs-only/not-applicable.\\n\\nWrite .ai/semport_plan_sonnet.md with sections: Commit Being Processed (shortsha and summary), Semantic Analysis (what changed functionally), DECISION (port or acknowledge with clear reasoning), Port Plan (if porting: concrete tasks with file:line references for Go code), and Disposition Recommendation.\\n\\n**If decision is to ACKNOWLEDGE (skip):**\\n1. Update the ledger: `python3 semport/ledger.py update acknowledged && python3 semport/ledger.py sort`\\n2. Verify with `python3 semport/ledger.py stats`\\n3. **Commit the ledger change** with a clear message summarizing why this commit was acknowledged:\\n ```\\n git add semport/ledger.tsv\\n git commit -m \"semport: acknowledge - \"\\n ```\\n Example: `git commit -m \"semport: acknowledge e3fe4f4 - docs typo fix, no Go changes needed\"`\\n4. Use outcome=skip to loop back for next commit\\n\\n**If decision is to PORT:**\\nUse outcome=port to proceed to implementation.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; + AnalyzePlanSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="2) Analyze & plan port (sonnet)", model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Read .ai/semport_new_commits.md which contains a SINGLE commit to process. Examine this one commit in inspiration/openai-agents-python using git show. Analyze the semantic changes (what functionality changed, not just syntax). Decide if this change is relevant to our Go implementation or if it's Python-specific/docs-only/not-applicable.\\n\\nWrite .ai/semport_plan_sonnet.md with sections: Commit Being Processed (shortsha and summary), Semantic Analysis (what changed functionally), DECISION (port or acknowledge with clear reasoning), Port Plan (if porting: concrete tasks with file:line references for Go code), and Disposition Recommendation.\\n\\n**If decision is to ACKNOWLEDGE (skip):**\\n1. Update the ledger: `python3 semport/ledger.py update acknowledged && python3 semport/ledger.py sort`\\n2. Verify with `python3 semport/ledger.py stats`\\n3. **Commit the ledger change** with a clear message summarizing why this commit was acknowledged:\\n ```\\n git add semport/ledger.tsv\\n git commit -m \"semport: acknowledge - \"\\n ```\\n Example: `git commit -m \"semport: acknowledge e3fe4f4 - docs typo fix, no Go changes needed\"`\\n4. Use outcome=skip to loop back for next commit\\n\\n**If decision is to PORT:**\\nUse outcome=port to proceed to implementation.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"]; FinalizeAndUpdateLedger -> FetchUpstreamSonnet [loop_restart="true"]; Start -> FetchUpstreamSonnet; diff --git a/test/attractor/simple_example.dot b/test/attractor/simple_example.dot index 9da458ac0..b3e7c8a08 100644 --- a/test/attractor/simple_example.dot +++ b/test/attractor/simple_example.dot @@ -3,7 +3,7 @@ digraph Simple { goal="Run tests and report", rankdir=LR, model_stylesheet=" - * { llm_model: gpt-5.2-codex; llm_provider: openai; } + * { model: gpt-5.2-codex;} " ] diff --git a/test/attractor/solitaire_fast.dot b/test/attractor/solitaire_fast.dot index 7ce0d9b88..96fe431e0 100644 --- a/test/attractor/solitaire_fast.dot +++ b/test/attractor/solitaire_fast.dot @@ -6,7 +6,7 @@ digraph solitaire { retry_target="impl_setup", fallback_retry_target="impl_game_logic", model_stylesheet=" - * { llm_model: gpt-5.3-codex-spark; llm_provider: openai; } + * { model: gpt-5.3-codex-spark;} " ] diff --git a/test/docs/core-concepts/models/example.dot b/test/docs/core-concepts/models/example.dot index 27ce421a8..889dcd35e 100644 --- a/test/docs/core-concepts/models/example.dot +++ b/test/docs/core-concepts/models/example.dot @@ -1,9 +1,9 @@ digraph Example { graph [ model_stylesheet=" - * { llm_model: claude-haiku-4-5; } - .coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; } - #review { llm_model: gemini-3.1-pro-preview; } + * { model: claude-haiku-4-5; } + .coding { model: claude-sonnet-4-5; reasoning_effort: high; } + #review { model: gemini-3.1-pro-preview; } " ] diff --git a/test/docs/examples/clone-substack/clone-substack.dot b/test/docs/examples/clone-substack/clone-substack.dot index d16fb48d1..f8e07312c 100644 --- a/test/docs/examples/clone-substack/clone-substack.dot +++ b/test/docs/examples/clone-substack/clone-substack.dot @@ -24,11 +24,11 @@ GitHub to Railway validated by code review only — no live deployment execution retry_target="plan_fanout", fallback_retry_target="plan_fanout", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .hard { llm_model: gpt-5.3-codex; llm_provider: openai; } - .verify { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .branch-a { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .branch-b { llm_model: gemini-3-flash-preview; llm_provider: gemini; } + * { model: claude-opus-4-6; } + .hard { model: gpt-5.3-codex; } + .verify { model: claude-opus-4-6; } + .branch-a { model: claude-opus-4-6; } + .branch-b { model: gemini-3-flash-preview;} " ] diff --git a/test/docs/examples/definition-of-done/spec-dod-multimodel.dot b/test/docs/examples/definition-of-done/spec-dod-multimodel.dot index d606ee15e..5dd78cf51 100644 --- a/test/docs/examples/definition-of-done/spec-dod-multimodel.dot +++ b/test/docs/examples/definition-of-done/spec-dod-multimodel.dot @@ -5,11 +5,11 @@ digraph SpecDoDMultiModel { retry_target="triage_merge", default_fidelity="full", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .opus { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } - .gpt { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } - .codex { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } - .merge { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-opus-4-6;} + .opus { model: claude-opus-4-6;reasoning_effort: high; } + .gpt { model: gpt-5.2; reasoning_effort: high; } + .codex { model: gpt-5.2-codex; reasoning_effort: high; } + .merge { model: claude-opus-4-6; reasoning_effort: high; } " ] diff --git a/test/docs/examples/definition-of-done/spec-dod.dot b/test/docs/examples/definition-of-done/spec-dod.dot index 866ebf269..c4ba539cf 100644 --- a/test/docs/examples/definition-of-done/spec-dod.dot +++ b/test/docs/examples/definition-of-done/spec-dod.dot @@ -4,9 +4,9 @@ digraph SpecDoD { default_max_retry="3", retry_target="triage", model_stylesheet=" - * { llm_model: claude-opus-4-6; llm_provider: anthropic; } + * { model: claude-opus-4-6;} .audit { reasoning_effort: high; } - .fix { llm_model: claude-opus-4-6; reasoning_effort: high; } + .fix { model: claude-opus-4-6; reasoning_effort: high; } #final_audit { reasoning_effort: high; } " ] diff --git a/test/docs/examples/nlspec-conformance/n-l-spec-conformance.dot b/test/docs/examples/nlspec-conformance/n-l-spec-conformance.dot index 2c6cb5400..907f56c53 100644 --- a/test/docs/examples/nlspec-conformance/n-l-spec-conformance.dot +++ b/test/docs/examples/nlspec-conformance/n-l-spec-conformance.dot @@ -2,8 +2,8 @@ digraph NLSpecConformance { graph [ goal="Implement a conformant system from a natural language specification", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; } - .impl { llm_model: claude-sonnet-4-5; reasoning_effort: high; } + * { model: claude-haiku-4-5;} + .impl { model: claude-sonnet-4-5; reasoning_effort: high; } " ] rankdir=LR diff --git a/test/docs/examples/semantic-port/semantic-port.dot b/test/docs/examples/semantic-port/semantic-port.dot index bb914947b..47c7639d2 100644 --- a/test/docs/examples/semantic-port/semantic-port.dot +++ b/test/docs/examples/semantic-port/semantic-port.dot @@ -4,9 +4,9 @@ digraph SemanticPort { rankdir=LR, default_max_retry=3, model_stylesheet=" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .hard { llm_model: claude-opus-4-6; llm_provider: anthropic; } - .analyze { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } + * { model: claude-sonnet-4-5;} + .hard { model: claude-opus-4-6; } + .analyze { model: gemini-3.1-pro-preview;} " ] diff --git a/test/docs/reference/dot-language/implement-feature.dot b/test/docs/reference/dot-language/implement-feature.dot index 3db9442d7..34ccf4133 100644 --- a/test/docs/reference/dot-language/implement-feature.dot +++ b/test/docs/reference/dot-language/implement-feature.dot @@ -2,9 +2,9 @@ digraph ImplementFeature { graph [ goal="Implement a feature with tests and code review", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } - #review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-haiku-4-5;reasoning_effort: low; } + .coding { model: claude-sonnet-4-5;reasoning_effort: high; } + #review { model: claude-sonnet-4-5;reasoning_effort: high; } " ] rankdir=LR diff --git a/test/docs/tutorials/ensemble/ensemble.dot b/test/docs/tutorials/ensemble/ensemble.dot index efd9ab99f..8af88cb36 100644 --- a/test/docs/tutorials/ensemble/ensemble.dot +++ b/test/docs/tutorials/ensemble/ensemble.dot @@ -2,11 +2,11 @@ digraph Ensemble { graph [ goal="Get independent opinions from multiple providers, then synthesize", model_stylesheet=" - #opus { llm_model: claude-opus-4-6; llm_provider: anthropic; } - #gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } - #codex { llm_model: gpt-5.3-codex; llm_provider: openai; } - #mercury { llm_model: mercury-2; llm_provider: inception; } - #synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; } + #opus { model: claude-opus-4-6; } + #gemini { model: gemini-3.1-pro-preview;} + #codex { model: gpt-5.3-codex; } + #mercury { model: mercury-2; provider: inception; } + #synth { model: claude-opus-4-6; reasoning_effort: high; } " ] rankdir=LR diff --git a/test/docs/tutorials/multi-model/multi-model.dot b/test/docs/tutorials/multi-model/multi-model.dot index 90c97a67d..e8ff9fce4 100644 --- a/test/docs/tutorials/multi-model/multi-model.dot +++ b/test/docs/tutorials/multi-model/multi-model.dot @@ -2,9 +2,9 @@ digraph MultiModel { graph [ goal="Build and review a utility function using multiple models", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; } - .coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } - #review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; } + * { model: claude-haiku-4-5;reasoning_effort: low; } + .coding { model: claude-sonnet-4-5;reasoning_effort: high; } + #review { model: claude-sonnet-4-5;reasoning_effort: high; } " ] rankdir=LR diff --git a/test/docs/workflows/stylesheets/example.dot b/test/docs/workflows/stylesheets/example.dot index 785142a88..9db42550b 100644 --- a/test/docs/workflows/stylesheets/example.dot +++ b/test/docs/workflows/stylesheets/example.dot @@ -2,9 +2,9 @@ digraph Example { graph [ goal="Build and review a utility function", model_stylesheet=" - * { llm_model: claude-haiku-4-5; llm_provider: anthropic; } - .coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; } - #review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; } + * { model: claude-haiku-4-5;} + .coding { model: claude-sonnet-4-5; reasoning_effort: high; } + #review { model: gemini-3.1-pro-preview;} " ] diff --git a/test/styled.dot b/test/styled.dot index 815fe8001..7ec940bb3 100644 --- a/test/styled.dot +++ b/test/styled.dot @@ -2,9 +2,9 @@ digraph Styled { graph [ goal="Build a styled pipeline", model_stylesheet=" - * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } - .code { llm_model: claude-opus-4-6; } - #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } + * { model: claude-sonnet-4-5;} + .code { model: claude-opus-4-6; } + #critical_review { model: gpt-5.2;reasoning_effort: high; } " ]