From 5932b500e783b6f5cde93e6bcfab8b2cb367dae4 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 23 Feb 2026 09:13:36 -0500 Subject: [PATCH] Implement GET /pipelines/{id}/graph endpoint for SVG rendering Stores DOT source in ManagedPipeline and pipes it through `dot -Tsvg` on request. Returns image/svg+xml on success, 502 if graphviz is unavailable, 404 if pipeline not found. Resolves spec gap #1. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + crates/attractor/src/engine.rs | 49 +-- crates/attractor/src/server.rs | 129 ++++++ .../coding-agent-loop-simplifications.md | 378 ++++++++++++++++++ docs/agent/reviews/confirmed-gaps.md | 16 +- docs/agent/reviews/spec-gap-analysis.md | 15 +- docs/designs/cli-design.md | 311 ++++++++++++++ pipelines/factory/README.md | 156 ++++++++ pipelines/factory/architect.dot | 18 + pipelines/factory/architect.png | Bin 0 -> 28044 bytes pipelines/factory/expand.dot | 19 + pipelines/factory/expand.png | Bin 0 -> 20604 bytes pipelines/factory/implement.dot | 33 ++ pipelines/factory/implement.png | Bin 0 -> 30148 bytes .../factory/prompts/architect/blueprint.md | 37 ++ .../factory/prompts/architect/diagram.md | 8 + pipelines/factory/prompts/expand/execute.md | 8 + pipelines/factory/prompts/expand/propose.md | 16 + pipelines/factory/prompts/implement/fix.md | 8 + .../factory/prompts/implement/implement.md | 14 + pipelines/factory/prompts/implement/plan.md | 17 + pipelines/factory/prompts/implement/review.md | 16 + .../factory/prompts/implement/validate.md | 13 + pipelines/factory/prompts/seed/draft.md | 10 + pipelines/factory/prompts/seed/ingest.md | 10 + pipelines/factory/prompts/specify/clarify.md | 12 + pipelines/factory/prompts/specify/require.md | 26 ++ pipelines/factory/prompts/sync/apply.md | 7 + pipelines/factory/prompts/sync/detect.md | 18 + pipelines/factory/prompts/sync/propose.md | 8 + pipelines/factory/seed.dot | 18 + pipelines/factory/seed.png | Bin 0 -> 21173 bytes pipelines/factory/specify.dot | 18 + pipelines/factory/specify.png | Bin 0 -> 26303 bytes pipelines/factory/sync.dot | 23 ++ pipelines/factory/sync.png | Bin 0 -> 39567 bytes test/branching.dot | 16 + test/conditions.dot | 16 + test/invalid.dot | 6 + test/parallel.dot | 19 + test/simple.dot | 12 + test/styled.dot | 19 + 42 files changed, 1434 insertions(+), 66 deletions(-) create mode 100644 docs/agent/reviews/coding-agent-loop-simplifications.md create mode 100644 docs/designs/cli-design.md create mode 100644 pipelines/factory/README.md create mode 100644 pipelines/factory/architect.dot create mode 100644 pipelines/factory/architect.png create mode 100644 pipelines/factory/expand.dot create mode 100644 pipelines/factory/expand.png create mode 100644 pipelines/factory/implement.dot create mode 100644 pipelines/factory/implement.png create mode 100644 pipelines/factory/prompts/architect/blueprint.md create mode 100644 pipelines/factory/prompts/architect/diagram.md create mode 100644 pipelines/factory/prompts/expand/execute.md create mode 100644 pipelines/factory/prompts/expand/propose.md create mode 100644 pipelines/factory/prompts/implement/fix.md create mode 100644 pipelines/factory/prompts/implement/implement.md create mode 100644 pipelines/factory/prompts/implement/plan.md create mode 100644 pipelines/factory/prompts/implement/review.md create mode 100644 pipelines/factory/prompts/implement/validate.md create mode 100644 pipelines/factory/prompts/seed/draft.md create mode 100644 pipelines/factory/prompts/seed/ingest.md create mode 100644 pipelines/factory/prompts/specify/clarify.md create mode 100644 pipelines/factory/prompts/specify/require.md create mode 100644 pipelines/factory/prompts/sync/apply.md create mode 100644 pipelines/factory/prompts/sync/detect.md create mode 100644 pipelines/factory/prompts/sync/propose.md create mode 100644 pipelines/factory/seed.dot create mode 100644 pipelines/factory/seed.png create mode 100644 pipelines/factory/specify.dot create mode 100644 pipelines/factory/specify.png create mode 100644 pipelines/factory/sync.dot create mode 100644 pipelines/factory/sync.png create mode 100644 test/branching.dot create mode 100644 test/conditions.dot create mode 100644 test/invalid.dot create mode 100644 test/parallel.dot create mode 100644 test/simple.dot create mode 100644 test/styled.dot diff --git a/.gitignore b/.gitignore index c5dd4628e..096f2fd15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target .env +attractor-run-* diff --git a/crates/attractor/src/engine.rs b/crates/attractor/src/engine.rs index 924a0d964..ea9ebbcb3 100644 --- a/crates/attractor/src/engine.rs +++ b/crates/attractor/src/engine.rs @@ -79,31 +79,11 @@ impl BackoffConfig { } } -/// Predicate that determines whether an error is retryable. -/// Returns `true` if the error should be retried, `false` to fail immediately. -pub type ShouldRetryFn = std::sync::Arc bool + Send + Sync>; - /// Retry policy for node execution. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RetryPolicy { pub max_attempts: u32, pub backoff: BackoffConfig, - pub should_retry: ShouldRetryFn, -} - -impl std::fmt::Debug for RetryPolicy { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RetryPolicy") - .field("max_attempts", &self.max_attempts) - .field("backoff", &self.backoff) - .field("should_retry", &"") - .finish() - } -} - -/// Default should_retry predicate: retries transient errors only. -fn default_should_retry() -> ShouldRetryFn { - std::sync::Arc::new(|err| err.is_retryable()) } impl RetryPolicy { @@ -113,7 +93,6 @@ impl RetryPolicy { Self { max_attempts: 1, backoff: BackoffConfig::default(), - should_retry: default_should_retry(), } } @@ -128,7 +107,6 @@ impl RetryPolicy { max_delay_ms: 60_000, jitter: true, }, - should_retry: default_should_retry(), } } @@ -143,7 +121,6 @@ impl RetryPolicy { max_delay_ms: 60_000, jitter: true, }, - should_retry: default_should_retry(), } } @@ -158,7 +135,6 @@ impl RetryPolicy { max_delay_ms: 60_000, jitter: true, }, - should_retry: default_should_retry(), } } @@ -173,7 +149,6 @@ impl RetryPolicy { max_delay_ms: 60_000, jitter: true, }, - should_retry: default_should_retry(), } } } @@ -200,7 +175,6 @@ fn build_retry_policy(node: &Node, graph: &Graph) -> RetryPolicy { RetryPolicy { max_attempts, backoff: BackoffConfig::default(), - should_retry: default_should_retry(), } } @@ -585,7 +559,7 @@ impl PipelineEngine { Ok(o) => o, Err(e) => { // Gap #7: Check should_retry predicate before retrying - if attempt < policy.max_attempts && (policy.should_retry)(&e) { + if attempt < policy.max_attempts && handler.should_retry(&e) { let delay = policy.backoff.delay_for_attempt(attempt); self.emitter.emit(&PipelineEvent::StageFailed { name: node.label().to_string(), @@ -2026,25 +2000,6 @@ mod tests { assert_eq!(resolve_thread_id(None, &node, &graph, None), None); } - // --- default_should_retry tests --- - - #[test] - fn default_should_retry_retries_transient_errors() { - let should_retry = default_should_retry(); - assert!(should_retry(&AttractorError::Handler("timeout".to_string()))); - assert!(should_retry(&AttractorError::Engine("transient".to_string()))); - assert!(should_retry(&AttractorError::Io("connection reset".to_string()))); - } - - #[test] - fn default_should_retry_rejects_terminal_errors() { - let should_retry = default_should_retry(); - assert!(!should_retry(&AttractorError::Parse("bad syntax".to_string()))); - assert!(!should_retry(&AttractorError::Validation("invalid".to_string()))); - assert!(!should_retry(&AttractorError::Stylesheet("bad rule".to_string()))); - assert!(!should_retry(&AttractorError::Checkpoint("corrupt".to_string()))); - } - // --- Gap #15: Manifest goal field test --- #[tokio::test] diff --git a/crates/attractor/src/server.rs b/crates/attractor/src/server.rs index e8f3d8ff5..ca80e9c8a 100644 --- a/crates/attractor/src/server.rs +++ b/crates/attractor/src/server.rs @@ -41,6 +41,7 @@ pub struct ApiQuestion { /// Snapshot of a managed pipeline. struct ManagedPipeline { + dot_source: String, status: PipelineStatus, error: Option, interviewer: Arc, @@ -104,6 +105,7 @@ pub fn build_router(state: Arc) -> Router { .route("/pipelines/{id}/checkpoint", get(get_checkpoint)) .route("/pipelines/{id}/context", get(get_context)) .route("/pipelines/{id}/cancel", post(cancel_pipeline)) + .route("/pipelines/{id}/graph", get(get_graph)) .with_state(state) } @@ -160,6 +162,7 @@ async fn start_pipeline( pipelines.insert( pipeline_id.clone(), ManagedPipeline { + dot_source: req.dot_source.clone(), status: PipelineStatus::Running, error: None, interviewer: Arc::clone(&interviewer), @@ -358,6 +361,64 @@ async fn cancel_pipeline( } } +async fn get_graph( + State(state): State>, + Path(id): Path, +) -> Response { + let dot_source = { + let pipelines = state.pipelines.lock().expect("pipelines lock poisoned"); + match pipelines.get(&id) { + Some(pipeline) => pipeline.dot_source.clone(), + None => return StatusCode::NOT_FOUND.into_response(), + } + }; + + let mut child = match tokio::process::Command::new("dot") + .arg("-Tsvg") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(_) => { + return ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({"error": "graphviz dot command not available"})), + ) + .into_response(); + } + }; + + if let Some(mut stdin) = child.stdin.take() { + use tokio::io::AsyncWriteExt; + let _ = stdin.write_all(dot_source.as_bytes()).await; + // stdin is dropped here, closing the pipe + } + + match child.wait_with_output().await { + Ok(output) if output.status.success() => ( + StatusCode::OK, + [("content-type", "image/svg+xml")], + output.stdout, + ) + .into_response(), + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({"error": format!("dot failed: {stderr}")})), + ) + .into_response() + } + Err(e) => ( + StatusCode::BAD_GATEWAY, + Json(serde_json::json!({"error": format!("dot process error: {e}")})), + ) + .into_response(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -739,4 +800,72 @@ mod tests { let body = body_json(response.into_body()).await; assert_eq!(body["status"].as_str().unwrap(), "completed"); } + + #[tokio::test] + async fn get_graph_returns_svg() { + let state = create_app_state(test_registry); + let app = build_router(Arc::clone(&state)); + + // Start a pipeline + let req = Request::builder() + .method("POST") + .uri("/pipelines") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({"dot_source": MINIMAL_DOT})).unwrap(), + )) + .unwrap(); + + let response = app.clone().oneshot(req).await.unwrap(); + let body = body_json(response.into_body()).await; + let pipeline_id = body["id"].as_str().unwrap().to_string(); + + // Request graph SVG + let req = Request::builder() + .method("GET") + .uri(format!("/pipelines/{pipeline_id}/graph")) + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(req).await.unwrap(); + + // If graphviz is not installed, we get 502 — skip assertion + if response.status() == StatusCode::BAD_GATEWAY { + return; + } + + assert_eq!(response.status(), StatusCode::OK); + + let content_type = response + .headers() + .get("content-type") + .expect("content-type header should be present") + .to_str() + .unwrap(); + assert_eq!(content_type, "image/svg+xml"); + + let bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let svg = String::from_utf8_lossy(&bytes); + assert!( + svg.contains(">` for write captures would eliminate `WriteFileEnv`, `EditFileEnv`, and `ShellCapturingEnv` entirely. +**Why:** ~220 lines of near-identical boilerplate across four structs. Every time the `ExecutionEnvironment` trait changes, four mocks must be updated in addition to the ones in `test_support.rs` and `openai.rs`. + +### 1.2 Duplicate MockFileEnv in openai.rs tests + +**File:** `crates/coding-agent-loop/src/profiles/openai.rs`, lines 436-516 +**Current:** `MockFileEnv` reimplements the full `ExecutionEnvironment` trait to support `Mutex` for write/delete operations in apply_patch tests. +**Simplification:** Consolidate into a single shared mock in `test_support.rs` that supports mutable file operations. The existing `MockExecutionEnvironment` already has a `files: HashMap` field -- wrapping it in `Mutex` (or making a `MutableMockEnv` variant) would replace `MockFileEnv`. +**Why:** Another ~80 lines of duplicated trait implementation. This is the same problem as 1.1 but in a different file. + +--- + +## 2. Duplicated Profile Boilerplate + +### 2.1 linux_env() helper duplicated across 4 test modules + +**Files:** +- `crates/coding-agent-loop/src/profiles/mod.rs`, lines 96-103 +- `crates/coding-agent-loop/src/profiles/anthropic.rs`, lines 164-170 +- `crates/coding-agent-loop/src/profiles/gemini.rs`, lines 215-220 +- `crates/coding-agent-loop/src/profiles/openai.rs`, lines 425-432 + +**Current:** Each test module defines an identical `linux_env()` function that creates a `MockExecutionEnvironment` with `working_dir: "/home/test"`, `platform_str: "linux"`, `os_version_str: "Linux 6.1.0"`. +**Simplification:** Add `MockExecutionEnvironment::linux()` as a named constructor in `test_support.rs`. +**Why:** Four identical copies of the same 6-line function. If the mock struct changes, all four must be updated. + +### 2.2 Repetitive ProviderProfile implementations across three profiles + +**Files:** +- `crates/coding-agent-loop/src/profiles/anthropic.rs`, lines 41-57 +- `crates/coding-agent-loop/src/profiles/gemini.rs`, lines 41-56 +- `crates/coding-agent-loop/src/profiles/openai.rs`, lines 43-57 + +**Current:** `id()`, `model()`, `tool_registry()`, and `tool_registry_mut()` have identical implementations in all three profile structs. The only differences are the string returned by `id()`. +**Simplification:** Introduce a `BaseProfile` struct containing the common `model: String` and `registry: ToolRegistry` fields, then each profile delegates to it. This could be done with a macro or simple struct composition. For example: + +```rust +struct BaseProfile { + id: &'static str, + model: String, + registry: ToolRegistry, +} +``` + +Each profile wraps `BaseProfile` and the four boilerplate methods delegate to it. +**Why:** Removes ~15 lines of identical code per profile (45 lines total) and makes it impossible for them to drift. + +### 2.3 ParallelTestProfile vs TestProfile duplication + +**File:** `crates/coding-agent-loop/src/test_support.rs`, lines 126-420 +**Current:** `ParallelTestProfile` and `TestProfile` are nearly identical `ProviderProfile` implementations. The only difference is `supports_parallel_tool_calls` (false vs true) and an optional `context_window` field. +**Simplification:** Merge into a single `TestProfile` with configurable fields: + +```rust +pub(crate) struct TestProfile { + pub registry: ToolRegistry, + pub parallel_tool_calls: bool, + pub context_window: usize, +} +``` + +The `with_tools` constructor defaults `parallel_tool_calls` to `false` and `context_window` to `200_000`. A `.with_parallel()` builder method or a `TestProfileBuilder` enables the parallel variant. +**Why:** Eliminates ~60 lines of duplicated trait implementation and makes the test intention clearer. + +--- + +## 3. Redundant or Unused Code + +### 3.1 History::new() is redundant with Default derive + +**File:** `crates/coding-agent-loop/src/history.rs`, lines 10-12 +**Current:** `History::new()` manually creates `Self { turns: Vec::new() }`, and `#[derive(Default)]` is on the struct. +**Simplification:** Remove the manual `new()` method entirely and use `History::default()` everywhere, or keep `new()` but implement it as `Self::default()`. Currently both exist and do the same thing. +**Why:** Two ways to do the same thing is confusing. Pick one. + +### 3.2 EventEmitter::new() is redundant with Default impl + +**File:** `crates/coding-agent-loop/src/event.rs`, lines 12-15 and 34-38 +**Current:** `new()` and `default()` are both defined, `default()` just calls `new()`. +**Simplification:** This is a standard Rust pattern and is fine, but `#[must_use]` on `new()` but not on `Default::default()` is inconsistent. Consider removing the manual `Default` impl and adding `#[must_use]` consistently, or just keep one constructor. +**Why:** Minor, but reduces cognitive load. + +--- + +## 4. Control Flow Simplifications + +### 4.1 Simplify followup loop in process_input + +**File:** `crates/coding-agent-loop/src/session.rs`, lines 193-208 +**Current:** +```rust +loop { + self.run_single_input(¤t_input).await?; + let next_followup = self.followup_queue.lock() + .expect("followup queue lock poisoned") + .pop_front(); + match next_followup { + Some(followup) => { current_input = followup; } + None => break, + } +} +``` +**Simplification:** Use `while let`: +```rust +self.run_single_input(¤t_input).await?; +while let Some(followup) = self.followup_queue.lock() + .expect("followup queue lock poisoned") + .pop_front() +{ + self.run_single_input(&followup).await?; +} +``` +**Why:** Eliminates the mutable `current_input` variable and the `loop`/`match`/`break` pattern. The intent is clearer: process the initial input, then process followups until the queue is empty. + +### 4.2 Simplify SubAgentManager::spawn success path + +**File:** `crates/coding-agent-loop/src/subagent.rs`, lines 67-87 +**Current:** +```rust +let task = tokio::spawn(async move { + let result = session.process_input(&task_prompt).await; + let turns = session.history().turns(); + let turns_used = turns.len(); + let last_text = turns.iter().rev().find_map(|t| { + if let Turn::Assistant { content, .. } = t { + Some(content.clone()) + } else { + None + } + }); + let success = result.is_ok(); + if let Err(e) = result { + return Err(e); + } + Ok(SubAgentResult { + output: last_text.unwrap_or_default(), + success, + turns_used, + }) +}); +``` +**Simplification:** The `success` variable is computed from `result.is_ok()`, then `result` is checked for `Err` immediately after. Since `success` is always `true` when reaching the `Ok` path: +```rust +let task = tokio::spawn(async move { + session.process_input(&task_prompt).await?; + let turns = session.history().turns(); + let last_text = turns.iter().rev().find_map(|t| match t { + Turn::Assistant { content, .. } => Some(content.clone()), + _ => None, + }); + Ok(SubAgentResult { + output: last_text.unwrap_or_default(), + success: true, + turns_used: turns.len(), + }) +}); +``` +**Why:** Eliminates the unnecessary `success` variable (always `true` on the Ok path) and the redundant `if let Err(e) = result { return Err(e) }` pattern which is just `result?`. + +### 4.3 validate_tool_args empty schema check is overly complex + +**File:** `crates/coding-agent-loop/src/session.rs`, lines 654-658 +**Current:** +```rust +if schema.is_null() + || (schema.is_object() && schema.as_object().map_or(true, |o| o.is_empty())) +{ + return Ok(()); +} +``` +**Simplification:** The `map_or(true, |o| o.is_empty())` is confusing because `as_object()` returns `None` when `is_object()` is false, but we already checked `is_object()`. So the `map_or(true, ...)` default of `true` is dead code. Simplify to: +```rust +if schema.is_null() { + return Ok(()); +} +if let Some(obj) = schema.as_object() { + if obj.is_empty() { + return Ok(()); + } +} +``` +**Why:** The original combines null check and empty-object check with boolean operators in a way that requires careful reading. The separated version is immediately clear. + +--- + +## 5. Structural / Architectural Simplifications + +### 5.1 extract_signatures_from_assistant should use if-let instead of match + +**File:** `crates/coding-agent-loop/src/loop_detection.rs`, lines 14-22 +**Current:** +```rust +fn extract_signatures_from_assistant(turn: &Turn) -> Vec { + match turn { + Turn::Assistant { tool_calls, .. } => tool_calls + .iter() + .map(|tc| tool_call_signature(&tc.name, &tc.arguments)) + .collect(), + _ => vec![], + } +} +``` +**Simplification:** This function is only called in one place (line 35), where the result is immediately checked with `if !sigs.is_empty()`. The function could be inlined, but even if kept, consider using `if let`: +```rust +fn extract_signatures_from_assistant(turn: &Turn) -> Vec { + let Turn::Assistant { tool_calls, .. } = turn else { + return vec![]; + }; + tool_calls + .iter() + .map(|tc| tool_call_signature(&tc.name, &tc.arguments)) + .collect() +} +``` +**Why:** The `let-else` pattern makes the happy path less indented and immediately shows the function's purpose. + +### 5.2 build_request constructs system prompt on every call + +**File:** `crates/coding-agent-loop/src/session.rs`, lines 371-403 +**Current:** `build_request()` calls `build_system_prompt()` every iteration of the tool-call loop (called from `run_single_input` inside the `loop` at line 237). The system prompt, project docs, and environment context do not change during a single input processing cycle. +**Simplification:** Compute the system prompt once at the start of `run_single_input` and pass it into `build_request`, or cache it as a field that's rebuilt only when `initialize()` or `process_input()` is called. +**Why:** Avoids redundant string allocation and concatenation on every LLM round-trip. For sessions with many tool rounds, this is significant wasted work. + +### 5.3 estimate_token_count also rebuilds the system prompt + +**File:** `crates/coding-agent-loop/src/session.rs`, lines 514-553 +**Current:** `estimate_token_count()` calls `build_system_prompt()` again to get its length, duplicating the work already done in `build_request()` on the same iteration. +**Simplification:** If the system prompt is cached per proposal 5.2, `estimate_token_count` can read from the cache. Alternatively, pass the already-built system prompt length to `check_context_usage`. +**Why:** Double construction of the system prompt per LLM call is wasteful. + +### 5.4 ProviderProfile trait returns owned Strings unnecessarily + +**File:** `crates/coding-agent-loop/src/provider_profile.rs`, lines 20-21 +**Current:** `fn id(&self) -> String` and `fn model(&self) -> String` return owned `String` values. Every call allocates. +**Simplification:** Return `&str` instead: +```rust +fn id(&self) -> &str; +fn model(&self) -> &str; +``` +All implementations store the model as a `String` field and the id as a string literal, so returning `&str` is straightforward. +**Why:** Eliminates unnecessary heap allocation on every call. These methods are called frequently (every `build_request` call). + +--- + +## 6. Naming and Clarity + +### 6.1 EnvContext fields lack consistent naming + +**File:** `crates/coding-agent-loop/src/profiles/mod.rs`, lines 13-21 +**Current:** The struct has fields `date`, `model_name`, `knowledge_cutoff` alongside `git_branch`, `is_git_repo`. The non-git fields use varying naming conventions -- `date` is vague (what date?), `model_name` is redundant (just `model` would match `ProviderProfile::model()`). +**Simplification:** Rename `date` to `today` or `current_date`, and `model_name` to `model` for consistency with the trait method name. +**Why:** Clearer intent and consistent naming. + +### 6.2 Turn::Steering and Turn::System are semantically close + +**File:** `crates/coding-agent-loop/src/types.rs`, lines 5-29 +**Current:** `Turn::System` and `Turn::Steering` both represent injected content. `System` maps to `Role::System` in the LLM message, while `Steering` maps to `Role::User`. +**Simplification:** No code change needed, but adding a brief doc comment to each variant clarifying the distinction would help. Currently there's no documentation explaining when to use which. +**Why:** A reader must trace through `convert_to_messages()` to understand the difference. + +--- + +## 7. Tool Construction Boilerplate + +### 7.1 Repeated parameter extraction pattern in tool executors + +**Files:** `crates/coding-agent-loop/src/tools.rs` (lines 26-28, 61-66, 94-106, 168-175, 216-237, 263-269) and `crates/coding-agent-loop/src/subagent.rs` (lines 189-193, 237-244, 275-278, 312-315) + +**Current:** Every tool executor manually extracts parameters with the same pattern: +```rust +let param = args.get("param") + .and_then(|v| v.as_str()) + .ok_or_else(|| "Missing required parameter: param".to_string())?; +``` +This pattern repeats ~15 times across the codebase with slight variations. +**Simplification:** Introduce a small helper: +```rust +fn required_str<'a>(args: &'a serde_json::Value, key: &str) -> Result<&'a str, String> { + args.get(key) + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("Missing required parameter: {key}")) +} +``` +**Why:** Reduces boilerplate and ensures consistent error messages across all tools. + +--- + +## 8. Dead or Near-Dead Code + +### 8.1 build_env_context_block (no-context variant) has limited use + +**File:** `crates/coding-agent-loop/src/profiles/mod.rs`, lines 50-53 +**Current:** `build_env_context_block` wraps `build_env_context_block_with` with a default `EnvContext`. It's only used in one test. +**Simplification:** Inline the default at the one test call site. Or keep it as a convenience but make it `#[cfg(test)]`. +**Why:** Public API surface should be intentional. If this is only for tests, mark it as such. + +### 8.2 SubAgent::id() and SubAgent::depth() are only used in tests + +**File:** `crates/coding-agent-loop/src/subagent.rs`, lines 28-35 +**Current:** `SubAgent` has `id()` and `depth()` accessor methods. +**Simplification:** Verify these are used outside tests. If they are only used in the test at line 348 (`manager.get(&agent_id).unwrap().depth()`), consider whether the `get()` method on the manager (and these accessors) serve a real purpose, or if they exist only to test internals. +**Why:** Exposing internal state for testing purposes adds API surface that must be maintained. + +### 8.3 Unused import: std::sync::Arc in profiles/openai.rs + +**File:** `crates/coding-agent-loop/src/profiles/openai.rs`, line 9 +**Current:** `use std::sync::Arc;` is imported at the module level. It's used only in `make_apply_patch_tool()` for the executor closure. +**Simplification:** This is fine for production code. Just noting it's not used in the profile implementation itself, only in the private tool factory. +**Why:** Minor observation, no action needed. + +--- + +## 9. Error Handling + +### 9.1 Inconsistent error types: String vs AgentError + +**Files:** Throughout the crate +**Current:** The `ExecutionEnvironment` trait uses `Result`, tool executors return `Result`, while `Session` methods return `Result<(), AgentError>`. The `SubAgentManager` also uses `Result`. +**Simplification:** Consider using `AgentError` (or a dedicated `ToolError`) throughout instead of raw `String` errors. At minimum, `SubAgentManager` methods that return user-facing errors should use a typed error. +**Why:** `String` errors lose the ability to match on error variants, making programmatic error handling impossible. This is a larger refactor but would significantly improve the API. + +### 9.2 Lock poisoning panics could be handled + +**Files:** `crates/coding-agent-loop/src/session.rs` (lines 143, 149, 199, 354) and `crates/coding-agent-loop/src/subagent.rs` (line 112) +**Current:** `.expect("...lock poisoned")` is used on every mutex lock. +**Simplification:** This is actually fine for most use cases -- a poisoned lock indicates a panic occurred while the lock was held, which is a serious bug. No change recommended, but documenting the deliberate choice would help. +**Why:** No action needed. This is the standard Rust approach. + +--- + +## 10. Test Organization + +### 10.1 ProviderTestProfile in provider_profile.rs duplicates TestProfile + +**File:** `crates/coding-agent-loop/src/provider_profile.rs`, lines 86-139 +**Current:** A `ProviderTestProfile` struct is defined with a custom `build_system_prompt` that includes platform info. It exists alongside `TestProfile` in `test_support.rs`. +**Simplification:** If the test-specific behavior (including platform in prompt) matters, add a flag to `TestProfile` to enable it. If not, use `TestProfile` directly. +**Why:** The comment on line 85 says "uses distinct id/model and a custom build_system_prompt", but examining the tests, the custom prompt is only checked for containing "linux" and "1" (docs count). These tests could use the shared `TestProfile` with a more flexible assertion. + +### 10.2 Session tests define CapturingProvider inline + +**File:** `crates/coding-agent-loop/src/session.rs`, lines 1225-1254 and 1411-1432 +**Current:** Two separate inline `CapturingProvider` structs are defined within test functions in the same module. Both capture request data for assertion. +**Simplification:** Extract a single `CapturingLlmProvider` into `test_support.rs` that captures the full request, then tests can assert on whichever field they need. The first one (line 1225) captures `reasoning_effort`, the second (line 1411) captures messages. A single mock that captures the full `Request` would serve both. +**Why:** Two near-identical mock providers in the same test module is unnecessary duplication. + +--- + +## Summary + +| Category | Count | Estimated Lines Saved | +|----------|-------|-----------------------| +| Duplicated mock implementations | 3 | ~350 | +| Duplicated profile boilerplate | 3 | ~120 | +| Redundant code | 2 | ~15 | +| Control flow simplifications | 3 | ~20 | +| Structural improvements | 4 | ~50 (plus perf gains) | +| Naming and clarity | 2 | 0 (documentation) | +| Tool construction boilerplate | 1 | ~30 | +| Dead or near-dead code | 3 | ~20 | +| Error handling | 2 | 0 (design decision) | +| Test organization | 2 | ~80 | +| **Total** | **25** | **~685** | + +The highest-impact simplifications are: +1. **Consolidating mock ExecutionEnvironment implementations** (proposals 1.1, 1.2) -- eliminates the most duplicated code +2. **Merging TestProfile and ParallelTestProfile** (proposal 2.3) -- reduces test infrastructure +3. **Caching system prompt per input cycle** (proposals 5.2, 5.3) -- both a clarity and performance win +4. **Returning &str from ProviderProfile::id() and ::model()** (proposal 5.4) -- cleaner API +5. **Helper for parameter extraction** (proposal 7.1) -- reduces the most pervasive boilerplate pattern diff --git a/docs/agent/reviews/confirmed-gaps.md b/docs/agent/reviews/confirmed-gaps.md index 0258c898a..ca97852c7 100644 --- a/docs/agent/reviews/confirmed-gaps.md +++ b/docs/agent/reviews/confirmed-gaps.md @@ -53,11 +53,9 @@ The spec (lines 1196-1206) defines 5-step thread resolution for `full` fidelity. The spec (line 1165) says: "If the previous node used `full` fidelity, degrade to `summary:high` for the first resumed node." A grep for `fidelity.*degrad|summary.high.*resume` across all sources returns zero matches. The resume code at `engine.rs:591-608` performs no fidelity degradation. -### 7. Retry Policy `should_retry` — too coarse +### 7. ~~Retry Policy `should_retry` — too coarse~~ — RESOLVED -**CONFIRMED** - -`default_should_retry()` at `engine.rs:100-103`: `Arc::new(|_| true)` — retries ALL errors. The spec (line 556) requires: retry on 429/5xx/network errors, no retry on 401/403/400/validation/config errors. `build_retry_policy` at lines 178-189 always uses the default. `AttractorError` has no `is_retryable()` method. +`AttractorError::is_retryable()` classifies errors by variant (`Handler`/`Engine`/`Io` = retryable; `Parse`/`Validation`/`Stylesheet`/`Checkpoint`/`Cancelled` = terminal). The `Handler` trait now has a `should_retry(&self, err: &AttractorError) -> bool` method (default delegates to `is_retryable()`). The engine's `execute_with_retry` calls the handler method directly. Handlers can override to customize retry behavior. ### 8. Direction type not validated @@ -107,11 +105,9 @@ The spec (line 1693): "non-zero means skip the tool call." `codergen.rs:98-103` The spec (line 1260): "manifest.json -- Pipeline metadata (name, goal, start time)." `engine.rs:217-233` `write_manifest()` writes `pipeline_name`, `start_time`, `node_count`, `edge_count` — no `goal` field despite `graph.goal()` being available. Integration test at lines 1527-1532 confirms the four fields without `goal`. -### 16. Error categories — no retryable/terminal classification +### 16. ~~Error categories — no retryable/terminal classification~~ — RESOLVED -**CONFIRMED** - -The spec (Appendix D, lines 2115-2123) defines Retryable, Terminal, and Pipeline error categories. `error.rs:1-33` has 7 variants (`Parse`, `Validation`, `Engine`, `Handler`, `Checkpoint`, `Stylesheet`, `Io`) with no retryability metadata, no `is_retryable()` method, no `ErrorCategory` enum. +`AttractorError::is_retryable()` at `error.rs:38` classifies variants as retryable (`Handler`, `Engine`, `Io`) or terminal (`Parse`, `Validation`, `Stylesheet`, `Checkpoint`, `Cancelled`). The `Handler` trait's `should_retry` default impl delegates to this method. No `ErrorCategory` enum, but the classification is functionally equivalent. ### 17. Spec self-contradicts on `default_max_retry` @@ -169,7 +165,7 @@ Minor sub-gap: the engine-level schema uses key `"status"` while the spec's Appe | 4 | Thread ID resolution 1/5 steps | CONFIRMED | Moderate | | 5 | Checkpoint retry counters not persisted | CONFIRMED | Moderate | | 6 | Checkpoint fidelity degradation missing | CONFIRMED | Moderate | -| 7 | should_retry retries all errors | CONFIRMED | Moderate | +| 7 | ~~should_retry retries all errors~~ | RESOLVED | ~~Moderate~~ | | 8 | Direction values not validated | CONFIRMED | Low | | 9 | Stylesheet lint brace-balance only | CONFIRMED | Low | | 10 | Undocumented Shape selector | CONFIRMED | Low | @@ -178,7 +174,7 @@ Minor sub-gap: the engine-level schema uses key `"status"` while the spec's Appe | 13 | Pre-hook fail instead of skip | CONFIRMED | Low | | 14 | No parallel integration test | CONFIRMED | Low | | 15 | Manifest missing goal field | CONFIRMED | Low | -| 16 | No error retryable/terminal classification | CONFIRMED | Moderate | +| 16 | ~~No error retryable/terminal classification~~ | RESOLVED | ~~Moderate~~ | | 17 | Spec contradicts itself on default_max_retry | CONFIRMED | Low (spec bug) | | P1 | Checkpoint resume incomplete | PARTIAL | Moderate | | R1 | Missing variable handling | REFUTED | — | diff --git a/docs/agent/reviews/spec-gap-analysis.md b/docs/agent/reviews/spec-gap-analysis.md index 32faa9e8f..ad7fc1d7c 100644 --- a/docs/agent/reviews/spec-gap-analysis.md +++ b/docs/agent/reviews/spec-gap-analysis.md @@ -4,7 +4,7 @@ Comparison of the implementation in `crates/attractor/` against `docs/specs/attr ## Summary -The core pipeline engine, DOT parsing, edge selection, condition evaluation, retry logic, checkpoint/resume, validation, and all 10 handler types are implemented. The HTTP server with SSE is implemented. Context fidelity preamble synthesis, thread ID plumbing to backends, engine cancellation, recording/replay, and preset retry policies are all implemented. The remaining gaps are **SVG graph rendering** and **no DOT-level mechanism for custom retry predicates**. +The core pipeline engine, DOT parsing, edge selection, condition evaluation, retry logic, checkpoint/resume, validation, and all 10 handler types are implemented. The HTTP server with SSE is implemented. Context fidelity preamble synthesis, thread ID plumbing to backends, engine cancellation, recording/replay, and preset retry policies are all implemented. The `should_retry` predicate is customizable via the `Handler` trait. SVG graph rendering via `GET /pipelines/{id}/graph` is implemented. **No remaining gaps.** --- @@ -19,6 +19,7 @@ The core pipeline engine, DOT parsing, edge selection, condition evaluation, ret | 3.4 | Goal gate enforcement with retry target fallback chain | Done | | 3.5-3.6 | Retry logic with backoff, jitter, preset policies, allow_partial | Done | | 3.6 | Preset retry policies selectable by name from DOT (`retry_policy` attr) | Done | +| 3.6 | `should_retry` predicate customizable via `Handler` trait method | Done | | 3.7 | Failure routing (fail edge, retry_target, fallback, termination) | Done | | 3.8 | Single-threaded traversal with parallel handler isolation | Done | | 4.1-4.2 | Handler interface and registry (explicit type > shape > default) | Done | @@ -64,17 +65,13 @@ The core pipeline engine, DOT parsing, edge selection, condition evaluation, ret ## Gaps -### 1. GET /pipelines/{id}/graph (SVG Rendering) (Spec 9.5) +### ~~1. GET /pipelines/{id}/graph (SVG Rendering) (Spec 9.5)~~ — RESOLVED -**Status: Not implemented** +The endpoint is implemented. The DOT source is stored in `ManagedPipeline` and piped through `dot -Tsvg` on request, returning `image/svg+xml`. Returns 502 if graphviz is unavailable, 404 if pipeline not found. -The spec lists `GET /pipelines/{id}/graph` to return a rendered graph visualization (SVG). The HTTP server does not implement this endpoint. No graphviz dependency exists, and the original DOT source is not stored in `ManagedPipeline` after parsing. +### ~~2. `should_retry` Predicate Customization (Spec 3.6)~~ — RESOLVED -### 2. `should_retry` Predicate Customization (Spec 3.6) - -**Status: Default predicate only** - -The `RetryPolicy` struct has a `should_retry: ShouldRetryFn` field and the Rust API supports custom predicates. However, all preset policies (`none`, `standard`, `aggressive`, `linear`, `patient`) and `build_retry_policy()` use the same `default_should_retry()` predicate. There's no DOT-level mechanism for per-node retry predicate customization. The spec defines `should_retry` in the `RetryPolicy` struct but only describes a default predicate — it's unclear whether per-node customization is required. +Handlers can now override `should_retry(&self, err: &AttractorError) -> bool` on the `Handler` trait. The default impl delegates to `err.is_retryable()`. The engine's `execute_with_retry` calls the handler method directly. The `ShouldRetryFn` type and `RetryPolicy.should_retry` field have been removed. There's no DOT-level mechanism for per-node retry predicate customization, which matches the spec (no DOT syntax defined for this). Note: the spec's default predicate description references HTTP status codes (429, 5xx, 401, 403, 400) but the implementation classifies retryability by `AttractorError` variant (`Handler`/`Engine`/`Io` = retryable). Reasonable for Rust but not a 1:1 mapping. diff --git a/docs/designs/cli-design.md b/docs/designs/cli-design.md new file mode 100644 index 000000000..d3c27608f --- /dev/null +++ b/docs/designs/cli-design.md @@ -0,0 +1,311 @@ +# CLI Design + +## Binary + +The `attractor` crate (`crates/attractor/`) gains a `[[bin]]` target named `attractor` alongside its existing library. No separate CLI crate. + +New files: `src/main.rs`, `src/cli/mod.rs`, `src/cli/run.rs`, `src/cli/validate.rs`. + +New dependencies added to the `attractor` crate: `clap`, `anyhow`, `dotenvy`, `chrono` (all already in workspace). + +## Command structure + +``` +attractor run [OPTIONS] +attractor validate [OPTIONS] +attractor --version +attractor --help +``` + +## `attractor run` + +``` +Usage: attractor run [OPTIONS] + +Arguments: + Path to a .dot pipeline file + +Options: + --logs-dir Log/artifact directory [default: ./attractor-run-] + --dry-run Execute with a simulated LLM backend (no API calls) + --auto-approve Auto-approve all human-in-the-loop gates + --resume Resume from a checkpoint JSON file + --model Override default LLM model for all nodes + --provider Override default LLM provider (anthropic, openai, gemini) + -v, --verbose... Verbosity level (-v summary, -vv full details) + -h, --help Show help +``` + +## `attractor validate` + +``` +Usage: attractor validate [OPTIONS] + +Arguments: + Path to a .dot pipeline file + +Options: + -h, --help Show help +``` + +Parse, transform, and validate only. Prints errors and warnings, exits 0 if no errors. + +### Environment variables + +| Variable | Purpose | +|----------|---------| +| `ANTHROPIC_API_KEY` | Anthropic API key | +| `OPENAI_API_KEY` | OpenAI API key | +| `GEMINI_API_KEY` | Google Gemini API key | + +Loaded via `dotenvy::dotenv().ok()` at startup (`.env` file support, non-fatal if missing). + +## Execution flow + +### `attractor validate` + +``` +validate_command(args): + 1. Read .dot file + 2. PipelineBuilder::new().prepare(&source) -> (Graph, Vec) + 3. Print "Parsed pipeline: {name} ({n} nodes, {m} edges)" + 4. Print errors (Severity::Error) to stderr + 5. Print warnings (Severity::Warning) to stderr + 6. If errors -> exit 1 + 7. Print "Validation: OK", exit 0 +``` + +### `attractor run` + +``` +main() + 1. dotenvy::dotenv() + 2. parse CLI args (clap) + 3. dispatch to run_command(args) or validate_command(args) + +run_command(args): + 1. Read .dot file from args.pipeline + 2. Prepare pipeline + PipelineBuilder::new().prepare(&source) + -> (Graph, Vec) + 3. Print parsed summary to stdout + "Parsed pipeline: {name} ({n} nodes, {m} edges)" + "Goal: {goal}" + 4. Check for validation errors (Severity::Error) + If any -> print to stderr, exit 1 + 5. Print warnings (Severity::Warning) to stderr + + 6. Create logs directory + args.logs_dir or generate ./attractor-run- + fs::create_dir_all() + 7. Build LLM client + If --dry-run -> skip (no backend set on engine, handlers use dry-run stubs) + Else -> unified_llm::Client::from_env() + If no providers configured -> warn to stderr, continue as dry-run + 8. Resolve model/provider + CLI --model/--provider override > graph-level defaults > auto-detect from available providers + 9. Build CodergenBackend + Wire up LLM client + coding-agent-loop with ExecutionEnv rooted at cwd + 10. Build PipelineEngine + Create HandlerRegistry, register built-in handlers + Set backend on codergen handler + Create EventEmitter + If -v or -vv -> attach stderr logging callback (level determines format) + Set interviewer: + --auto-approve -> AutoApproveInterviewer + else -> ConsoleInterviewer + 11. Execute or resume + If --resume -> load Checkpoint from file, engine.run_from_checkpoint() + Else -> engine.run() + 12. Print result + "=== Pipeline Result ===" + "Status: {SUCCESS|FAIL|PARTIAL_SUCCESS|...}" + "Notes: ..." (if present) + "Failure: ..." (if present) + "Logs: {logs_dir}" + 13. Exit code + 0 if SUCCESS or PARTIAL_SUCCESS + 1 otherwise +``` + +## Clap types + +```rust +#[derive(Parser)] +#[command(name = "attractor", version, about = "DOT-based pipeline runner for AI workflows")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Launch a pipeline from a .dot file + Run(RunArgs), + /// Parse and validate a pipeline without executing + Validate(ValidateArgs), +} + +#[derive(Args)] +struct RunArgs { + /// Path to the .dot pipeline file + pipeline: PathBuf, + + /// Log/artifact directory + #[arg(long)] + logs_dir: Option, + + /// Execute with simulated LLM backend + #[arg(long)] + dry_run: bool, + + /// Auto-approve all human gates + #[arg(long)] + auto_approve: bool, + + /// Resume from a checkpoint file + #[arg(long)] + resume: Option, + + /// Override default LLM model + #[arg(long)] + model: Option, + + /// Override default LLM provider + #[arg(long)] + provider: Option, + + /// Verbosity level (-v summary, -vv full details) + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, +} + +#[derive(Args)] +struct ValidateArgs { + /// Path to the .dot pipeline file + pipeline: PathBuf, +} +``` + +## Verbosity levels + +The `-v` flag uses `clap::ArgAction::Count` to support two levels. Both levels write to stderr. + +### `-v` (verbose) -- one-line summary per event + +Prints the event kind and key identifying fields on a single line. Matches the style of the C reference implementation (kind + node + attempt + optional data), extended with the richer fields available in the Rust `PipelineEvent` enum. + +``` +[PIPELINE_STARTED] name=Deploy id=abc-123 +[STAGE_STARTED] name=build index=1 +[STAGE_COMPLETED] name=build index=1 duration=4523ms +[STAGE_RETRYING] name=test index=2 attempt=2 delay=200ms +[STAGE_FAILED] name=test index=2 error="assertion failed" will_retry=true +[PARALLEL_STARTED] branches=3 +[PARALLEL_BRANCH_STARTED] branch=lint index=0 +[PARALLEL_BRANCH_COMPLETED] branch=lint index=0 duration=1200ms success=true +[PARALLEL_COMPLETED] duration=5100ms succeeded=2 failed=1 +[INTERVIEW_STARTED] stage=review_gate question="Approve changes?" +[INTERVIEW_COMPLETED] question="Approve changes?" answer="Approve" duration=12340ms +[INTERVIEW_TIMEOUT] stage=review_gate duration=30000ms +[CHECKPOINT_SAVED] node=test +[PIPELINE_COMPLETED] duration=45230ms artifacts=3 +[PIPELINE_FAILED] error="goal gate unsatisfied" duration=32100ms +``` + +No JSON, no multi-line output. Suitable for tailing in a terminal alongside normal output. + +### `-vv` (very verbose) -- full event details + +Prints every event as nicely formatted, multi-line output with all fields. Uses indented key-value pairs under a header line. + +``` +── STAGE_COMPLETED ────────────────────────── + name: build + index: 1 + duration_ms: 4523 + +── STAGE_FAILED ───────────────────────────── + name: test + index: 2 + error: assertion failed + will_retry: true + +── PARALLEL_COMPLETED ─────────────────────── + duration_ms: 5100 + success_count: 2 + failure_count: 1 + +── INTERVIEW_COMPLETED ────────────────────── + question: Approve changes? + answer: Approve + duration_ms: 12340 +``` + +Every field on the event variant is printed. This is the "dump everything" mode for debugging pipeline behavior. + +## Output conventions + +- **stdout**: Pipeline summary, result status. +- **stderr**: Warnings, verbose events, progress messages, LLM provider warnings. + +## Error handling + +| Condition | Behavior | +|-----------|----------| +| .dot file not found / unreadable | Print error to stderr, exit 1 | +| Parse failure | Print parse error to stderr, exit 1 | +| Validation errors | Print each error to stderr, exit 1 | +| No LLM providers (without --dry-run) | Warn to stderr, continue in dry-run mode | +| Engine execution error | Print error to stderr, exit 1 | +| Pipeline completes with FAIL | Print result, exit 1 | +| Pipeline completes with SUCCESS/PARTIAL_SUCCESS | Print result, exit 0 | + +All errors go through `anyhow` at the CLI boundary. The `main` function catches the result and formats it. + +## File layout + +``` +crates/attractor/ + Cargo.toml -- add [[bin]], clap, anyhow, dotenvy, chrono deps + src/ + main.rs -- entry point: dotenvy, clap parse, dispatch + cli/ + mod.rs -- re-exports + run.rs -- run_command() + validate.rs -- validate_command() + lib.rs -- existing library (unchanged) + ... +``` + +## Cargo.toml changes + +Add to `crates/attractor/Cargo.toml`: + +```toml +[[bin]] +name = "attractor" +path = "src/main.rs" + +[dependencies] +# ... existing deps ... +clap.workspace = true +anyhow.workspace = true +dotenvy.workspace = true +chrono.workspace = true +``` + +Add `anyhow` to workspace root `Cargo.toml` `[workspace.dependencies]`: + +```toml +anyhow = "1" +``` + +(Already present in workspace deps.) + +## What this design does NOT cover + +- Web/SSE server mode (`attractor serve`) -- separate subcommand later +- JSON/structured output mode (`--output json`) -- can be added later +- Signal handling (Ctrl-C graceful shutdown) -- defer to follow-up +- Config file support (e.g., `~/.config/attractor/config.toml`) -- not needed yet diff --git a/pipelines/factory/README.md b/pipelines/factory/README.md new file mode 100644 index 000000000..a4f1f0562 --- /dev/null +++ b/pipelines/factory/README.md @@ -0,0 +1,156 @@ +# Software Factory Pipelines + +Six Attractor pipelines that implement a spec-driven software factory. Markdown docs are the source of truth — code is a derived artifact. + +## Document Structure + +The factory operates on three layers: + +1. **Product** — what to build (requirements, acceptance criteria) +2. **Architecture** — how to build it (blueprints, diagrams, stack decisions) +3. **Code** — the implementation (derived from the first two) + +The first two layers are markdown docs. The third is generated from them. + +``` +docs-internal/ + product/ Layer 1: WHAT to build + business-problem.md + personas.md + product-description.md + current-state.md + success-metrics.md + technical-requirements.md + features/ + NNN-feature.md per-feature requirements + acceptance criteria + + architecture/ Layer 2: HOW to build it + foundation-blueprints/ + backend.md stack-wide backend decisions + data-layer.md stack-wide data decisions + frontend.md stack-wide frontend decisions + system-diagrams/ + entity-relationship-diagram.md + sequence-diagram.md + system-architecture.md + features/ + NNN-feature.md per-feature blueprint (data model, API, UI) + +src/ Layer 3: the implementation (generated) +``` + +## Pipelines + +### 1. Seed + +Bootstrap product context from raw inputs. Run once per product. + +**Input:** Conversations, notes, designs, existing code — anything unstructured. +**Output:** Filled `product/` docs. + +![Seed pipeline](seed.png) + +[seed.dot](seed.dot) | Prompts: [ingest](prompts/seed/ingest.md), [draft](prompts/seed/draft.md) + +--- + +### 2. Specify + +Define what a feature does in implementation-agnostic terms. Run once per feature. + +**Input:** Product context docs + a feature idea (passed as `goal`). +**Output:** `product/features/NNN-feature.md` with user stories and acceptance criteria. + +![Specify pipeline](specify.png) + +[specify.dot](specify.dot) | Prompts: [clarify](prompts/specify/clarify.md), [require](prompts/specify/require.md) + +--- + +### 3. Architect + +Translate approved requirements into a technical blueprint. Run once per feature, after Specify. + +**Input:** `product/features/NNN-feature.md` + foundation blueprints + codebase. +**Output:** `architecture/features/NNN-feature.md` + updated system diagrams. + +![Architect pipeline](architect.png) + +[architect.dot](architect.dot) | Prompts: [blueprint](prompts/architect/blueprint.md), [diagram](prompts/architect/diagram.md) + +--- + +### 4. Implement + +Generate working code from a feature blueprint. Run once per feature, after Architect. No human gate — the blueprint is the approved plan, and the validate/fix loop converges autonomously. + +**Input:** Feature blueprint + foundation blueprints + codebase. +**Output:** Committed code (migrations, models, API, UI, tests). + +![Implement pipeline](implement.png) + +[implement.dot](implement.dot) | Prompts: [plan](prompts/implement/plan.md), [implement](prompts/implement/implement.md), [validate](prompts/implement/validate.md), [fix](prompts/implement/fix.md) + +All nodes share `fidelity="full"` with `thread_id="impl"` so the agent maintains full context across the loop. `goal_gate=true` on Validate ensures the pipeline cannot exit until all acceptance criteria pass. + +--- + +### 5. Sync + +Detect and resolve drift between the three layers. Run continuously (after merges, on schedule, or on demand). + +**Input:** A change to any layer (code, product docs, or architecture docs). +**Output:** Updated docs or code that restore alignment. + +![Sync pipeline](sync.png) + +[sync.dot](sync.dot) | Prompts: [detect](prompts/sync/detect.md), [propose](prompts/sync/propose.md), [apply](prompts/sync/apply.md) + +Short-circuits to Exit when no drift is detected, avoiding unnecessary human interaction. + +--- + +### 6. Expand + +Evolve the product by adding, modifying, or removing features. Run as needed. + +**Input:** Human intent (passed as `goal`), e.g. "split feature X" or "add Y". +**Output:** Updated document tree, ready for Implement. + +![Expand pipeline](expand.png) + +[expand.dot](expand.dot) | Prompts: [propose](prompts/expand/propose.md), [execute](prompts/expand/execute.md) + +--- + +## How They Compose + +``` +Seed ──→ Specify ──→ Architect ──→ Implement + (1x) (1x per (1x per (1x per + feature) feature) feature) + ↕ ↕ ↕ + Sync ←────── Sync ←────── Sync + (continuous) + ↕ + Expand + (as needed) +``` + +- **Seed** runs once to bootstrap product context +- **Specify** and **Architect** run once per feature to produce requirements and blueprints +- **Implement** runs once per feature to produce code +- **Sync** runs continuously to keep all three layers aligned +- **Expand** runs when the product evolves (new features, splits, removals) + +The docs are always the source of truth. If you delete all code and run Implement for every feature, you get the system back. + +## Prompt References + +DOT files reference prompts with `@`-style paths relative to the DOT file: + +```dot +plan [label="Plan Implementation", prompt="@prompts/implement/plan.md"] +``` + +The `@` prefix tells the engine to read the file contents as the prompt. Prompts support `$goal` variable expansion. diff --git a/pipelines/factory/architect.dot b/pipelines/factory/architect.dot new file mode 100644 index 000000000..83e58ec69 --- /dev/null +++ b/pipelines/factory/architect.dot @@ -0,0 +1,18 @@ +digraph architect { + graph [ + goal="", + label="Architect" + ] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + blueprint [label="Write Blueprint", prompt="@prompts/architect/blueprint.md", reasoning_effort="high"] + diagram [label="Update Diagrams", prompt="@prompts/architect/diagram.md"] + approve [shape=hexagon, label="Approve Architecture"] + + start -> blueprint -> diagram -> approve + approve -> exit [label="[A] Accept"] + approve -> blueprint [label="[R] Revise"] +} diff --git a/pipelines/factory/architect.png b/pipelines/factory/architect.png new file mode 100644 index 0000000000000000000000000000000000000000..4340128365cb2cdc9f136905c85d7f9b4e55fd3a GIT binary patch literal 28044 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 literal 0 HcmV?d00001 diff --git a/pipelines/factory/expand.dot b/pipelines/factory/expand.dot new file mode 100644 index 000000000..434c1c5ae --- /dev/null +++ b/pipelines/factory/expand.dot @@ -0,0 +1,19 @@ +digraph expand { + graph [ + goal="", + label="Expand" + ] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + propose [label="Propose Changes", prompt="@prompts/expand/propose.md", reasoning_effort="high"] + approve [shape=hexagon, label="Approve Changes"] + execute [label="Execute Changes", prompt="@prompts/expand/execute.md"] + + start -> propose -> approve + approve -> execute [label="[A] Accept"] + approve -> propose [label="[R] Revise"] + execute -> exit +} diff --git a/pipelines/factory/expand.png b/pipelines/factory/expand.png new file mode 100644 index 0000000000000000000000000000000000000000..84546dd44927e457835c6ad7682a5512fb4195df GIT binary patch literal 20604 zcmbWfc|2Ed+cmrmAqgQv$dDxS6p|rRsSqK_EK@S4D0Aj0Ns^>OB~ubI&m?3D5i%r| zlA%;)&-!-V_x*g{=XpQxAMd_CSM`&8_dd_#JdU;2v5pIg|tjbIv2wvidrkauOz3EgRbLN(f4M%2m_q1?v zt%H=J)cmwN@6$MvZ89)mG3p)5dg$xCFr$>~V{lxHY|BX&qb|eo>EO&OS2t;i8pV%n zVsa)w5*YMm_D>4K?i0+lm)7gf$W+Lz=-o+8?XPqk37z*<3O>loNJB{;Li%l+8e=ip zfBy$1`MhqZ5yyZ2hV%_lOpjXs=UXY&Z$(89694rN@!MBFx8c${hr)_L<;Vs;rfB5~ z6Q5rvgjoFi{8o2GqzC-Zr$nVuC+j@R%iCVOoqJ+zjFE>YE{IE~`i0Nz2)FX(-qu#< z`HxLZOibx=)Kbr#&YrcAWG^l*rlZOzD=W*&%Brs3Dd?qeZ*Xw%_U+rt%a@6RhK7dc z&&vc^`tA<88XFs1TB;CqwX#wvUb|6J$G{+7lfR|4^(d|*N9shjv#>NaHEqYm!osqB zd-zAR|%X2pbrgczeWwY0RPq@?!m-(MNY z%g2|t$U{kfT3%qsbsp83@84JY+B)v-FZ7yhy~Zg&^P!@I+UM-qv&pJKf#&AJ?J4qm zj^AJH`<`Xw?B*sO)XJ-Gue4J_TzsZBgu>I)lZuMUvE$y-?|!TPXBHLCeZqJc1GC7l zKHt85J9_Mxh_JA*h{!Sg;op^$laof|_wq2s4qMEMVU0*&n#jO1Kks@t>H9voL`hDS0tOy^Y zM(mR(Pp)329T2Hy$j-@`pEn&)ww6xjc8GMQy zsNjk2-o5+W{o&K6Pct+79jtHZ2qIRFYHPoK{kn)J@(05y(UWIL*RGhR?5wP_GS_2y zK7IMpu+1^>x6rO#6o=h$3@>p&&gi7HO@p02GU34kh67a3-ESl% znds{WAJ@h|>+A0Be)n#toC|xndUscszA@|zmQ_U^OI!}70$Q>mABnmW4eKb0x5E>t3@jlql9$7Aufk!1^ zSvFqVyxe>0)iq8mx?;thw#kw53*TSI@4^$DX})ez6Ihj*xhMNBMI?^P-`Ua3$B(Om zHg@(~n~SqGNOE&?3&|}kELb1N!P;kKEnV~B{`UQQTF!IqKcZ*Po;?Rh7iY)iq?U4yo%dA&?zY%<+*eLH`k4RAz(K+%6`yROrDfl8 zb9Z%h_44v^`pYdLgp1tDM04Ei2+r7yg@c2`$&)9Mykc)=W@pomdS1SK+2b>fH|vq3 zN3RY#8`Rqtc$nDW>{(h`Uc7h_-+t-Rej-?te`;!qgOfAm&Ye3cDRDdYwZ3^X&AHLE zxGlGJ+5DzjIsY4~BH?34j_mheoNJ0{5xSeSKRpD>t|CFk@k1VNXwwjEsz# znHi#?{mq+0*LK+3*ho3|KAo*zq3?)0%)87vy#Mx%8xJ2pF7{vg*_UIIla&=cD6VOC zH6~^kKmYZe%BPKtC@mb_+}xaGsrhtJPb_N!*GT!@eN=t-ySH!o`S@(`u(q~WR{TlJ z{3Xv!GWpH)#%p}*>(`5m^Q6-kuPrMl_wxDkV2V;~GfEF@aRI+Zu<-XiN2r#vB{`$A zW7{SVX=+kZQ`cPn-HH^|+}zyPSL`Sei$6z^E*kNq1IU3pt~t2441b~8wV0Kke;nEE z$dOAwKec2WjzemQ(){?`rs2?aE>xbg4i0i&pR`d(S$v*$;=fTbmlx+J5QryEoKOi| zrP;OQ}uZ~ytrl@(jm6+9H~ z1#PU!i;7MU7N2u=b{-iS$vK(#wxXXjC7j%tIlhU>tAifHCNFQWza7ftCT{>~8yrD1iTIF{Jg z-)~jvJC8DHYHGS>pb}vtVxI_^$m=va-rh0Qa4HM{~#KI{O{uBdtS+BErIO`*a-r?^QQenYVA>6o+Lh^BAv} zV>2)HuoKt)#a^*JS_DjqbBM^#T-+ZVf)BJX}m^!C-O zt=JEn(c0El+NC;8@`M)uwdD&xgKi}yC1^kD&NzJhoy72v7TKDjXKH!bn zP*_-4QIRZvypyY|tD_^enHj0jb4D%lC~)cFD0_MRL}u8j;*V1*A||$VGC4G~;f~aq z8-JO)FW}Var7B+dGyR@ai{cKqPgzG=ojK>^lMIpZ`3US@?dsyj+o}ePQ1wNf#3xJ(auKagE?B|bhr6_xd+Ute70hLEigV6R>|tS-(wRlg+!BqV0v7dp1Iv_wZoyN_1yW~4z% zGR)NDuFoj>VVvA0!0|}N=AC6C*?o^Ay1I`~WFZb&Xm?9WK1ZpknMDfvIz1h0GcY&a z5PIdxjvYIeSFUSBzkT}_Y3_icVm&X{=g*(NzsrlccFkToIbCjR_g1g-=a1i4ni?M` zJM%FsJ6jy61E`|Cy&Zom<$(%>qJcYTlGHIYOgNM=IXM|UD5S}gkeaHS(cRnIOG8=T zn`MY}jU=SyYE+WeGli0!nyP%^8w+9GdTVd0#n{Y@%DH!Nc!Ma4?^ z7B>BJ2Pnt9NGRT0@x$QYjt0q1n>HOk-ic$7-c3uX8Xg|*t!QCp#yOi+QdAU$#Zj%# zuPrVra*{o*rWW$_+Bd2z#>pt@RaMHw7&e02a?aTJPao5A3gVT6gCjoY1yJzZ+#I(4 z*wLd*(TzNTi+l?kqO-sRz+&kya~Q@wnwkLg(%ax<@2>SlfWIMkFEcAM|*DspJQOP`NASbjnsH4QO; zR(25NVFxE?&D{=bNo0ODU*378QnqmrAwm*RY>FmswfXwfzY zbz>5I#r_}zgQm2#;ppdW%=C!?-ZeBdpu9-Yr6^v=b_)MU@F^qU=sm<2cGSRNTdH1vUtd$|85cP-d`a^VNS%th zqmz?%W=~sNo0jV>UdpF0%Fcgz^61f=uM&#Y{{3b2nWI}y+FOb7p{yf212Y5odV6?C zN=gP7hA1BEY;E=O^epq4wRCpQ2DC~`v%!secsvIZjfgm8XZI*Pd=uT>$(fmsiFqbw zW;U_Yltk&JU!RemE-!NajsnzIOjYq;^dN$-U2D%W%yd3?F8nN^9@g96e^^sS~#}yP5gvhvb=~7V8M#z%YCl3Ww8f1Y@ zL@bAlppw$+&~$jE-(R<3X75j)@&o}Tl#=ccBnt#8!cbQqbcsnvaINf`IULJ+*NvPEn5 zL~larec><$SD z8Pz*3yKi4#d;8Xg=LimQLBT_xnBQ%q2t!QbUO`j$3JFnCQPCF|*U)HtdP=40@c@5& zUFWN&ol&G9*f}`5rxrq94nC4GetF^hN&i-!gci`TeKIoVf|3bRngGWjBLK!;89uZs zC|m%qC2F7Oro41PzHGZ)e6IgAa06(H($3E2=EE8q6gBJP4UyP+PWhi1zKa-W~M`w{O3aq`MwK#|U-<`g=s+ZK>zP zV$aH%h_ej@Vv z)Yq?X?kf+ise)Wm5o2*7Q~*jB9)}GKoFZzGH-|p`5M2XmAc6sj{*?ThpC8{~MUf{6 z1_p-v&y7rv9cu$>Mpl2gQ$y|YwjT)L{eFL@Q4H}g?lo(2Lc-Da?%jjB@HEc}OR!^~ zbMN2Lpw0&aX2!4Vq){=)f^)F#Kz0&aSj6VO*EVhM=;%29KxKQeRzN3GAF{d-%TXhv zUgA<3Kb5GCpbRf>u(p6+ikz6Ls#Wi2x`1T~c84QJHX|+c^fWYgbPXD4b~-sb*IPKg zdiBa%u}b3_pN^uDJz&-QDnI$4*6;oKWW@O|9j@o?9kX*qc0=ePBLdfco2<71?k(M&_%+$Y_NxUVy!0?^UU_OgQ-<=A4G414;>CaWf%7 z%eBMAiJ2p4tSjkz>%;TWI)ZJjttYa2f#NMwv(N~TT*NV|$6+n4=)v239SAIF2kZF6>Z`}DA86JVxS5{LpLkTOz;RnZ6Ogx) z?2cp|@D0Yi@3|Vra)GOhe`LgfWE&eB2St5m|LrHpGI@xkbFWb4gBuA6=OMauyv3eG zYo=ejwpB&+gn@zQ^nldPxGxhE8IK-?jnZ81duCo%zj8eB_o(P23TpbOn3$o#LHftl zT30PKOPO}Y%l zhdn`1_k_NF)U|8di^Zbq4xc|?_}~Fggbk=5zMzpDO3lDTLkX-**DyUj4dtt6!uJlq z2|z$?Epc5uRl)O$s-!%5x5>f&K*m(2%h(FmVpRZb4vKmB&s7q({ z6E9SSLG&OeP|BeaVv|_K&1w9erasO$Eu@N&#f~W9JZ5CQ3oNR1grl;u$cIC@OAkSB~iNAt(Hb1G|}z3h3&?L6(zwa)wG!dtYl0D^p8LSFY#D@x>A3uIT zf5^A0S;q<8Yxsm9s_Rr{;&&aEqzgUpjdwY8ZRKxFObi9oW=;Nhgqrpv=rG>OJE&cr zE~TE~A5DBzP+)N6$kc4j25TNi6=m^et4&m%B)RI=t*~b@X&=h332)iDm85bQ8fNrN ztr}VY|0{TY)Fg-q8>lJJCnKhQ7@L+HMLd83&lU9b^!P3P++A5(xByz`?(Tj^TtXtQ z`OL!nJT$xHedkU(ISC~mEiNo?$546YjsdEtt@v0=0|T<5fYP-=sD+oUd^`0|o_r3y5p>lS{H74U*gUa zukBJ}zO#Ez*Od3>&70?HZfFY-wI)8x&z?P_r=^vam2FHtvsQ96&+MM!1zJMVzLmv* z^X+;4l&%YRbUuR#zq-!7g^=90?{UwR`W^jU)-^Al@%q-h5IZx?_Y>{gL ze#OqH4RsQ#`gN}TCyb3J$H%v!%0UuRIOY?7<3_C{-G+8X_doKBC50?r4U#r<`~5^L zImEQb-A>5naaXTmz4O5ekQA;RI)Cxv=MpQL+U5DxrC%WE%S%i96%;@#+wUuz>FGUd z_zto3BKRK=ER>uyj=kdI*Ed9=`~%dWc#+N}SZbJqMrWz_)QN)!iB!EvW-kvEbTDJc z0d(u%cL$}o3>Hm69xxLAjY4T_Yb(l{Q&6x?b*QVW)}e4dj*cp{ZL{2!h~L&)%H9g5Fyc{0}}pVN*N|B=hITkcGMVQzKzIMrk7wx$Nzo zzFZCjbM@-c6>s20daGLkOj9|1IyySJMMVqHl##Bvfpl-~KBlCsJRgz174kG085!h@ z!itKDG20bZrs!T|T|rMkdvoz5Z2{Wmi2J{p3=DY*YU)UUNH<_eq)TWjYnLS!Z?A$; zcBCG_G5uR`rWtV1&({|+(!!I0>g?pIZr;>v0OWgPV`G5B@!F4eAG_@`QVHmhLoL53 zjBpGL4D@)smpEu*;sc&W1OwgsUcM|RD{EfupRCD`+QdNINKFmEQGzUW?%a25TLTlM zQ}3DKGUUurkOk}^nm5O8i=Is=$iH#p#@wGje}4Xi>a73`G1s&ZGGDPn`wpb1BS%_P zD!Wa_=Y5~=xr?&P!*g7K2{Nj(lG5jnRFIwapxvo@B_$=ZL!)UGwbxxhS)BfE(9+QG z#%qhReg~WaU3=Txdr!`F2iI&Q+Xlb{qWJjHqvhwnTnFk;N@5$y{~^jo5SIBF85+Ed z*d84_>0}gF2%qTd97_^M#&vc72F;L+ka2k96h!ML5)#xXDXOfj46*+Barf?YEnl-? z8?pA5miJ!J$dpHQLS|B%$W&LZ6F>UU0tyDPm`0IDamgYr@kK^XJes< z{v|6VH5KO*M`#J~@pV73ozVW*8~dHOmHZYT@vxvEzRC8&#g`gc?=2b|614 z00Tf&6`k!7JbvFFdkn!DQ4a}6>~xWK@FSMZP^^&Oe*gS=dEu)8c3E4SX2X<>p>)j9 z!bQXFLwS>(G#SAjfuD6mTe~uFO;JXMnUFmHSqIdpS8#!+%ccUA4wZxlO~CGMPmn|i zI!40Ab7NYaWAnZqbTkdmpD`rXi~fjI*Hnj48XGZsW{iTI^D2Uf`@lt)p_lCg0|O94 zhlVP2AN+=1hSLiy&b*2R{{ws`f)f%FK=Hm-{*;%Ow<`0R zMB5Mgo`;tg2=f(#CqTQ%Z)Ai?zNPEgv+*}?dR@5iPcN@;&o5pG{X@Uce~*)fMg#tF z$LHFGKDt8+C+%EZ{J(Uh$~lOhE}|hketp(06=><~B+*(_U95MrWUOB1FW<_!IS(k` zL@lCw>{c|0>dTe^&N+@)?)%{($F&8}tJ`*ZETLn>QGjW4gCqw>GNkh6SFh^rlh7Bz zW7UtkAJx)o8Xo4_pgFuwf@A9&zd;vK|5_6ESXKL%J$WLipm0i0k6v7DGd=z7#KgX? zuBh19B8bw;?mj*~ryLy1e3eE(BX{lE#kpgLxJ9`DC?GRHG(;5yZ_?en=v}%&G7b!~ zE1lirO}2UQB2q9JVO|sXVQ7d`oME8QkwK}?MoigfhAUj1jA(rIssrWX^P6OX@YiTS zqYdkWN0m#dy7bF|2)=*+erjqek#b=5N`2$M?u(qF;xh2c^@#~+oB4~N%!`f9s0-N} z8O>^YSF9=Va^lFB`$kE#>t+na3Sho3C>pSnj$%Q}0lyp`o!)Rum_sjGdG7S0`W2p-cbm1z^*Z!_(RO(P zwimFxfD^^Z!9hj<`a^mr2$WzduI&mz>#LyN;O9uOxJ&opQnifk%Bm{F(AXJ*S`D%4 z^JB2-LBR$)5+MXHl0N`#P!<;Nv05@79v+C+(6IiD>H-=o>O?l2A&Jz;$=kL8>wvUC z=W}s$iwI!jGc+;z`>jueHD)^#)6Khg(Nud224TzoZhfQJcit8)B(Pqh7HtaTo+3#3 zNDfGLg1dKPX%m_!KvfcN-!8AHKp`8&LG64vP z=UpJ%e9Ir7K2Z__H9;F#O|%?J&wtSiO!@WmCqa;m1zj?)&~qOjA18ugWIz*S{b!rR z!-Ui2b}cuNNN0vh;+iMc@M%Oa&^*ipbrn#vGhe(wo5WB@=MB;V_yLZ@+@5b;pIwJu zURc}|-hwUy8xN1fY3k{Rp_=@gFK!bO5rJ}6;$Dob5ZSPenK|@at5wzI3$r822s_j` z28Oth&s-Ck$Ya^r-zsLSDTrLls=?0AS?I%9-3Z4`N@L`D7Y082yo;#STDpGy`qI*p z$6JGA$6_8m`gCQ}7Q&f1Y>Rg)l$T%NJPMxtqM|OtrJkTw_(ovyDhVE+JETI4LS^wL zTvcUuwB~_oP~dF)*R5#%5y9ZsTRG(xc>SE6g;mbn;~5E(IX7ng9&Jxh=9$UK#N^~M z+or3QgTpeQJ7f9nwgsSl(4VXolXX-;K=4z5VbHh@p`hm9wJS}ZELHWnh{-dEN#Ucr zd3kG_nkMn#NJpS{0l+wKa?;YIzyP6MUAbPK^mOl65g_OaXmF6CJ5F*fY=+dt{bb@C z#|${!-qpTu6=#YT*t19e!Z)Ku+jVrj$Or^2%py2~AJ8>M8^ZlVIk-|^;EUwomNP%_A7Pexk#9um9f(Q!ub`e?8*h#fx$(d}?N9e&8=hjvOI(nVH){%Yl2}S+pJ;6vD+d zA{eiYIDiPS_H(igG%PPSH%#)>I}@%07wP(~0kz^26T``>Jr(_GH$C`vl2aOY1?9iQ>TO!OtiJ5l9KvK0@DTReHHS(goq>FfEaH`^>>%oJyINX8DQ<-G!hcnTR<>&v14McvG)G{?)LZri^ za8K!ybf9S@3FCcm)p!Rt5;!56Jzy#5!LK2O931=A#Nf=lOnd4qB6l-oJ-&uk`9tRI z-6oWjltmoBgRR*Q@iLa0KLha1%>0tB6=S`&^gX`@=OEn8!3+xbU~a(M{{9UVZHTXn z0THRj>p|;F)T{0#r*w7m%%~nddB_0q``Z2cmiX?P5&L(!rfA{+=&{kEr)%Ki=4KN; zB?*ym?ZE$cfJ_-$l{VFPf z6R)nJ@P!$&Q4S2RZ@D;mC_u~2Z3g=_UvNZ|N1Z<&TeN-Kws}M`TAUq%LiH`ySS?Wc zpyh=r=ukM#V4asagde(>!BGJN7u4@(>&>_;G9oQ4E!Dn`XU;8k*_rd>j~`+Z5 z_wT#e*-<_a?G>b<7EIt^WtDSy!!9C{gGK>r)8v;gC=BFBr1^*nGI(!*1HsG#Nks9& z_qo7ISZyG>4E0|cr*OP7UcE~^DdpC!PoRYWZE@Y+CzsL=mix@^3{srA|BaTEtp{8# zYHEhftx$PzF*is;s)mq>^gCP~Fa+rTS3LJw@0;q!hL_|)@$lq;MXn#pe}C)C0U!dW zNbPGHA?Y_8p1{6iL6OR1oH)Ps8|!Q5-sRS5a0mV-``TdrUICmALIcEHWIH5a5IFad z%Cr+%>h!fV>t;2_zz(^%>Khtr{Fl7GzY}wJpZ#;aUCivIfS};VhQ4|-akJ^xMAV$b z8#l)K$Db>w%ARFfeLc(Wzr3(Frs>7Ym#jBXiN+CPIHeGYE-%j61O%)BnxLLVY1&#_ z^X@FELVvYkoWySkLe72U4bM1tC;1o63x^n-_X0~u>gu*@MACbf4a0r_!MjeEUa_#O zL1MA5VqrU6gD8KHSXHv4s_jw8 zGws`S<$X<15X6*&2ZOt2Xqvf!&jZfVdBv1@#&6rNzbC-)HA~ zQGju<#^<2R983LK=(eq-nKKY1o8u^xu}a3McW#8h|fMy_jNnuNsb0d)|lU5KB(U=nRA3Ur@ULEB;Cw<^|}6M&?2lVF8;v1 zzyQIc;vYOObD*%SP(q07&&&q|E^w|u(Ea=)oJvW(Vvy3s6J^JCmNNfvg?40QyyMW)YPA z!Nx{A7tUJaqukA*h2%{7Vakao;n|>HW&iOvCxL=2tH7}aJ6}&uH6|B9%~40 zq07rC=QivMi5MLYqPFtVuMy-En^z-*aWbA5Wes9wO-&F=phVmBZVL0tbC@D>nq(?W;vhzR&L zVZf`r?0q|?#ST2q^wcS?{a$vcBA|GouJ-6Z(iv_cU^_%HEf3E3z`dP6e;(k}&u<0C zJ=eU<3*YSVoL5fnVq?^{=Z?wf0YP~#J^Tf{EGIi#G50Z!d{<8oV7*z1xV-#dAT|&_ zB3L&?4n|axLgnh82fYrgX7dMm6Q?(Q$@3UrJ@+knVVNEBGtGTL=lAjqxvkZr* zBSju<0=-2!|2E|615#4)L6YerwI3Ab0_>L)K_4`xXo%oRr%(R={?-*_ekJJGV2M0rO6Yk`QH6#?TH+B&y#^h3CnrgHd1^vm=oX}dQiBoD z2&}{40z}HLV`wCdz0~=T7pS~TSUB5krOw|t@%Ix42YdTEUh!RruZNlWU$6*WIGQ+K zt6LtWnSSMp`U97@O_N?Av+E#Z2mrZE6d!0$>jmlQ(7G`n7h_Bs0REJs`3B42IrHfzP6nyY(dfoS%mtYgcdz)uaVZoTK96NhEREPTKIq ziNz1kZ~T+$Z;8LVhMfY+zR=AV-Shi2EySr-93||4w2X`uLIR|likcdZqcWM4O+j^m z=`iN>WgAnHkHZ01U$!ef3DU*eDer+j!OsduD;f8C}s5)Q0FB=;t-f%s*z^<_G`axcF10sXy>}%h?r!f0ZGvQEJ?ac1!N>GXU zFV3UKGHoZZOI!ijg|br_bmjeIJp_}3tM;U^qySU1-l`F&2{j_&!1kfq!=Xk z-iL<(UYt#v!b#Ns@DhGd%=!f61~3)2bs$TURSAhxL|puH=g*&$$CQpmsuzfaFT}z~ z0mwW)`({K2CN|Fg_dq;auDu(4V`Mx(I|Ci*uF8>J$!AWuaU-}xYCQJVuH}1x4nSsL zSN^Mu2E8wN*HtNM_P%a!zu@7)$jm$h%ylsEPk4T7xwcidG)+Pa9WCt+NPi&*JuY6< zJ9_k+eMtJIKyGve7B9d>E9@Mze1E9K{rlIiPtj?Ce!#1$bz{>OR(Q0d2CoKE&d8NQ zLV$)$8cp~&rf|vE7Y*pBLD`*Xn@O^-udl4hd{ zjLjW51=07T=T@{kd-ez~BV0jqb$eK2Hs$+xdY&yjMe{aM{W4WTOHpCr_dOwlLBLID zJ1H1b(aY8Cl~32IrBM0wV6B6F%$0mr^~(U-ab(25c6-2cx?g;hbL(qsw) zP2WlO?o-P-@||<;U*6BghP)S?^Q~N1@ZNC^4kH2dj_#e&ATNAnqIu;hGR{FaI1PEf$koStQDchDMDxhna%RTNk{KrkG z#l%C-cZck)psc+7M28fJm|`mWnS9O}jB8r(>($iMAPR2ZKI!2x_t4Tn)sCG;SMB`y zUobl@{QeCEoYdMb9roen6%#|>|E22?#$-0?YzrJZ@9Xl%BgHm97-`O?gH2*$c2)v( z2t2yv^!K=m1*Wf~q4~*DXI_OxMh2y%-b9&c7!bqF%kJ395FD{c6~fp1=<#EucI-vh zA))g0R?BMt%_NQRP)b|*<5oq{usUnByEcI>Qd2ifcB69%i!V+*n}kI<<>46QOl#WE zI`~2qp(sKOpJRfJD`;sl2><}b0`-Fh6pVrW$uQ+zJH)9eaXO3F}Jg^{9S^*v%j@fSX7j5R6$Dt#mQ5Fi+!;)p!ValoEP+4V9Ne~@v@yg zoAvndZi289YMb$!ZV(QHd#Z}0jw4zdXa|%JJ%J})1E$!(#vev zUHtk&L~ZIB_6Uw0JKjz$AkD&0#7G7|60&2YpFu3Ayg2l{V0rr}l}FE>&43QU>OoF? z{r(-vrs6oi!)Bh+XVc)u6l!>19ygsBVNc$k&;lpZ&X|93hqc4K!yBZx`*WHD-b^NKf z$-*eYifDL2Wq`+GKlD2&IYpf-#>RrBj1Z8_AAKl0PfZYe_W$$!KnWb*%Nn!Kv4hJO zJ&D*HX}J_2@O#P0n~&$2mFRgCP~3wo{#6mD(nI3(C#8*d!J!|)*CEP(`<1rs3VZjl zxnveievEm@Awvgo`ZVgNBZ`!i{9)CD@UM} z50gx6VrI}`&e_}l2DED^L`w%7cBL6V0;)HYl7l4sYPB?aiO@b!i+=ekl@1*8CoI*@q}WfYI9u!Nvy!MuJYB?a7^1(wm05(ZdC>lPxZ zFqMU(1xp+YGpT&ufqfgIc;q2p<8}5}eijE;0gmHE$PRb!9({ZQd3i6(QP4idKX)7Z z#y||ISM<8-Ghux~prOkPZP(+Am#S*bojdIDhso5vV~}WZj%~5%*d2nP4Of4B0yzvi za?tcZ0mLrdWN9s6aQeLYe%&cULm8I=Nt8#pw*OqJPzWt(P~WM1 z8H(L>9`g2C3{koBizBb*n^@;IAV{K|nntQXQ_uaBI%}?Hw=gN#nlv zXGq|{fs{j-rO6tn_19;(x+@H2ssYFp>xr685IFi!Klj6#?)7jhW`BgS>P5Cqw54Y; zm-q&K#Bo|@gOqnkMbWVmOz@uHoL4+yZ+ozmBo_lmqsOWcxatefW~v?}^6HcnPRv*h zal$EAd;cpE9*oHFaU*TO)U@(PTw^C5EfJ@{yzE6m0W307Dk>`Q#LLRwI>?Koi&-~P z^##)EfBTlH_;J)mNW(ZJ5kBnL{{2fhtoRfRQSOwo4J$6*hbA^^UTiGgmMs)SRCM&V zXwA|nn$MH-zQE00$#RVPMn*7KVdt%2NglqEn@_uqjg6W>FL$t|Wiw1C&w%>riLoC) zs<12QwFd_i*eG95KPa}KCWQ!U^vL0{#DJbTqAnaC4ps(tKYSdo+uA_HBg4agU}NER zHxk~yFEf-|E`=L>jFpu%c$1b^0H-Dv9KETlgW`>2Brp31(giy58x<1i1;bGyCv!2H z0X&aeEx}ZWYG5Uhap!3l&5e0PCU^#EutQL=I$-6twg848U`#lmprCU{GDMRz|c*f7B3VIa67zWvDw7Yz%G4{+a%3BO&35Q8cJupT{k zfd0sT{Pu7aVVVk17JhsLFEu?E8tpq+hWO}?U|i5G;ReVZ%zoBrXll1qR zkhuxq#vZ>MyVgMmXHwzi7gELSrLkb&3=n z2B22Jx%%_W{z4{}ZE>Xuev-+*932r60gDc%dp!|N(BlaLYBv@VEhv(Rgog39;*e1x zNMu-id?z}A28MqAm`4`xONB9SQ40t2aX4?j7$&AI89CAHLGwQ zppi2gv=IP^ik?1TDFlk=TYw5*T>NobRaLdF=_;#SN@`NlUo1Gf1UP_bn4!L(x&+%2 z45YXY#j@KtJJZIYj1Ju*`C%aBV(`hx#AIWxf#LN9IiWER1z0-WPn{xpIPl?Uvzp|c z-jpfE#~7doIRAeQV(m=UiKrqU(2$go2^HIu&M@wwfN+A4R9itk*Br?$^KT040OnKx zV-3uNm0sIPr|SLv{o6Gm5R*SRI%onGY$!Qgsj@Q?-Un|526Z-WE}QMTn2JrP`*82~ z!UD)ozKDy;n>uVBu>wK!TI(AV$v6Tp52I4j&VY)+cjMc|w_h)1?;~@Np4L&k)tfJ~+`e z0M7zxU~~fK7BF$;fAJKC{g|&L= z!f|iQa-VIu21ZjsTG2P3@>WbdiV>;okPx!Q#4v*bU2qF7&%Dq1^7ekZ)Q|~QdzkQcnAH*46y|l;vyze$ ziIRX?BOh(%dsoYtQC_}`ei~etAR1^IesJd2o*8{i3j>WE*QulXWjvUg|-gIyJL<|^)`08Lpb(m z(cxDBX*6un=R!O3-o8B|A_p*nv1?bxwq1u#ojQerIoQ?p+);&AE+qgU01X@jKj~^W z#WCXHd4O|*3l1N-l&wTpUmy2;4as@7E)sg(-`?VIaDMK0;Cocn3bBlfwf{W(6$+~^3}Jn*h=s+MMF0Op+kCH4O(gS zNlH$>gxE*RPbT>dgj~ZcBWh~uqC1+O;3`Hig2kXK_J2>#A_sv)z+ewZjaopuT3RqH zMoP202joeUEZh`Wbtp^4K7&Dq&}&Ev0V`{LbscJ%%>yVx5XQyD-K?#xUH)BIKwJ(E zf3E#~vN-j|glcL}oTX`iT1vTQ_6SU&Owj6TXoP}EWM|_g559zk8oe`^Ea?RV z|1OfnSy&j)Lc3)PY!CP~Sx2xkR!0!}2RhI7e|v!az>1K|SKi&hBfvh0m1K(6G*8(U z-3T=qh;?%y(liQ`?(yT1g&&Z_?<6Oinwf1O07c+pliIhBTgsO5-k&G=`JE zgpXefu+sE{Mu~Qv2Ot!)!v+ItFp)9jL|;Zw=16xj z)aS;GD^o!#d;WeJ{r?Sg?hyrr#lpb>eX5mA!cW+&OsNa@xk;5h?CD-~NTamh}F!Was=wMT?Yb4ONs_Doy}Qm%}w?DQ||m&$NE{O=bb9DXKaQ$NdP zc=DuWx%b}0qx+B?i!Eq0J4twamc91OQ_C?HCb-mMk!8TsM&!q4nZ|f96NS?P$2;T5 zbM#$Y z7$uvt53XTH@jLC)-yy2JM%gh6dgSRA!jv0YomBypWfbMPD1kz38oSY@{S5 zQCB?#Ui-JxgL9iQ9zPD}!Fjc;y1Wnc{5CKG7klwZN$}1g4A3a;L>CM{cceXEgd7Y)C2L!B&4xZmBHQnA*gE5T`b3pSpn)`}C<$sr=@99wN>$5i005VRH?i z=**+gqGZ?R3(#n~JHdvfvU|W6MnW_l06@m7$bB(D+GBH3!41Y*F_4x$d#KrJw2vH_ zr{=Bu_u3X!Hh5_~6c!J&#`Fyg&=9~d`oWm+2A230C-|!;PzVV{LwR!rHD+@W!lOn) z^n6|0jj5Kk?_AhLv(9U5uYj`@dVpNaveK94jh)7V7~YJ)bLpcyN7_SMNp>!J>i*M` z!c_O!vPIY5KD^qX3RiILkstasb?>;29)L}oKOXxjBrZ-r^uDfGY`ZFC4Iv>XEEmpk zfVA0781UHGG%lS)MKUr9TM6=5!h?jw7(?h#u-Tq{62FIUus@HXeHeGE&U#Fl@U69$ z9OD(@(hn}*l7E4coGegnF<@K*Qy%2oSFGbLE%?x4gqkAxYFD}y=501r_7T6pC22- zxLZ4s?|*(wn2;K26aM?>YDV9Ax|Q@_jU^;a6IoSJY4_f}uy&jC4Ta*>BRs6EtY{e~ zCG8RBB>j+i*Z;bYu&W^Lgf;BJs5|htgha!z0J4uR9lsIjH6oDN@D3QJ#D8yeT6FlY zkF9@&v=5JcS=sx(zC_ikCr=`pCv4aSnQ{Ub$1&OFi?`Lj|9j&TAGx-` ziIqlRHoS&moHMj1&G_G{EXcn$Aw6~!nVPyfhL0(faA1O?;nyU+(W0e=j8NFWf3JuL z$y$PzwJD6Rknd6H@nV*$%1WBB4b|6o{!ascBP=km3;q}``voior!S%h#`va9r(jZm z95z~cN#@c|yKZN7(h~a@@>>0SN0lZ+OcSO>B$U=?DrTWrV1@w%H4jQ6WCJ)f5YM2k zP$i&7PY;#MLP7={SHX}v%%FH?2ASoR74)M0AuhuJ0Zt((DygJ2+|l8N6OZaU2zd>h z8Dp=IAgx)cLZv{oaQ&y1in(Te0V^KT#*Qkm$2Bt-Z~eD~pCzTFFt_UA=}AUBKb0F6 zMv3MX#`b()K7MQunS(^4am3+Woc{6S!|?D}7Ft5X=srUbf`1Lh(hEL5m|Vn&<6kTc zWN3NU!397!asosUTm&;OR0Q;FFaZcD7^`Ca_o@KYJ-~Y+7=2=h`*2-zv(9|{=!NPH z)iZ5l=f9hKqE`JcPNS%#lVOx1UChjemX^r<;SXSA!m>~kFmyn;_;{0!_>cP#mN|YR zI3nT|c62j=f=EFO!^rcG1c8d+by>i4%We9>@(?iU>!D-f@ZKa5*#!bZmOko+!9w!o`!b4^3kYQMV= z)p)MggiSAwU5cFWfKy^!lO?6<1R-G)fpV)d$t|r})!+gcNubOXao~b@%+4JW)H`;L+uVSx;<_s2Oe}r zHjuO~m0Cpqb;F5ORBxlJtQ z--%_Ym7BU)0h!SBcDq9%=@Ui!TtO2)I}4T-p@D zu9>aG06i?DM{Z)Ls;aABc~&kl%Mz;;_e3z3DU?tpO%TD-S^vL&T_g8$+CIqHHuDn$ Pp2QI?Jr-)F8{jH literal 0 HcmV?d00001 diff --git a/pipelines/factory/implement.dot b/pipelines/factory/implement.dot new file mode 100644 index 000000000..03bb75f04 --- /dev/null +++ b/pipelines/factory/implement.dot @@ -0,0 +1,33 @@ +digraph implement { + graph [ + goal="", + label="Implement" + ] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + strategy [shape=hexagon, label="Choose decomposition strategy:"] + + subgraph cluster_impl { + label="Implementation Loop" + node [fidelity="full", thread_id="impl"] + + plan [label="Plan Implementation", prompt="@prompts/implement/plan.md", reasoning_effort="high"] + implement [label="Implement", prompt="@prompts/implement/implement.md"] + review [label="Review", prompt="@prompts/implement/review.md"] + validate [label="Validate", prompt="@prompts/implement/validate.md", goal_gate=true] + fix [label="Fix Failures", prompt="@prompts/implement/fix.md", max_visits=3] + } + + start -> strategy + strategy -> plan [label="[L] Layer-by-layer"] + strategy -> plan [label="[F] Feature slice"] + strategy -> plan [label="[P] Embarrassingly parallel"] + strategy -> plan [label="[S] Sequential / linear"] + plan -> implement -> review -> validate + validate -> exit [condition="outcome=success"] + validate -> fix [condition="outcome!=success", label="Fix"] + fix -> validate +} diff --git a/pipelines/factory/implement.png b/pipelines/factory/implement.png new file mode 100644 index 0000000000000000000000000000000000000000..e3ab57abc2de90a20804f6bf1a6d8fa22627f2f6 GIT binary patch literal 30148 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%0D+m{)9l`|x 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 ingest -> draft -> review + review -> exit [label="[A] Accept"] + review -> draft [label="[R] Revise"] +} diff --git a/pipelines/factory/seed.png b/pipelines/factory/seed.png new file mode 100644 index 0000000000000000000000000000000000000000..18a19ba20fe9ed3ab934f85931a03870e9918356 GIT binary patch literal 21173 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 clarify -> require -> approve + approve -> exit [label="[A] Accept"] + approve -> require [label="[R] Revise"] +} diff --git a/pipelines/factory/specify.png b/pipelines/factory/specify.png new file mode 100644 index 0000000000000000000000000000000000000000..32fec7cb4fcd01ba9fe6e324f1a0d9429da092bc GIT binary patch literal 26303 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 literal 0 HcmV?d00001 diff --git a/pipelines/factory/sync.dot b/pipelines/factory/sync.dot new file mode 100644 index 000000000..dd74fbdfe --- /dev/null +++ b/pipelines/factory/sync.dot @@ -0,0 +1,23 @@ +digraph sync { + graph [ + goal="Detect and resolve drift between product docs, architecture docs, and code", + label="Sync" + ] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + detect [label="Detect Drift", prompt="@prompts/sync/detect.md", reasoning_effort="high"] + propose [label="Propose Changes", prompt="@prompts/sync/propose.md"] + review [shape=hexagon, label="Review Changes"] + apply [label="Apply Changes", prompt="@prompts/sync/apply.md"] + + start -> detect + detect -> exit [condition="context.drift_found=false", label="No drift"] + detect -> propose [condition="context.drift_found=true", label="Drift found"] + propose -> review + review -> apply [label="[A] Accept"] + review -> propose [label="[R] Revise"] + apply -> exit +} diff --git a/pipelines/factory/sync.png b/pipelines/factory/sync.png new file mode 100644 index 0000000000000000000000000000000000000000..19262f8f8f561e230b89874b80344afb6104d27c GIT binary patch literal 39567 zcmZs@2Rzp8`v!csG$kpCN~O?1$|$p8R3a-Pqf#hY*(-`NB1s`aq3o5tM<|g|RyJkt zmCbux&-(w~_w)XKpXYhXxbORWUDtV@$9WvbdAZ8UNK$WL+(4mFs4tuslc!MD*yGRd z)=}Z#!L8++@gJ(2Qj%hnW%B>GIpN+E%3jI^u``O6Zw6Z|)M}@emzSBCZnJ!OesHr| z`{9(+cD~vt6KZPGlHUE?Ch31?RjQTSxr=excdjiO#`QDd#;Wz}*QYh}^Yibpx3{N1 zdNu#YWzpCqjuR)mQ}pWZkN>{#_3Kx^QzopQqDz)JlXB#}q#fP=eNq>J`=+M1zC7Qz zOW6A5A%zd4zr8QZD=B?Q@_hYz=Q-!ix0ja|7pC);Z|6I#n4(`;SyWtX{QC>7yu7?Y zR(xV2cdTkk;rH+II(Ej!+@X@5+T*`V@3mxZ*V57|dA3{X%$YMeQwroQ|7WqAAJ)ZQ zOR}E2YinzJ(ztUqSMJPB54(H%`a6%`TIVS;$8KO?FlD_YxyNd?z5TMlklyXv#nqvb zJ5Ol4SIntrnS1&9xkN`Fk&uwM*P8v>ZvM~MaMp`+jp1mEdTbcw!XW&`>Kjc;@#RPn<0O)KmJ=H zXaC(fi#wm6?G~D8cPtO!_cUn9AQi&dg`gB{?lqgM0Um>ea{H9_uU~Y{~K!U0(1$Wzzk&TDKuppGihWX4B5& zPDhmEjIgTDb_+_#$95TH^vThcj!j-sv8el705>=H4V(gt<;S%6Y+~MfS^hmm zot&ox1(WpZIWI{|?-YF?l$e;v-;$P`{4ziP^nDYP*P`|!ZXs1kI+x7{8;r|Le)sg8 zFzuCHnj7Hit_;2`AwgBs4sw5h47*>nBTLQ6BhE(HnA`go^ts3xyFa^y&> zRnP9hp`q6TW{1x`*(8^CIZ)u$ogW_}wf#%Zo+w`v+;J zqZ}MNPu)MjBKhpV-n|Z3%azCOH5?24L*lL4_WuHLTDQWWSF;@T4#RZ#0ixoZE25Vb(NKud(58FbJRSb%*@PO zzka>yI2UMw zgHk>T_>^V=5}_;Nobzi>ojgg2IJm2R|sR zsE}b>82XvSrmd~LOVIq1&T>r5QHt5e6ehF&n)MDjHq&(tN(ds&mb!P;QGY$v!vC$A z#Xer%SDl@mi~~(+hLwn4?&HVh>r)I`{Bh8?GBa0pd~&XxLIm8zG2OK5WI~;ZY2Ww9 zID~%&e|~CA(ObdJ#nm(TGl|{p?EU-4U%h&zsHjMJ9vB#FVteDpK8m=wxWIskiAiZ? zWxYyIZ|}O(Vq&W$J@>p%PEO|3q#{efb3@|k4la$D5t)Bi#>5!m+xlQ$|JfelukWRN z6OxiXH8ow+VKy{0ym#+jL`)100>z=gajnADt6#1e=7^h{^9|I;vvG1hGaIP8ar5T= z9}~iV8noUBS-p7vd<&m`V-cPsF(u_7ej+4fKOT7IPm^J3MFs1jL+hEDncuv5b6!S9 z8$0|)#LgcJh$nN-$q|lLitQYT&tnx84bILMQcc!<=MFUT`)?trc^a)8A7y5ja`ECtmuJsDnDkWm1q9S*#5OcsvEL@X zkBf`O;p^9n8#Zj9upc=R&Fxx^1Zi+;l`XP33J!I>%HM}?*ul$7&SU%}&P?*XpQt%> z2(rYF9}CONV_M64*HtV2eeikqF?u{flJg12QE)?drY9yYDb`0LZ2U1%Y1hsYZo985?} zEvc)c7Zl7=-LZD<+BboLcUm%epFMlV!1(psRLGqd{-j8Y3FAQ&bfhcG$`pcoy1QTa z`i9f}Zk-Xpd*Gu)o*9n4s;YYP=FLa3u^c5OC0DOq)4F@t4IgJN_==vMp1Zp{IX(>y z49Gqd?BF|@qhb1M^Giyo&Ye3)5?*5B3R%gsiaG`c56ad%LVN&93|R?|-SNP-0G5F393;h+WDK3k$2`8^W5T zq!fSuzEw&}>gds6P?y!o*Y^n9_xNbIB_ z%w?zQ|GlFwdGXll@87?lzi?sS!Gn)$Y7}31dtZ=|Va3s&nX$xLJseVdSM=it(VXf%cJUMB4{KSb;d;}Jjb2(q*a{7Z_y}A-= z`{>oH?SKCK0Z97Q-hLW!SyP{8&6$$*4+x_^gss8b1(FE&^JCb$b!$XaRN$teM z$a^(soDdNa!JB(k1PcDhG}E)od#v3z-c=?hA<;TD#^~`uqs)@sE#yJ`NL{F>h?}G1 zxv49EElpK0wmh&pqxM`&dHJR9KYnCbPaS^!`n6h)^_8Bh3JMClcJC%9x1yrrq<}zY zZ?DVKr%&gaOH0?|0N{Jvyu7tPvbO!RTv-ZAN+rclcVt+O?ML-rKnd+1z#FvBwYa4` zE*xr1QRtc7!u#0C>6l*KQ9FxJ4IE|e>-i4-(ce9$YG0R_E-hFo#i^C9q7lmfUFvi-VlzFjs$jl4XJ9&@UKiQYaBAFjYo z=1*zPG%~(x%nYHYr$^vmW%r(V?j}Lv8wjGbv@|R{-226g!dFL>YZ{enCHq@X3e=WXL<|C3B}b8~BJ=f9tCUG>TG^77G%iBFZ4@g=D^4ZQ0R(yRgk{s5g` zFJC%^u>1M@e~-P!pJ_27Gd$BLPp&uldQj8t?1r}WSv*P4a7^G&bJ6e*NvR7LwkHn~7mzVr<8vV`9jVN)J3EZhH(eqEYQIvCtlarN|^~uH^oJPaVrz(Pk%Q8%Q5Rn8po}+fk!lHTZ5gz^X zXQ`=+vTxtM?B%6-i)~AMilfnHX)6-=7Gm{(kJ#;4^6~XevYOBqnf*;y5iIJNmnXWB z{^<207us9e+T(4@OTOEWD9NIFrW^mdnr^f&P|%{zB2Ao5NlxyJt!;Ki>~@so10o_p z*aP0*$NBj`|M)?NCp7yV;^?>v8+27uz0T8j9t7#wzJN8=I_=x>-0Mf`4YNnDHl_Uls%Y;4Foc=G6yOEb%U0n7a zKmH;jg8inJR%Ap3B~EkqkBa1hds>re-qrVO&4}lai8>wXO2Mi!giz6?g}yn(U``U<}&r+lx{5kMZ%9)Ytps zu_B~;Dgp=0UYxMY^d3!1e54sGNbN8(Fv^cy*;VFCV7%6?TSX@-g+66lO<>jYh7y4o zot`{FkQ6>#wYC@VGD)XWyR*oZfoYlJ>%QgKD@scD=O*tEgjVX!Mpk6sz7-BYS`qI) zq+ZYT^z@X8ZOIE59K82;0>FBG`Eq`IeEit)<01Hvll`?%otYo_tyo1>6{*0sb6@ki z=hYnp}8;sRhSyWw3%g>({K$VHy`fcg#lfMhzS(mJE_6=iWV+%|3BX0!E z+!VrPXm{?s0F2|~<3m<*Ewk*p6CvrhN!-HWJcMrLNk0J`dGYHIl7dt^)xc>}S( z@yNcSMe)$;ExUJ1i-|d9l1G2(>#L=wSB661fba&;|Dc)o041nDt1Rt`@2Ik7LA;Xs zs`h!-h1jU5n~QT(wo9`;z_*N=nwmj2!>595$4jl5A_LyMDMxdtlJ`Iuo9@oU|7^`h z`l>{&&(?P|dE&5vn|BKo;cWiQvEfHzPESwYvSkZ-#}zA9U>Bv%mRg4`uI*?@)Y^5@ z$bn?}z(9}CP!>=ctJkaJUq-F`13DjfgJ7#Lo^U#Vqy`Yj_4G{Z{FNbMMZ@kt$k$VG6;;Z>FIFr z1ard%)M*>-rx4+_2Pp=BIt=n$Yk6A%E{d`!%5K$;kh9Dl?_$H+(}NnS`X zb_(rt4*Sz-1&as z$>!Z|_*RgOr4DMJo>NxsxmJAEcIyjNmoHy#Ox2I(1hTWVvTE5uNA(|Z#(((8kzZId zCue86J$ouhw=5&GnS|(2lP_3_l`B{F*2gpB3l?V2etq_wXf~-FTbyp2$X`WMGn*dp zI8T~mrZ)2mrFpQh4Jjk>e%D1rowJ7rHK?WiCv+;pu0$(Y4=Gz(o@9P7v+v!zcgcG7 z&y+RoUiAdq2l;SZ>rc_N*P*4Qb#ih_xLroSY15{48#c(F69Z_ov$)(WfhIFym3d}9YtgP4nqa%2Pp>h6#jtGH0$)ncjwk>Jea+$fyBgW5j}w}_-f+K zHF|Y11>e8F=n1lU*4}@2rQ_oWgKCN{HM0f{)} zx%0z7&%g@q^WmxP`?c``Zp(;G~l6Zr>_tIhP5QLu>JNn}5_kn6W@ zIidK1?kfEJxyPp`hkHT4AIV66g^~AW`?9jKpV?L?nC&KM?H9(@E-ft`y_!Hy8qe&7 zbYOp#jQa;QZO7K&fBnCWzzfz|)DK@Hh*E*xXF9bJL7&I>nB`uNdwWo!Ra^T{|@*Yz428p(!jxg}p(QuUkaW}`y? z-MoFn6}8dIr|$n=C;4nw2hzmCY)>!)6VpVyBTdu|i!vkc+L`tQ zy?pg55vNgPrhQdNXy{HJ-RDj(rUu&!9FMA{KX-9iw=`R6KMv}Xuk`hP@kIgaNp^5k zoye~wTyYS-M=FE|4BLYjVk{seq%W5y2nuEj+G>QN)cl{JzN&Yt&`E?MH{y}^T)tds zedHw9KdsVP7aQN8pg5`mXBy`2@9(7u98WXUM9m?@WlC`Xc9*0=@EaZ7-AYK79(k#6S+X<{sbM(vb|p30uW#~ZW%H0lYB7161s z8=avxPa(jPLcrX5vuG~V2zi_T_(}1^!GqS9FRNWyiitwQaLl2qN_O!{9?~yK5n7)&3keIa-@I8Hq|Vi=S9wbM zm|6(oJD612kbH*@#838U;l^@*-i_!j>?~q`HYDlLoX{@MM=*2cExdSl&bdEJ>Z~U~ zK8aR<_@bJcmz|wAo12>f**{46a+(e`ZU>Ep0y8!~{=(tE$4IZt$jU;mb-!s5X$m=; z2i(yIJ5^QJh1N->#>e0Kpy3P%2 zczEHXLtF$$HpChlo~w(it@n8lYYQ{-!n{fDD+$!g%KJ1(r58 z__2RaR&x)CnEq`Gg;Ozogq!^Q*%=E=0fmhwSii1-D?k6?Dgcc|=`{ZHXXRyO^AWe` z*1AjCf=Mn%r7!`g{2&uJmlhS-*V$RvS^V_E#fvvfUmP54OxcKqd-Ukh$p;JRxmoqU z5l7s{onj)(3nqN`8uBgL?AW~Tt=~;(H6fulKa+GOP{t+-)-v~EPyPW>PP(`97RJkV z-*yLkbxTKwZriq^^4BK-;`6_M|ERJyPnAY^vPwFwR&Rq1in>y~hV=RK>+rLe-kkJ8 zCx+^-5FCETCnhF_EFEgl0Un;0({1*_1k8dHr`xrwJnVuuQ0{jN{;8;tkVnXDMtCSx zUg!p6G z^j+voN-8QgGchr7s%M5dYw+hv}ELvaq=~G{8PF8Lfi~mDq4XZVz4qkfw z2pOD?P3GRH$ziub9PYxZcjplD`*0kPKVSIxl#JvpR{&*O{`Qi*ntE?1I=W1K8)R+L zrepPSLKw`5d<3HZ#z0-{$y2Ae@g<;1G1MA7SDOB`_Gir;>+O(kY~~o7Jw7?pOvKwS zKJ{k1EU%*Sv$to*Kig((-`AAJg-QYXM@vUX`PZ1a#S3R7pI@G$b+fkNGJ*S9S-XAa z!LCl!C|s`1_}1L4wy?NJ#E#3Ni>85|Su9`G5tZ^_3F1NGpIz}1i1r8=p|1caBdMiL z{omg|J+9x#(*;iDD;_rl8!ku4aP7*V*d$H#?>T`vd&W8i}mVtqR{aTtqY`8|P^pz{GJ{q=(6oWVi4Sj`>+g;EP zuJ~}ON0A29Eh8i2I0T%o_`IdeU#U!;NhVo*1M`b&S?1E~*R7LHHEfGZ%8iIP56G*G zg!c+8ZVyW>2@}@ za6y4?tdASW#hW@h;e*W?3hI1@tufh)f0{`DG8~Z;(8X!te@H#!zW9ol2?+^|0mCO3 zI5;@25Dq->tF5TVen#pANDKSL7$T_M^78VwZrM_$+hb~O9yg^I!|t{f zj4tIOpv&;w{(N-9FA=u`F;Csx71=-&zYrXw;aXA8mk)vxe z^$}paAIq|g!)L$77Pbjop7DYFi~Qh{md0Nxx-5iNtsAwH;NorDoE|NH>{mjK*Px6f^$aN zjS}~r=TuZw0vq*QDk}n$RoX^+By=j?6kz4-^bHK25$<&EgqH!qhipJ~TB6Hx=3g%WAc^EqgR zCxO?{-Q8~{=%)V6GGDI05(iEX5@QiS&0oqMqZIZSs`V>??D4$iMN*e-mOKAzP^@ne zrAQyGMNUti-GHD%rv8l0?D%a|%*W7q$U(B|j|>N&tA)a8iX7G|AiqPWGUzsT7~OaD z04nIZO`BGy-#&T2@B2X?F3pOpk=!zr0kg(fj+cOx=K~-4Jh`r|T~ZUNkYUuZa(;T` zb4A57qHKmr(&R3TOI?(bV!|GtmD>hM2y3=#?dD@<{fZDtcZ%4agr53iHup*zc9T`+ zjf;neG|P0l!QGAHlsNUQ5`SJjKS2x5BT6x{j>FgNHXE9|`|z!^{pG;X-=<2#We!K>Mf!v4wdqqNGVn$zO*|%rT1HtkCKg#q9wdH%D zUw2#z;FH#`Y-mTt@R3p0PoieDFE5QO*Ze}C zxl_QD-F|Uu2SFt)ERMI!d}=3@RsJjFjGOlexnP^%7uY3aDf{gbx_Pk&4@B~om$K#r z1O=JiN(_JBGW%8fQyU5(`XNcwiFMS}U1%}cI5<2&01?UBMr5qT$jIn=vD@~UxjB-g z%@&{UJ4Z2Ke1<0D1WE$y<)Csj0p;inX3-6-Fkt)&)+tKnjMTZ~$Bq#cgis6s6sXFv zu^@~yRFCVxR!fiHPHnF)qD6{uqQ%G@VFSPmf7Q;hHI% z2E{8hdm9}LIPvlR==gCEIw!2gZ^0>& z@bO~^&WFHIMtXV`T8ODBVo{-gr@ZYkVEsc9{a;u-N#|{rC;pWF2K*j^BiWA7Rl!c^ zkBj#FTVaY?h{%^D;h>RYooK)O07v}`5+$T7YEDj093E;&Ux)}e1me81c*JsVsx3bL zL*Ihh{u1kc?0P=s!A!?mV8dfPF(!{)s7!jR_F>yDDJpIwUKc1$M>TWb0D4fsUF8F& zI}CKVr0y!pG#l`PlLBU!&cx4qLXimoGoOP0A|VJYdo|Pakdg39BG&8{G*^^NykaP& ziJrayzj*cIt@UIFmeH=w0BKqcXY^Q3R!f3kacrIYushKDMB}vrRE~y^w>Lta{#;P- z5H+hRUQ-l#NEeAq%XBIZn@-WN>e^14ectjpEG{|r=r?~MVMkAySY~Mj*)LfMSPYi~ zsjTojp|ct08)7(-3zevC)V9kGzP7jt3BSL2IG&oG-=O)tl9CVO;gwU3d0Dk_>Vi`? zVT44>pNijk8hs?i@7Rr%6aXIzWK+J2_HIv~g12UZsUfmr&R4_Qb*L$AVkoU`yzZJ| z?OfJUiCLpp5zd1`J^DcpAhVUvU%xIz;<=h@n^9p5rk!^<`@c@wKLCXTp|)0y<^jzf zp$NK~#-BeEVF@B>8Ahe^@Ce|o4uNsGJ~P@uG3~E$1($P{%_2i3)r!mAFi+}7$H0IT zOaidZ-1^5_1LsEqFwV7S>>H`6-(!;~h(A<3)0JfbkB^6R+z~C3P6n%X<^_0^HV zw!C2QXAw7)!2;3i&OU4XK8V2=92DZ7TtZV))N|L8Eb0+AZmlE=~Z(ytOHUC zbGy{DEomL_St-qvJIuAzufj0X2AL6kmdsOaE2rpvMPj|wbar9cM`?mmoGzOLU8(yQNwRMjJ zTae{hL%XEJ#7ORH1v7NZP_(~8e~|ECEQTt?fMWoZ|`o);nFa`FSQh)Bc z{-Va|MWMTrj`hxe$)4gg|1qKubZ*=z0P%h|#t>RD+6xf`_bsqKys7*W!}oujAO*v0 zK#8y>sEs(yg|)S>Q0n_uhtGYFx^jeoOx8<&r@`yN*_G9o3zQ?!W;;O*-s}b(Y<{m_ z^WMBX5){HtsOqrR5kOAFtYwc_@?n%L!iIHF3U@>0W%3nbkwjs-I;qf^AUNvQbj1Q=iV^ljin| z0hS%#h@^_;V76c223MmE=gi%Fjo{=n!+}Itfd_;4f%08Z%|`#Tb#-;kG0!y#Osmlv($S{iNq9GnPS z7#Y8!rKqJ$N_ewM-vr#~FVd0z=5Z#-(~}0ZI(wo*kYJ~I_)%&ja$*(-n}_`na@Tcr zzu+X9i5LRwfeg>A;~N-QtVt_bvZ@vUv;~a|Pgh1kB`KwP>~QIoyg6lJ2^gUQ{Zh*3 zX!R*7S)Iw6!$*&5Z6{8JfICkAZ z;8Dsu;$~e>#|oDvu<&~br|x@%{lkN--N--aXQ0y+AX?*FZxh~@bPE)qd2z(JkzFAw zstt_;3S1IU9y~aN*IT!7BMVe&@D+6Q^i-#>U*C)VY13|jm&D7*wYsH$2AuM9Fh9qU zY+EgVQ4iOl%7v21k7yl}r^RKZ3D7H9- z0A43ScOs>AfZ@9}*;{Qo(z+X#ApoSlmMngS50`GBaM~^}PA|aZHa*FI2fr9wa32gI zQf3I?9A&U6%@fKV(RIAN=?E21$YA(hyL5r6s;?a1L}IAdSJgku&A9jv&t^g?1%l0xoW{}FMy zZMVO^Jmdz^r1SUhF!Z(;RX*Nwy{W4k0pP6(Y7cX9X-`k_N4&Xo$e;$tX@L=LUjFb5lX4-*BWwQym^$wE{M=6HM zuJ313`0=|n?6+}7zq-(}yu=ZcgH|13tR^StbsQLPjVz_Ou~GGBhDms4PHJVv&@(s{ zl~DrU8%&Rn%Rj&(UQ1PB9_i5*T?JY1s}xsG80Dfbr}|4Y@rQBHzoISP2IhT@EY8{c zU8nAcRV?bUkK{}T5rY-%wW)HI(KM)p7mba_gj2xR#=voO_~5~VDF*Q%q(VYMUcx1Z z6@pUP9am|E{<90L=S46^TX*aTNBwOx%lnrgTo((@?hv%fyU*@N7ei2og-A|Q6c*;M zcLob=0$cvrg7?w{*9f-j@(k!F8P-lVA1~@%@4ThYJ?tRuvdVcxq;cH)2gbB$W(BkiZ5oj z#(n#6mU;0SbZfK|x8V^XhcP`X>(}q!raz9eoL!f0KE#OL2}@W%cU4yQ^xq~(P-@f= zRt*d||F%ZS!S9Xl809QT>MMtA+_+IMa8pf5$%m3KD$un3VX?$jsG@Siz(5A!15PWy zre+&)7J7Jq$}27}C&D4{k?9^7_-cAk;;2Vo(f!@p@N7qN=z>9ACoq~8i%|u9B{J8` zLUcyA?MEzey!1FuL>pT8$HV#btF!YgOa`l{HdXypG~^|Jo<756=exIWA7qhmZ!dDCgEZ_Y$!w?CroL5L z4MaACm;!i0@Irm}Ozz(={P}YyOg+#LbboxHhP?)kcjCAv{)7AX?|$r3bV{dt|Ned0 zhYv6QHW>Ex_4QAy`H~jn=xi1!~f@)p( z+(dtJhrStd3Gc;{{3-vc#-=yPl_~G;L(oA`2<$UeQF$#RtIh;>Ch`{yT?H_Gz&Sxa zQ%h#boZ5WDOE%em#y4tSqVfa@+xQ_1=}+_W@kxYM6MY&P;)BnXb?evP`PeZMn0aMJ zPc4}3zPY(J@*q)?Y@z$x{`sk6J5{Sp*lGS&%T*Xbm=jV@A|eETo9!orF&vCHKx>ht zDKuGiZf;oBn5=;@uFmQrC zfAdByRO6M0hXN?Ka|PL621R>cwa^xYm3p;QH+j0ITy(0ydw@7jQ{ zz^@Xafp>Mhh}9#|G(kZ@h=%B=;o)o`dr4q)bse;z`wL3)d;~l)yYBEO?jwqpD%Vm^ zS63~=Vq{L}pakL;z%IgZdrWL>?HQVoJ6{2T(R>O>p?fp3J9bguV%OcB3=CB%ddfr~ zY0I@^iY}ABnDcBQM-D{>S$<)@ZMhq{yPv%tjPuQVYVjE+-mjiN&xdW{YK}E8B2OpS z&W8vpeSMK5al9&SAqm$DR*EdmXhC}@1l>HK7;f%jxohJUZ01fH$?L zh2a|nU*0}f-%?ig6o3T>Vy%GrV1$UZrRBoVNHZDXaEg$=aKTl8(u249-Q67}B_s2! zVgPFmg~0TyXWuz|xMOhBm6SZep(P}G5;Tp0Y;z-{w{RZGTPN(0qE}mCOB~T@CHM}krL~t z)Hgw@10rR1l!-K71e}9o5IJELTpN!Z9Z}2$(8+oj&q?gqxw8~6%?vWg2|^QB_UOta z?Qbu(Ol*b=>^R^)g>aQYdSCQ0tM5mh?G;_}1WTBSz@puc8`jG+M&aQlRVtF`yUY0=9wW0i?mxfPjD? zo4-`oW_HMp@qqR+s$`-^QkhIY|AFdpye=~2+okbx;&)%qxG=J?m*`lpR}pdv!to++CdmLbd?bY6&L7 zaTET-GPlnwD*nbRVjK7I;@;}WtrfpRD2p^vhY6HPnC`DG? zuXR9WE!mApzAz6nL9T~m;w_A|AQ7B_m}_PZ9_S}?3<#q@G^H;vJwde5DnNZZ3n`wD zgm*SozeJv6{09Z- zl-2kFoHUlhhuwhxwuu)sHWHT^FHg<017;Q$zQo{d=-qkVOgkT=&mHes9t?I#+xg5qLv z&yf!uR}>Yu(9wmS2$=@i9iE-N3!*9@{Rg@~u#=?741sm<1`QJtKR?;`2`3H;>Ab!t z^?x1$V5kbrl-z;zrk*Eh2jWHtEF`dpk2BPVwVI4$!Iu?4B&miV;fd^*;2oc ziE+o_8^!9w&iC!@8OL>-H?x9YXj9KJ1cG!zd2$4#A@okG-L(2F%bX$nN$#LfjTdH& zgRJ`xVW8{#y}Xf>$3=)KfdN!wQ`XLpNl96^Ic_Q|D?^sJbwHO6KJHQw0EAd)lX=q) zw|0N6<}5h>6Lns=C4QkM9bcF$CTo zwx(P{`Ps8)Px9SbHw1}0j$M5Q$B>gD-WUB>*v~cjjqTGoUp@bNR37K40%MR_9Z-*c zNHDMsR>FJ2zAKSWX zSMyPkBO${ukJ;M!a|g`#ZH2m>dn%l9ya%`yThy(kUVU@FEKnkyyjr6j$6nU z7;+JAr(ka(qffAg0zSfGat=<2nztiLX^gJ(nt%_0=44_CJ*5%$88DoihbPQtdYA}p zXB?@j^Prxj?(!i$q5l)uo&%2!qsGF;u&N!%5i|zuEgNB&f`^nzQ?pIgFlEOS8(153 zV{w_*NW1tIxy1u6s`(i zj5Rioh1oTZ@}pA*x2V0HqD=FEA94%ku8{4dPb|c8!g4`6W%xY=OndVQ!GYGpzj@Pd zY~D?XD{vxGp-CnE21*aQf7rT~=%IOuF3qr@EIB=XjA12Z?TR<0cqkz2T8rr-b(GCc z=;XI+qpsjNPr!1CHmmp?jc^!114+wZoN5?1>q*jK>ctC3cn_w3h!kzGbBmr~>D|N2 zuIaDQ2@c&Sc1if2GpZ35CUMWF9b19Y;0I!SSByy^Y$-_p6FciMu-Ew zkY;-=&7R*7B#4`bOr7E^hLPo!S+wb{nrp}bi&tTVxAsSWtYu}IXLx&F+qE$d1 zw4UmJ&zYN>E6r&$n^jg>Ne1T71#X2R1Ew229N;9-FOjd;ty}jN{v9%v>cO{izGUM% zFqxpmswKU=r z!SI7Pmh=KvWU?{7zrz$nkR?zs;gy+$ZMQ5}g83kRo&bJBDniMlhj4|jsKsK+X;>1? z)4irtkEwxr7&xCnw;}2^VKnXJ)~dB(Zwv(8P6C7J?583#T|+~LOj`#gZq`Ep-N|>4 z0YH*4)x=X`V>3@BsCoA6O0wHXIlnML`3Za&Mw_W<_LRf?MtXIpoX()z+S*&`=}W-I z&45gmgyk1H74(hcV2M_)TJ?T&IywIakqHUUQ58YjlwgL~)!jV|^cJ*wy1%{2Xd*+< zddWw3@ZQ}<`ZI8 zN|Yq%(+Y+=sDVtbqCFNgA8c>VFd>N(!t-&MaIoghgG&l5{=pbxzyxstaY4h_35OeT z-7QQ6FaL&c|7zprvx*Ax@`d1mYE^Q@Cu%~(H{y5Q(R$=&?H5Y|L-83q)sSP8HkVb^ z)D(a;_lwEeeaF9E03=*kzQ!geYb^N1SAl^21&W7|@8GP-urdbn3W&A}a)v?J+BC4A zs*PbX2%df*H4M**}Ua>QR3m5kj#a} z6m9M$zhi`SZ@n@!g|Pu*O@$wsWPdUuMh58-yFdxDtaWtOCTK#);v;Y7L+yA5d&rOQ z%RR4kf-**Q9@BR!p#=QphP7WzQH_`9F+?O>3Kj{3Oc$vlV{|}FOLp61REq=*`ao1T zr>3?G+A^7(LIl$?Ft~(+pVP1=E-K7&sneq^-5 zA6u8$FJp|Bj2eL1V0SAd(?#%LK|77{{UiSCYehvjx+Wr*gER-+e+sw^ukL5W3=H8J zx3hv!kci_RtqStqFXROz_$rL+5m^iRF5DJHfC+8t+o{7i_ZcHQFnUM`OE7JFwz74^AwuE)gl-hYsBBddldm;CBmuUbzcJ2r~qSNfG=OZeR~`kc?i8U_wBFS zSZ9u_?gC~zX*s$VrNRwu5O#DoY}X9@25-{UwT_7}q7;vgkDmn<4J~L43M6K{pS^d# z+m&rSwE@zLc|jw|-UaSoW5kCHXIFl*A=+j|{p!0-6&4R8Yf z{CZ0w5Z2iJLPEPxY_W`DP(R@%Sh>Ft8-SL5-?ndhw{A_IQRq+MVccr}(1(S>&$_TO z`1#B=Z$XRUd<-xE>-09J9Km^a1Ha7!7p=4nvMWp=1t7Dou1LJ>g1rH7XW74hCG;8> zH@CCUU@;d=#GQkg_4V~S#q(lLF3ER)M>Cz@B(5JlApPQUVQ`$UyToq_( zyX92>c8zSyhmgzei(=4^u;q)m(4ngW{eXkNKO<@n2KL(C+5zy7&f3DquoYI8PZ$ro zYiE2(O3D#4QkWgwgoq>)!POz+mxQh-ZvfXpdTUIOz#_T}JaGX)!7<=$&d@smUF}GTGnm0L z9%&UO_FxP|`0*R=!RG8Ym}ZD|@9)10tJp8lc>o8ev7l)8ZeT1J7LfiVTZbyYnT;-* z6ngQQHH`H3XJX&{{(b8{=mg@@#PPya0?krL9OX+z!N(aO=j6Gbo<>DXSf7YHRO+wNFMDBLH)JaZ!;hdfs$(`XhvcU7Rx; zhou>EP!&G^1$>9{9K#0G=zzeCorj#?HY3oOUbI_!kvSqHgF4;8uP$ ze@%bzAV$3?5x_gY0lYBg41VtnQ7-WY{jE8IRyH;YD0t~vU$6570HHwo_FhNvfaDXW zna641%iumO0on{VWhfw9T!8TbCcR$Fh;*7i9rH>z>Dm0Vp+OclvGe$NsJuvoXPOOC zbirAkS62^&1jNq9=8OY4a8FrY{w&f|7&@OiD`4C=N!)gGrvBVoPtMmHmK#Wg#>8A| z=B&8@>#D^FF1-jvlK+k=J4_j0Y>wj=p~cx@hZqQESB8w1!rj%FsI?aThig-cryDpp za8$gFoLpR<#CqWG?~k3Iq!qF$gMtwOIF7kY37mZD)Ni0FDBJez(HM}8-eJE6?6tSE z^IEf^##f-YN}(-5-c{FW0t6xJk4op|DE1Td@t&Gsp}$B4{K)#l+mI(&dhR3dqx(%oo8(PG~Lg~|g7 zr6043{8Sh(=CPeI`k0#f0enFfq_oK7WX0y;6Cy1B`MGOKkiA zH)dt9=x2C@NwR_((~cLgTv|A#k!xGp#erk~_`?U*Ow+!V)YQ~?{nL<=kw#^em3vy# zFs^}5wb*`09A#EkR@MO_j9V7=v$3tk1QyH)XV7z@(;w@vJ#y>zZH(Nn!PnI^QNrIw zM!JF=55bU2Rdw|lG?&Dbm3km(pHftG^gi6M@{%i^_0gL=F(;^XooLaD&_{3^wVwva zr-ptogQ3Di2sy0VUuzvxrNdW<{|TQX0iG;OJxDckax?6gyLxpUU_0h0_M_sERWUU! zLU-3NZcOEnHsJtH3X@2Xn^Opr(I)q`rwwA@4UkyfCX435?BS8^#-1X0sUt# zS|jxLZcPVEbR-~(u~F@yrw>U_XE1EdJ_7*?j^lLw_92f{w2p#l5fST92Ah`R8IB^@ z(E6Ugc=7C+Gb@nuaAyVw#QM+rX_zmhvwa;=ru(fq%Yp_S#A6(a$*C#A)q(Gz!hl`W zi5Ju8ws1{{Q%1%qxFYxyMN>pzcAgIU{Ob(#5kOxtBsS6L3lD(7Fa-%X9Z* zj&N>?dw$8&e1Dn!&gC(MXhqWo9xGmn&nH|Ek`#5k!qNht-UxSv5wo}_vuOAJ5*RiYRaz&+Xzek9PVyGa4%;#_ zGcCtD*8o*kBfJEfo*rJ}D>k@zaRbH+h%Q5JQvwlI1PYUO$Bwn9PGv}Vvf+rq)b0=% zxVxsNW)w{I+vMbJFz*m!H$w0!x^XSA5zu1Jp-7`R+|bs30^bd0=eH7}_ZhP&nPNOI zC8Y(AoHWy*#R0f>$!sJB1rlHy1cwpVTIo=*Wi7_^N$$n--_X%o@2!0Re%q zxHuUgO!_n%=Euv6Blb1mpJ0s%h5bhXld6lL`FOxO!0q!6-sa2)ivoD%XB1f|Jn8BN zQ)Eo=02dc2r=8}H3*I6YpQBPwVKUNEXb*5!6-b~+G>oWds;7WNk;f%a?G?ld0?{eHCqqy37<`S`k1r_sn!bfKwD1+iSLX#3C@2*V~|j8PNvanY|a++qXQdmGnT0MV~#?^Xk9Vx`=C&8CrM?txSj zfnYX;C(HAJ9aZQd!Zw0bI&E!j9js<-0RZkbP@3#huj4i0TX6jtgFA$)-oCzq{CoJYHEsb+1aZ0*mxK-#X!mqx8^)Whx_#BCmN_|YjK&95iT2nUwr*;WD!T) zCI`Oy5pe)xLdD3~_zV`XYU^c*iIdPs(W5w`>jfF4);xSn@d08*6Ll1L|j z11W(4aG<3035-61L~uu{Mqb`cjJ-rK>+kRP6i^nj`EwM*GoEl;w4-m8Ra9Kit=B&0 zFNH9Iqy#A91dajnGjL?i4!Bp0kOZ`YHLqNm(olZAg%&lst+X3>W+|Zi&%x+7`D6nw zZ-(T8k4VN+IWEquX{A?;=_6ow3;^#_ZS8izi-lXonkdLIY8hJrj#8X7+x0h){(3LE zFp5Yt4+HZmjQT>XTSvx+!C9(oI9ejvkyo5yJw5C}Y-Sj*W6mDs_%$JM?HUtagSh@M zK1C`usLnH*n#>@E(37<4sE8%R#B9R*gCr0_&MF=nsfnZm&t!yX!_2I`Q5LEIMfS*T zU*LxGpTSzLpukZ@T{kv6vJ3Nz9Tak5hu8d{+}VjrQEv2Bq~t=X<}e5%1RV&&X~!iy zcu_l1M^1t=2cq6ADfxL?JK#P2ybB%*?}U zP5oyKK!u6|m;d`#J}@qy{kXN9{)|})9XN0xzqoh}n)tJ)svf3kQiO$t6T#k~JZ{*H zgF6*7%8#$0ODcGX_BXJU-@dGz$@Mik4t z9(0alxfnKngbS$P%yPxguB1R>bwGuM7M@Yt9I!Js7&-Y9ZjxGFn4mFePG5m)aa1Fl z0bPJ2UIwtTc&JmdxUG#De#!z!mgFL~;q43z>nI={+9VT?T&*^l9q&?2#g8%xbF#3k zqL@hxfQhRc2uC3VAg#vDKD0J}9+ZChk`MXL1KL6I`3_vGLQJ-&Wo4_@=2Re}U?F^n z0oOWz@0^XGgB>yH2w~-hmewkGKhV%a-@OUb7AoH%C`P#5!xg6{L3~s?9G%Wd+_0CN zlERHU83dk|pHO9IJ{3r^|1k#UmylOio`$KS)BL>*$@cjUE6&b?vb;p;1?9|wEtFSQ zW~2Ou$sBWu`zWRp-O`6SI1Yk16@Z9|eyti9Is%fH>7PNB{ni1R@o3J%-n~16b!5YK zz3}yAqo7$UQIF>q6cfYQHU;f}7}xe71|fXywira(=RxzBX*+woCC@&XY7@h0!bhX4 z6qtX5?t=nk;fBW*XJms|xAHXX;K?Yms$j}i?8R5&Jb>jqcAj{G2DKLo6|z29Bmgt0 z>|iuKDCi5edvcE$F}fbIWh?Y(?@~b8{|(>xo2M zE2^n2$i>wwf@l+?=t7}mUQk8NT9;zP%ef2>OcmCguC6QhWRVq&Q9 zh5Z+^S#UcN*CBTH=eUw9UHyiO1S_5WPNash3!q2OQzAk_&Nj!@xB`-!jkE?XEzKLc zxw{{LOttbf;Lh(2&j3N5(b>DYpekLet*zyOLJrE>>$xy8Do#RAuA-}adlqA9r8!GxWZ9Py}?CKTMTz}RNpM7raE^T!*}RIs=>ssr}PaBc!Owu<>TYl zlwuJgGB%Ao%Y!*!s>)TeI>uE`irTsx@pAiCyoHQWjZ$|9eJ&3YwE<^MgE72rD^G)I zCx?CxlT>#w*FV|)jcLF2ktL9F@+fu|-@uD}Pq^{vBomMn8)X~rtN|NH{ooI-UI|CB zmWAa#N1t&&XlVeYKvDZ#l^**|67CFVP*o37wlOm=pNXG>qjQ>dDd?o}BZ|=qiWw~( z-8t}(2SS#EwT_QXw=`s$9Wm>zdI&^PG_)bq__%1EnB?DCpEgX^a-y3WykpUzbPIxQ zKe}WPg~UVmU|~#>=&6F7SBH^D)F`I`wdCaGAK?IE_DAYV9h_iBNP7f$!wt3yW1Sl@ z)J*PgpuBze?jagJ0@QImECh?>^X83dhBT;ha87@M*as4F70C`5&))=3ds{e|C;DH0HfkY{|I+S4xEiiJ%FT6qQZ7t-jH*NV% z$-wkmo4=Q#VLyx73~G4)l(MR7W^3YI*WX=T4=ce$Ylux?{gf=}>#lqTfKQcKKxHae<^* zk1R<_8Hj~ry#G&YXCBsb-p2c%Y-O!%DJt0+yD+v=S(0p7BN=N-3$jb0jU>AfO(jc7 znvkL>M3zZ=RMJA0Q1*(J^SaIRoO7=0{Bh17XRhbE9%HC}-|y#hfA0Ieee}m8Nd$m- zp0BU5xAkrkuy6>vVUa4LVIu3wo&JBA!!vL2u-goa09z#+zT6PRAU2l6=Y=F8Qt8|G z=>ewoxZk(J1XlwJh`7Q?KCRxB`|ayjvF*X_31xwEM;Af+&xEX}pLlp7lB2Y*(>UY> z3`9k*U-z6fnl!9d@Epj`)wM@&azk*^MGIq+C$KQSVB%NyS%#gnrNP>XW$+JV-Fv1oJArU5*~v4tG`%}k{ra+2s(4dgD2mtG_L*LU{EC*jKxcrS~waQP~eBj1buPQ zjl3nUksa$>|F`rJbG#U>V=)T_JK4V2g0Mq$v56A>`C@&k8;&)lz|_d@r@xmMXemDO zxwoW7&Mz{VHHs(WfhGfio)Rn(Zb>7WotJuj*XK^1GR1@<)3fcccI>m@XHLLEa12QR zb)wIN^D+*-eyyna1ar2N4d!0G4vrK3%3!Z*qsh-(Yv zC}=%L(gJ*<#+1(k7Obqp%Ji(m>6)4v%j#1ncF`A9j-No;VYM{<_qJyJ@boZpgJ0YXDk0aa-=bhwj^AO z+Ltr~Nb}tHr}iivcn_&F9SRhY``=_{M3ts38~D8Gj~_oqfQbv7_2Gjr49ZfP`6}#@ zJpIfv9b4>%odB{UE2{v<*D&_@+0F$^VC@-`tVs7H6OFUA?fXrj(=2N*nhb$Mc+JtT zI#AOD>ZiqKL8k+D1-}r`0J$iUN?HLYl(o03s{z8{J zw-SbfQPCK1b$F$cqI2iYLN^f~L~h;Eh%am~RUMtyfV}*{m|J7_5vySH6Qxbj=a~R3 zjO-mn=B-dLJH)!y%LZRlS#E{EKt(z}m_J@&(fjv%Y3KiVdH(==U_2riBO{~zM~`|U zkWwh$oP-98-38P$GgrDVda-K#+cdK29VQ14dnhybTExLuPaeXhskN z#8u+EBZv%1+V#8M2}~t20hSjgq$U;4QT%sLq+^wpo)b(Eg||)Aq4#IWCum$0p&Y+D zcd$c(;r`X3c@8N`Jx}Ew$4JFz7b6;0LmimpJA8hRJr^V~;>}2qDkE$4M#zX7vn

LaGAEpAw@_@XNmc{Lafg8*?hYmWC%eAW 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 literal 0 HcmV?d00001 diff --git a/test/branching.dot b/test/branching.dot new file mode 100644 index 000000000..bd5209db3 --- /dev/null +++ b/test/branching.dot @@ -0,0 +1,16 @@ +digraph Branch { + graph [goal="Implement and validate a feature"] + rankdir=LR + node [shape=box, timeout="900s"] + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + plan [label="Plan", prompt="Plan the implementation for: $goal"] + implement [label="Implement", prompt="Implement the plan", goal_gate=true] + validate [label="Validate", prompt="Run tests"] + gate [shape=diamond, label="Tests passing?"] + + start -> plan -> implement -> validate -> gate + gate -> exit [label="Yes", condition="outcome=success"] + gate -> implement [label="No", condition="outcome!=success"] +} diff --git a/test/conditions.dot b/test/conditions.dot new file mode 100644 index 000000000..e814c5b53 --- /dev/null +++ b/test/conditions.dot @@ -0,0 +1,16 @@ +digraph Conditions { + graph [goal="Test condition evaluation with OR and parentheses"] + + start [shape=Mdiamond] + exit [shape=Msquare] + + decide [label="Decide", shape=diamond] + path_a [label="Path A"] + path_b [label="Path B"] + + start -> decide + decide -> path_a [condition="outcome=success && context.mode=fast"] + decide -> path_b [condition="outcome=success || context.fallback=true"] + path_a -> exit + path_b -> exit +} diff --git a/test/invalid.dot b/test/invalid.dot new file mode 100644 index 000000000..210e1edbd --- /dev/null +++ b/test/invalid.dot @@ -0,0 +1,6 @@ +digraph Invalid { + /* Missing start node */ + exit [shape=Msquare] + orphan [label="Orphan node"] + exit -> orphan +} diff --git a/test/parallel.dot b/test/parallel.dot new file mode 100644 index 000000000..30da4a3ff --- /dev/null +++ b/test/parallel.dot @@ -0,0 +1,19 @@ +digraph Parallel { + graph [goal="Test parallel and fan-in execution"] + + start [shape=Mdiamond] + exit [shape=Msquare] + + fork [label="Fork Work", shape=component, join_policy="wait_all", error_policy="continue"] + branch1 [label="Branch 1"] + branch2 [label="Branch 2"] + merge [label="Merge Results", shape=tripleoctagon] + review [label="Review"] + + start -> fork + fork -> branch1 + fork -> branch2 + branch1 -> merge + branch2 -> merge + merge -> review -> exit +} diff --git a/test/simple.dot b/test/simple.dot new file mode 100644 index 000000000..edcf5b257 --- /dev/null +++ b/test/simple.dot @@ -0,0 +1,12 @@ +digraph Simple { + graph [goal="Run tests and report results"] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + run_tests [label="Run Tests", prompt="Run the test suite and report results"] + report [label="Report", prompt="Summarize the test results"] + + start -> run_tests -> report -> exit +} diff --git a/test/styled.dot b/test/styled.dot new file mode 100644 index 000000000..815fe8001 --- /dev/null +++ b/test/styled.dot @@ -0,0 +1,19 @@ +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; } + " + ] + + start [shape=Mdiamond] + exit [shape=Msquare] + + plan [label="Plan", class="planning"] + implement [label="Implement", class="code"] + critical_review [label="Critical Review", class="code"] + + start -> plan -> implement -> critical_review -> exit +}