fabro/run.json
Fabro 1868453b0d init run
⚒️ Generated with [Fabro](https://fabro.sh)
2026-04-15 11:52:32 -04:00

495 lines
No EOL
38 KiB
JSON

{
"run_id": "01KP8XFY02RXHCR69H9FQ02X64",
"settings": {
"_version": 1,
"project": {
"directory": "."
},
"workflow": {
"graph": "workflow.fabro"
},
"run": {
"goal": "# Canonical `Blocked` Run Status Plan\n\n## Summary\n\n- Make `Blocked` a first-class shared run status across the durable projection, server, OpenAPI, generated TypeScript client, web UI, and CLI.\n- Keep `Paused` separate. `Paused` remains operator intent; `Blocked` means the run cannot proceed until an external condition is resolved.\n- This is a full status-unification pass: align the shared contract on `submitted`, `queued`, `starting`, `running`, `blocked`, `paused`, `removing`, `completed`, `failed`, and `cancelled`; remove `dead` from the canonical serialized lifecycle.\n- No alerting/email in this pass. `BlockedReason` is introduced now so notification work can key off a stable domain contract later.\n\n## Key Changes\n\n- Canonical status contract: update [docs/api-reference/fabro-api.yaml](/Users/bhelmkamp/p/fabro-sh/fabro/docs/api-reference/fabro-api.yaml), [lib/crates/fabro-types/src/status.rs](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/status.rs), and the generated models under `lib/packages/fabro-api-client/src/models/`.\n- Public/internal type changes:\n - Add `Queued`, `Blocked`, `Completed`, and `Cancelled` to the shared Rust `RunStatus`.\n - Rename shared/internal `Succeeded` usages to `Completed`.\n - Add nullable `blocked_reason` with a new `BlockedReason` enum; initial value set is `human_input_required`.\n - Remove `Dead` from OpenAPI and generated API/client status enums. Callers that currently fall back to `Dead` must instead treat status as missing/unknown locally.\n - Add `blocked` to the `RunStatus` and `InternalRunStatus` enums in `fabro-api.yaml`.\n- Projection and summary behavior: update [lib/crates/fabro-store/src/run_state.rs](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-store/src/run_state.rs), `lib/crates/fabro-store/src/types.rs`, and `lib/crates/fabro-store/src/slate/mod.rs`.\n - Persist `Queued` as a real durable state by appending/projecting a `run.queued` transition when a run is start-requested and enqueued.\n - Project `run.failed` with `reason=cancelled` to canonical `Cancelled`.\n - Set canonical `Blocked` on `interview.started` with `blocked_reason=human_input_required`.\n - Clear `blocked_reason` and return to `Running` on `interview.completed`, `interview.timeout`, or `interview.interrupted` when no pending interviews remain.\n - Keep `Paused` driven only by pause/unpause control events; interview events must never produce `Paused`.\n - Update transition helpers so `Blocked` is non-terminal and `Completed`/`Failed`/`Cancelled` are terminal.\n- Server/live read model: update [lib/crates/fabro-server/src/server.rs](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-server/src/server.rs) and `lib/crates/fabro-server/src/demo/mod.rs`.\n - Remove the ad-hoc API remap layer; server responses should expose the canonical shared status directly.\n - Extend run status payloads and durable summaries to include `blocked_reason` alongside `status_reason` and `pending_control`.\n - Extend `update_live_run_from_event()` so `InterviewStarted` drives `Blocked`, and interview resolution (`InterviewCompleted`/`InterviewTimeout`/`InterviewInterrupted`) returns live runs to `Running` when no pending interviews remain.\n - Keep `/runs/{id}/questions` and answer submission unchanged; those endpoints remain the detailed question surface behind a blocked run.\n- Board/UI model:\n - Change board columns to `working`, `blocked`, `review`, `merge`.\n - Map `Running` and `Paused` to `working`; map `Blocked` to `blocked`; map `Completed` to `merge`; keep `Submitted`, `Queued`, `Starting`, `Failed`, and `Cancelled` off-board.\n - Keep paused runs in the working lane with no extra indicator in this pass.\n - Update web mappings in `apps/fabro-web/app/{data/runs.ts,routes/run-detail.tsx,routes/runs.tsx}` so `blocked` is a real lifecycle/board value and `waiting` is removed.\n - Because this pass does not add a new `run.blocked` event family, update `STATUS_EVENTS` in `apps/fabro-web/app/routes/runs.tsx` to include `interview.started`, `interview.completed`, `interview.timeout`, and `interview.interrupted` as status-affecting events.\n- CLI consumers: update `lib/crates/fabro-cli/src/{commands/run/wait.rs,commands/runs/list.rs,server_runs.rs}`.\n - Replace `Succeeded`/`Dead` handling with `Completed` plus explicit missing-status handling.\n - Add display/color handling for `Blocked`, `Queued`, and `Cancelled`.\n\n## Test Plan\n\n- `lib/crates/fabro-store/src/run_state.rs`:\n - `interview.started` sets `status=Blocked` and `blocked_reason=HumanInputRequired`.\n - interview completion/timeout/interruption returns the run to `Running` when no pending interviews remain.\n - pause/unpause still yields `Paused`/`Running` and never routes through `Blocked`.\n - cancelled failures project to `Cancelled`.\n - queued state round-trips through projection serialization.\n- `lib/crates/fabro-store/src/slate/mod.rs` and `lib/crates/fabro-server/src/server.rs`:\n - durable summaries and `/runs/{id}` responses expose unified statuses plus `blocked_reason`.\n - no serialized API/store status is `dead`.\n - live managed runs enter `Blocked` while a pending interview exists.\n - board response emits a `blocked` column, places blocked runs there with question text, and keeps paused runs in `working`.\n- `apps/fabro-web/app/data/runs.test.ts` and a new `apps/fabro-web/app/routes/runs.test.tsx`:\n - summary mapping accepts `blocked`, `paused`, `completed`, and `cancelled`.\n - blocked runs render in the blocked lane with the existing answer-question affordance.\n - paused runs stay in the working lane.\n - no UI code depends on `waiting`.\n- CLI tests in `lib/crates/fabro-cli/src/commands/run/wait.rs` and `lib/crates/fabro-cli/src/commands/runs/list.rs`:\n - `Completed` is the success exit state.\n - `Blocked`, `Queued`, and `Cancelled` render correctly.\n - missing status no longer masquerades as `Dead`.\n - `Succeeded` is no longer accepted or displayed; all success paths use `Completed`.\n\n## Assumptions\n\n- `BlockedReason` starts with one value only: `human_input_required`.\n- Notification behavior is intentionally deferred; this plan only makes blocked state canonical and queryable.\n- `RunListItem.question` stays optional and unchanged in shape; `Blocked` plus `question` is sufficient for current UI behavior.\n- `Paused` remains visible in the working board column for now; the paused-specific visual indicator is a separate follow-up.\n",
"model": {
"provider": "anthropic",
"name": "claude-sonnet-4-6"
},
"prepare": {
"timeout": "5m"
},
"execution": {
"mode": "normal",
"approval": "prompt",
"retros": true
},
"sandbox": {
"provider": "daytona",
"preserve": true,
"devcontainer": false,
"local": {
"worktree_mode": "clean"
},
"daytona": {
"auto_stop_interval": 30,
"labels": {
"repo": "fabro-sh/fabro"
},
"snapshot": {
"name": "fabro-v7",
"cpu": 8,
"memory": "16GB",
"disk": "20GB",
"dockerfile": "FROM ubuntu:24.04\n\nRUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates build-essential pkg-config libssl-dev unzip python3 && rm -rf /var/lib/apt/lists/*\n\n# GitHub CLI\nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y --no-install-recommends gh && rm -rf /var/lib/apt/lists/*\n\n# Rust\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN cargo install cargo-nextest --locked\nENV CARGO_INCREMENTAL=0\n\n# Bun\nRUN curl -fsSL https://bun.sh/install | bash\nENV PATH=\"/root/.bun/bin:${PATH}\"\n\nWORKDIR /root\n"
}
}
},
"hooks": [
{
"id": "cargo-fmt",
"name": "cargo-fmt",
"event": "post_tool_use",
"matcher": "write_file|edit_file|apply_patch",
"blocking": true,
"script": "cargo fmt"
}
],
"pull_request": {
"enabled": true,
"draft": false
}
},
"cli": {
"target": {
"type": "http",
"url": "http://127.0.0.1:32276",
"tls": null
},
"exec": {
"prevent_idle_sleep": false
},
"output": {
"format": "text",
"verbosity": "normal"
},
"updates": {
"check": true
}
},
"server": {
"listen": {
"type": "tcp",
"address": "127.0.0.1:32276",
"tls": null
},
"api": {
"url": "http://127.0.0.1:32276/api/v1"
},
"web": {
"enabled": true,
"url": "http://127.0.0.1:32276"
},
"auth": {
"methods": [
"dev-token"
]
},
"storage": {
"root": "/Users/bhelmkamp/.fabro/storage"
},
"artifacts": {
"provider": "local",
"prefix": ""
},
"slatedb": {
"provider": "local",
"prefix": "",
"flush_interval": "1ms",
"disk_cache": false
},
"scheduler": {
"max_concurrent_runs": 5
},
"integrations": {
"github": {
"strategy": "token"
}
}
},
"features": {
"session_sandboxes": false
}
},
"graph": {
"name": "ImplementPlan",
"nodes": {
"fixup": {
"id": "fixup",
"attrs": {
"model": {
"String": "claude-opus-4-6"
},
"provider": {
"String": "anthropic"
},
"prompt": {
"String": "The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures."
},
"max_visits": {
"Integer": 3
},
"label": {
"String": "Fixup"
}
}
},
"implement": {
"id": "implement",
"attrs": {
"label": {
"String": "Implement"
},
"provider": {
"String": "anthropic"
},
"model": {
"String": "claude-opus-4-6"
},
"prompt": {
"String": "Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."
}
}
},
"fmt": {
"id": "fmt",
"attrs": {
"shape": {
"String": "parallelogram"
},
"max_retries": {
"Integer": 0
},
"label": {
"String": "Format"
},
"model": {
"String": "claude-opus-4-6"
},
"script": {
"String": "cargo fmt --all 2>&1"
},
"provider": {
"String": "anthropic"
}
}
},
"preflight_compile": {
"id": "preflight_compile",
"attrs": {
"provider": {
"String": "anthropic"
},
"shape": {
"String": "parallelogram"
},
"script": {
"String": "cargo check -q --workspace 2>&1"
},
"max_retries": {
"Integer": 0
},
"model": {
"String": "claude-opus-4-6"
},
"label": {
"String": "Preflight Compile"
}
}
},
"toolchain": {
"id": "toolchain",
"attrs": {
"label": {
"String": "Toolchain"
},
"shape": {
"String": "parallelogram"
},
"max_retries": {
"Integer": 0
},
"model": {
"String": "claude-opus-4-6"
},
"script": {
"String": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1"
},
"provider": {
"String": "anthropic"
}
}
},
"start": {
"id": "start",
"attrs": {
"model": {
"String": "claude-opus-4-6"
},
"label": {
"String": "Start"
},
"provider": {
"String": "anthropic"
},
"shape": {
"String": "Mdiamond"
}
}
},
"verify": {
"id": "verify",
"attrs": {
"provider": {
"String": "anthropic"
},
"goal_gate": {
"Boolean": true
},
"script": {
"String": "cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1"
},
"label": {
"String": "Verify"
},
"model": {
"String": "claude-opus-4-6"
},
"retry_target": {
"String": "fixup"
},
"shape": {
"String": "parallelogram"
}
}
},
"fix_lints": {
"id": "fix_lints",
"attrs": {
"prompt": {
"String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings."
},
"provider": {
"String": "anthropic"
},
"label": {
"String": "Fix Lints"
},
"model": {
"String": "claude-opus-4-6"
},
"max_visits": {
"Integer": 3
}
}
},
"exit": {
"id": "exit",
"attrs": {
"shape": {
"String": "Msquare"
},
"provider": {
"String": "anthropic"
},
"model": {
"String": "claude-opus-4-6"
},
"label": {
"String": "Exit"
}
}
},
"preflight_lint": {
"id": "preflight_lint",
"attrs": {
"provider": {
"String": "anthropic"
},
"label": {
"String": "Preflight Lint"
},
"max_retries": {
"Integer": 0
},
"model": {
"String": "claude-opus-4-6"
},
"shape": {
"String": "parallelogram"
},
"script": {
"String": "cargo clippy -q --workspace -- -D warnings 2>&1"
}
}
},
"simplify_opus": {
"id": "simplify_opus",
"attrs": {
"provider": {
"String": "anthropic"
},
"prompt": {
"String": "---\nname: code-review-simplify\ndescription: |\n Guidelines for writing and reviewing elegant, maintainable code in statically typed languages (TypeScript, Rust). Use when: (1) reviewing code for simplicity and clarity, (2) designing types to make invalid states unrepresentable, (3) evaluating whether abstractions or design patterns are justified, (4) naming variables/functions/classes, (5) deciding on encapsulation boundaries, (6) evaluating code comments, or (7) asked to simplify or improve code design.\n---\n\n# Code Design and Review for Simplicity\n\nThe job is not \"How can I make this work?\" but \"How *should* this work?\" The challenge is writing code that makes the task look easy.\n\n> \"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.\" —Antoine de Saint-Exupéry\n\nComplexity is the enemy—not because complex problems don't exist, but because unnecessary complexity obscures solutions. KISS, DRY, YAGNI are necessary but not sufficient. True simplicity requires:\n\n1. **Start simple and add complexity only when proven necessary.** The burden of proof is on complexity.\n2. **Understand the problem fully before solving it.** You cannot simplify what you do not understand.\n3. **Make the code explain itself.** If you need extensive comments, the code should be clearer.\n4. **Prefer boring code.** Clever code is often complex code.\n\n## Naming\n\nNaming is the most fundamental tool for communicating intent. Developers spend 75% of their time understanding code—clear names dramatically reduce cognitive load.\n\n**Principles:**\n- **Purpose-driven.** What it represents should be obvious from its name alone.\n- **Domain-specific.** Names reflect the business domain, not implementation details.\n- **Functions are verbs, variables are nouns.**\n- **Length follows scope.** Short-lived variables can have shorter names; widely-used entities need descriptive names.\n- **Consistent.** Pick one word for one concept. Don't mix 'fetch', 'retrieve', and 'get'.\n- **No abbreviations.** Prefer 'category' over 'cat'.\n\n**When naming is hard:** Difficulty naming usually signals a design problem—the function is doing too much or the concept is unclear. Write a plain-language comment explaining what the code does, then condense it into a name. If you cannot condense it, restructure the code.\n\n## Comments\n\nCode should speak for itself. Comments are a last resort. Remove low-value comments—they add noise and rot over time.\n\n**When comments are appropriate:**\n- **WHY, not WHAT.** Explain reasoning behind non-obvious decisions.\n- **Surprising behavior.** When something seems wrong but isn't.\n- **External references.** A URL to docs or bug report explaining a workaround—sparingly.\n\n**Remove these:**\n- Comments explaining what code does (fix the names instead)\n- Commented-out code (version control exists)\n- Stale TODOs that will never be addressed\n\nIf you need a comment to make code understandable, first try renaming or simplifying. The best comment is the one you didn't need to write.\n\n## Type System\n\nThe type system is the most powerful tool for ensuring correctness. The central principle: **make invalid states unrepresentable.**\n\nEvery type defines *representable* states. Business logic defines *valid* states. The gap between them is where bugs live. Close the gap by designing types where only valid data can be constructed—invalid combinations fail at compile time.\n\n**Example:** A user profile that can be guest or authenticated. Naive: boolean flags and optional fields allowing \"authenticated but no user ID.\" Better: discriminated unions (TypeScript) or enums (Rust) where Guest and Authenticated are distinct types. The compiler enforces validity—there is no gap.\n\n**Practical type design:**\n- **Avoid primitive obsession.** A `UserId` should not be interchangeable with a `ProductId`, even if both are strings.\n- **Use union types to model states.** Each state is a distinct type with appropriate data.\n- **Validate at boundaries, trust internally.** Parse data when it enters your system, then work with known-valid types.\n- **Use private constructors with factory functions.** No way to create an invalid instance.\n- **Types are documentation.** A well-designed type signature explains business rules better than comments.\n\n## Design Patterns and Indirection\n\nDesign patterns are tools, not rules. They solve specific problems—apply them when those problems exist, not prophylactically.\n\nEvery layer of indirection has a cost: code becomes harder to trace, debug, and understand. An interface with only one implementation is usually wrong abstraction.\n\n**Signs of over-abstraction:**\n- Wrapper classes that add no logic, only forward calls\n- Interfaces created \"for future flexibility\" with single implementations\n- Factory patterns for objects that could be constructed directly\n- Multiple inheritance layers that obscure what code runs\n- Dependency injection for values that never change\n\n**Before applying a pattern, ask:**\n1. Will it enhance clarity or introduce unnecessary complexity?\n2. Does the benefit justify the overhead, or does it just add bloat?\n3. Can teammates understand it, or is it too obscure?\n\nA pattern is justified when you have multiple implementations *today*, the abstraction makes code easier to understand, and the team recognizes it. Simplicity often beats sophistication—refactoring later is acceptable.\n\n## Encapsulation\n\nObject-oriented design groups data with operations that act on it. This enforces invariants and makes invalid states unrepresentable.\n\n- **Hide internals aggressively.** Private fields maintain invariants, not secrecy.\n- **Operations maintain validity.** If an object can only be created valid and all operations maintain validity, invalid states are impossible.\n- **Avoid anemic domain models.** Data structures with getters/setters and separate service classes defeat encapsulation's purpose.\n- **Favor composition over inheritance.** Deep hierarchies create tight coupling.\n- **High cohesion.** Elements of a class should be closely related and focused on a single purpose.\n\n## Code Review Checklist\n\n1. **Can I understand this without comments?** If not, names are unclear.\n2. **Can I construct invalid data?** Runtime checks should be compile-time guarantees.\n3. **Is every abstraction earning its keep?** Single-implementation interfaces are suspect.\n4. **Is this the simplest solution?** Could it be achieved with less?\n5. **Is data encapsulated with behavior?** Service classes manipulating passive data structures are a smell.\n6. **Would a new team member understand this?** Fresh eyes are the ultimate test.\n\nSimple code is not easy code—it is code that has had complexity removed through careful thought. The goal is not to demonstrate cleverness but to make the solution so clear that the problem looks easy.\n"
},
"label": {
"String": "Simplify (Opus)"
},
"model": {
"String": "claude-opus-4-6"
}
}
},
"simplify_gpt": {
"id": "simplify_gpt",
"attrs": {
"model": {
"String": "gpt-5.4"
},
"provider": {
"String": "openai"
},
"label": {
"String": "Simplify (GPT-54)"
},
"prompt": {
"String": "---\nname: code-review-simplify\ndescription: |\n Guidelines for writing and reviewing elegant, maintainable code in statically typed languages (TypeScript, Rust). Use when: (1) reviewing code for simplicity and clarity, (2) designing types to make invalid states unrepresentable, (3) evaluating whether abstractions or design patterns are justified, (4) naming variables/functions/classes, (5) deciding on encapsulation boundaries, (6) evaluating code comments, or (7) asked to simplify or improve code design.\n---\n\n# Code Design and Review for Simplicity\n\nThe job is not \"How can I make this work?\" but \"How *should* this work?\" The challenge is writing code that makes the task look easy.\n\n> \"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.\" —Antoine de Saint-Exupéry\n\nComplexity is the enemy—not because complex problems don't exist, but because unnecessary complexity obscures solutions. KISS, DRY, YAGNI are necessary but not sufficient. True simplicity requires:\n\n1. **Start simple and add complexity only when proven necessary.** The burden of proof is on complexity.\n2. **Understand the problem fully before solving it.** You cannot simplify what you do not understand.\n3. **Make the code explain itself.** If you need extensive comments, the code should be clearer.\n4. **Prefer boring code.** Clever code is often complex code.\n\n## Naming\n\nNaming is the most fundamental tool for communicating intent. Developers spend 75% of their time understanding code—clear names dramatically reduce cognitive load.\n\n**Principles:**\n- **Purpose-driven.** What it represents should be obvious from its name alone.\n- **Domain-specific.** Names reflect the business domain, not implementation details.\n- **Functions are verbs, variables are nouns.**\n- **Length follows scope.** Short-lived variables can have shorter names; widely-used entities need descriptive names.\n- **Consistent.** Pick one word for one concept. Don't mix 'fetch', 'retrieve', and 'get'.\n- **No abbreviations.** Prefer 'category' over 'cat'.\n\n**When naming is hard:** Difficulty naming usually signals a design problem—the function is doing too much or the concept is unclear. Write a plain-language comment explaining what the code does, then condense it into a name. If you cannot condense it, restructure the code.\n\n## Comments\n\nCode should speak for itself. Comments are a last resort. Remove low-value comments—they add noise and rot over time.\n\n**When comments are appropriate:**\n- **WHY, not WHAT.** Explain reasoning behind non-obvious decisions.\n- **Surprising behavior.** When something seems wrong but isn't.\n- **External references.** A URL to docs or bug report explaining a workaround—sparingly.\n\n**Remove these:**\n- Comments explaining what code does (fix the names instead)\n- Commented-out code (version control exists)\n- Stale TODOs that will never be addressed\n\nIf you need a comment to make code understandable, first try renaming or simplifying. The best comment is the one you didn't need to write.\n\n## Type System\n\nThe type system is the most powerful tool for ensuring correctness. The central principle: **make invalid states unrepresentable.**\n\nEvery type defines *representable* states. Business logic defines *valid* states. The gap between them is where bugs live. Close the gap by designing types where only valid data can be constructed—invalid combinations fail at compile time.\n\n**Example:** A user profile that can be guest or authenticated. Naive: boolean flags and optional fields allowing \"authenticated but no user ID.\" Better: discriminated unions (TypeScript) or enums (Rust) where Guest and Authenticated are distinct types. The compiler enforces validity—there is no gap.\n\n**Practical type design:**\n- **Avoid primitive obsession.** A `UserId` should not be interchangeable with a `ProductId`, even if both are strings.\n- **Use union types to model states.** Each state is a distinct type with appropriate data.\n- **Validate at boundaries, trust internally.** Parse data when it enters your system, then work with known-valid types.\n- **Use private constructors with factory functions.** No way to create an invalid instance.\n- **Types are documentation.** A well-designed type signature explains business rules better than comments.\n\n## Design Patterns and Indirection\n\nDesign patterns are tools, not rules. They solve specific problems—apply them when those problems exist, not prophylactically.\n\nEvery layer of indirection has a cost: code becomes harder to trace, debug, and understand. An interface with only one implementation is usually wrong abstraction.\n\n**Signs of over-abstraction:**\n- Wrapper classes that add no logic, only forward calls\n- Interfaces created \"for future flexibility\" with single implementations\n- Factory patterns for objects that could be constructed directly\n- Multiple inheritance layers that obscure what code runs\n- Dependency injection for values that never change\n\n**Before applying a pattern, ask:**\n1. Will it enhance clarity or introduce unnecessary complexity?\n2. Does the benefit justify the overhead, or does it just add bloat?\n3. Can teammates understand it, or is it too obscure?\n\nA pattern is justified when you have multiple implementations *today*, the abstraction makes code easier to understand, and the team recognizes it. Simplicity often beats sophistication—refactoring later is acceptable.\n\n## Encapsulation\n\nObject-oriented design groups data with operations that act on it. This enforces invariants and makes invalid states unrepresentable.\n\n- **Hide internals aggressively.** Private fields maintain invariants, not secrecy.\n- **Operations maintain validity.** If an object can only be created valid and all operations maintain validity, invalid states are impossible.\n- **Avoid anemic domain models.** Data structures with getters/setters and separate service classes defeat encapsulation's purpose.\n- **Favor composition over inheritance.** Deep hierarchies create tight coupling.\n- **High cohesion.** Elements of a class should be closely related and focused on a single purpose.\n\n## Code Review Checklist\n\n1. **Can I understand this without comments?** If not, names are unclear.\n2. **Can I construct invalid data?** Runtime checks should be compile-time guarantees.\n3. **Is every abstraction earning its keep?** Single-implementation interfaces are suspect.\n4. **Is this the simplest solution?** Could it be achieved with less?\n5. **Is data encapsulated with behavior?** Service classes manipulating passive data structures are a smell.\n6. **Would a new team member understand this?** Fresh eyes are the ultimate test.\n\nSimple code is not easy code—it is code that has had complexity removed through careful thought. The goal is not to demonstrate cleverness but to make the solution so clear that the problem looks easy.\n"
}
}
}
},
"edges": [
{
"from": "start",
"to": "toolchain",
"attrs": {}
},
{
"from": "toolchain",
"to": "preflight_compile",
"attrs": {
"condition": {
"String": "outcome=success"
}
}
},
{
"from": "toolchain",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_compile",
"to": "preflight_lint",
"attrs": {
"condition": {
"String": "outcome=success"
}
}
},
{
"from": "preflight_compile",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_lint",
"to": "implement",
"attrs": {
"condition": {
"String": "outcome=success"
}
}
},
{
"from": "preflight_lint",
"to": "fix_lints",
"attrs": {}
},
{
"from": "fix_lints",
"to": "preflight_lint",
"attrs": {}
},
{
"from": "implement",
"to": "simplify_opus",
"attrs": {}
},
{
"from": "simplify_opus",
"to": "simplify_gpt",
"attrs": {}
},
{
"from": "simplify_gpt",
"to": "verify",
"attrs": {}
},
{
"from": "verify",
"to": "fmt",
"attrs": {
"condition": {
"String": "outcome=success"
}
}
},
{
"from": "verify",
"to": "fixup",
"attrs": {}
},
{
"from": "fixup",
"to": "verify",
"attrs": {}
},
{
"from": "fmt",
"to": "exit",
"attrs": {}
}
],
"attrs": {
"model_stylesheet": {
"String": "\n * { model: claude-opus-4-6; }\n "
},
"rankdir": {
"String": "LR"
},
"goal": {
"String": "# Canonical `Blocked` Run Status Plan\n\n## Summary\n\n- Make `Blocked` a first-class shared run status across the durable projection, server, OpenAPI, generated TypeScript client, web UI, and CLI.\n- Keep `Paused` separate. `Paused` remains operator intent; `Blocked` means the run cannot proceed until an external condition is resolved.\n- This is a full status-unification pass: align the shared contract on `submitted`, `queued`, `starting`, `running`, `blocked`, `paused`, `removing`, `completed`, `failed`, and `cancelled`; remove `dead` from the canonical serialized lifecycle.\n- No alerting/email in this pass. `BlockedReason` is introduced now so notification work can key off a stable domain contract later.\n\n## Key Changes\n\n- Canonical status contract: update [docs/api-reference/fabro-api.yaml](/Users/bhelmkamp/p/fabro-sh/fabro/docs/api-reference/fabro-api.yaml), [lib/crates/fabro-types/src/status.rs](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-types/src/status.rs), and the generated models under `lib/packages/fabro-api-client/src/models/`.\n- Public/internal type changes:\n - Add `Queued`, `Blocked`, `Completed`, and `Cancelled` to the shared Rust `RunStatus`.\n - Rename shared/internal `Succeeded` usages to `Completed`.\n - Add nullable `blocked_reason` with a new `BlockedReason` enum; initial value set is `human_input_required`.\n - Remove `Dead` from OpenAPI and generated API/client status enums. Callers that currently fall back to `Dead` must instead treat status as missing/unknown locally.\n - Add `blocked` to the `RunStatus` and `InternalRunStatus` enums in `fabro-api.yaml`.\n- Projection and summary behavior: update [lib/crates/fabro-store/src/run_state.rs](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-store/src/run_state.rs), `lib/crates/fabro-store/src/types.rs`, and `lib/crates/fabro-store/src/slate/mod.rs`.\n - Persist `Queued` as a real durable state by appending/projecting a `run.queued` transition when a run is start-requested and enqueued.\n - Project `run.failed` with `reason=cancelled` to canonical `Cancelled`.\n - Set canonical `Blocked` on `interview.started` with `blocked_reason=human_input_required`.\n - Clear `blocked_reason` and return to `Running` on `interview.completed`, `interview.timeout`, or `interview.interrupted` when no pending interviews remain.\n - Keep `Paused` driven only by pause/unpause control events; interview events must never produce `Paused`.\n - Update transition helpers so `Blocked` is non-terminal and `Completed`/`Failed`/`Cancelled` are terminal.\n- Server/live read model: update [lib/crates/fabro-server/src/server.rs](/Users/bhelmkamp/p/fabro-sh/fabro/lib/crates/fabro-server/src/server.rs) and `lib/crates/fabro-server/src/demo/mod.rs`.\n - Remove the ad-hoc API remap layer; server responses should expose the canonical shared status directly.\n - Extend run status payloads and durable summaries to include `blocked_reason` alongside `status_reason` and `pending_control`.\n - Extend `update_live_run_from_event()` so `InterviewStarted` drives `Blocked`, and interview resolution (`InterviewCompleted`/`InterviewTimeout`/`InterviewInterrupted`) returns live runs to `Running` when no pending interviews remain.\n - Keep `/runs/{id}/questions` and answer submission unchanged; those endpoints remain the detailed question surface behind a blocked run.\n- Board/UI model:\n - Change board columns to `working`, `blocked`, `review`, `merge`.\n - Map `Running` and `Paused` to `working`; map `Blocked` to `blocked`; map `Completed` to `merge`; keep `Submitted`, `Queued`, `Starting`, `Failed`, and `Cancelled` off-board.\n - Keep paused runs in the working lane with no extra indicator in this pass.\n - Update web mappings in `apps/fabro-web/app/{data/runs.ts,routes/run-detail.tsx,routes/runs.tsx}` so `blocked` is a real lifecycle/board value and `waiting` is removed.\n - Because this pass does not add a new `run.blocked` event family, update `STATUS_EVENTS` in `apps/fabro-web/app/routes/runs.tsx` to include `interview.started`, `interview.completed`, `interview.timeout`, and `interview.interrupted` as status-affecting events.\n- CLI consumers: update `lib/crates/fabro-cli/src/{commands/run/wait.rs,commands/runs/list.rs,server_runs.rs}`.\n - Replace `Succeeded`/`Dead` handling with `Completed` plus explicit missing-status handling.\n - Add display/color handling for `Blocked`, `Queued`, and `Cancelled`.\n\n## Test Plan\n\n- `lib/crates/fabro-store/src/run_state.rs`:\n - `interview.started` sets `status=Blocked` and `blocked_reason=HumanInputRequired`.\n - interview completion/timeout/interruption returns the run to `Running` when no pending interviews remain.\n - pause/unpause still yields `Paused`/`Running` and never routes through `Blocked`.\n - cancelled failures project to `Cancelled`.\n - queued state round-trips through projection serialization.\n- `lib/crates/fabro-store/src/slate/mod.rs` and `lib/crates/fabro-server/src/server.rs`:\n - durable summaries and `/runs/{id}` responses expose unified statuses plus `blocked_reason`.\n - no serialized API/store status is `dead`.\n - live managed runs enter `Blocked` while a pending interview exists.\n - board response emits a `blocked` column, places blocked runs there with question text, and keeps paused runs in `working`.\n- `apps/fabro-web/app/data/runs.test.ts` and a new `apps/fabro-web/app/routes/runs.test.tsx`:\n - summary mapping accepts `blocked`, `paused`, `completed`, and `cancelled`.\n - blocked runs render in the blocked lane with the existing answer-question affordance.\n - paused runs stay in the working lane.\n - no UI code depends on `waiting`.\n- CLI tests in `lib/crates/fabro-cli/src/commands/run/wait.rs` and `lib/crates/fabro-cli/src/commands/runs/list.rs`:\n - `Completed` is the success exit state.\n - `Blocked`, `Queued`, and `Cancelled` render correctly.\n - missing status no longer masquerades as `Dead`.\n - `Succeeded` is no longer accepted or displayed; all success paths use `Completed`.\n\n## Assumptions\n\n- `BlockedReason` starts with one value only: `human_input_required`.\n- Notification behavior is intentionally deferred; this plan only makes blocked state canonical and queryable.\n- `RunListItem.question` stays optional and unchanged in shape; `Blocked` plus `question` is sufficient for current UI behavior.\n- `Paused` remains visible in the working board column for now; the paused-specific visual indicator is a separate follow-up.\n"
}
}
},
"workflow_slug": "implement-plan",
"working_directory": "/Users/bhelmkamp/p/fabro-sh/fabro",
"host_repo_path": "/Users/bhelmkamp/p/fabro-sh/fabro",
"repo_origin_url": "https://github.com/fabro-sh/fabro",
"base_branch": "main",
"provenance": {
"server": {
"version": "0.176.2"
},
"client": {
"user_agent": "fabro-cli/0.176.2",
"name": "fabro-cli",
"version": "0.176.2"
},
"subject": {
"login": "dev",
"auth_method": "dev_token"
}
},
"manifest_blob": "65febf1192e817247dd67c24e4659e393f7a1a43c4ca541c205a3d8a92bc86b9",
"definition_blob": "3c4d65b2ff3a1167d8f0c61caafb22e12c40c6145087706a457e27cab798cb4b"
}