diff --git a/run.json b/run.json
index 2b6131a86..835434960 100644
--- a/run.json
+++ b/run.json
@@ -505,7 +505,7 @@
"kind": "running"
},
"status_updated_at": "2026-05-29T20:14:56.507237Z",
- "last_event_at": "2026-05-29T20:33:04.633166Z",
+ "last_event_at": "2026-05-29T20:42:53.602946Z",
"pending_control": null,
"checkpoints": [
{
@@ -789,9 +789,9 @@
}
},
{
- "seq": 0,
+ "seq": 382,
"checkpoint": {
- "timestamp": "2026-05-29T20:33:04.675267Z",
+ "timestamp": "2026-05-29T20:33:10.000081Z",
"current_node": "implement",
"completed_nodes": [
"start",
@@ -802,30 +802,177 @@
],
"node_retries": {},
"context_values": {
+ "last_response": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as th",
+ "outcome": "succeeded",
+ "response.implement": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as the only activation control.\n- Updated `enabled_api_trigger()` to only check for an enabled API trigger.\n- Added the no-compatibility TOML test rejecting legacy top-level `enabled`.\n- Updated server conflict detail to `automation has no enabled API trigger`.\n- Removed top-level `enabled` from server fixtures/tests and deleted the disabled-automation master-gate test.\n- Updated OpenAPI schemas and regenerated the TypeScript Axios client.\n- Removed the web UI master toggle and all create/replace payload usage.\n- Updated web run availability to derive from the API trigger only.\n- Used a subagent audit and a red/green cycle: the new legacy-TOML rejection test failed before implementation and passed after the model change.\n\nVerification run:\n- `cargo nextest run -p fabro-automation` ✅\n- `cargo build -p fabro-api` ✅\n- `cargo nextest run -p fabro-api --test automation_round_trip` ✅\n- `cargo nextest run -p fabro-server --features test-support 'api::automations'` ✅\n- `cd lib/packages/fabro-api-client && bun run generate` ✅\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `git diff --check` ✅\n\nReference scan:\n- No removed top-level `automation.enabled` / master-gate references remain.\n- Remaining `enabled` hits are trigger-level fields/usages or the intended `enabled_api_trigger()` call.\n\nEnvironment note:\n- Installed `default-jre-headless` in the container because OpenAPI Generator required `java` to regenerate the TypeScript client.",
+ "thread.start.current_node": "toolchain",
+ "internal.thread_id": "preflight_lint",
+ "internal.run_id": "01KSTNZTNTN8QP3QMKQZ6JCH3F",
+ "current_node": "implement",
+ "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
+ "internal.node_visit_count": 1,
+ "failure_signature": "",
+ "graph.goal": "# Remove Automation Master Enabled Gate Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Remove the top-level automation `enabled` field so trigger-level `enabled` is the only activation control.\n\n**Architecture:** Automations keep their existing file-backed TOML store and REST API, but the top-level master gate disappears from the Rust domain model, persisted TOML, OpenAPI schemas, generated clients, and web UI. API/manual run creation checks only for an enabled `api` trigger. No migration or legacy parser path is added because automations are brand new; TOML that still contains top-level `enabled` is obsolete input.\n\n**Tech Stack:** Rust, serde/TOML, Axum, OpenAPI/progenitor, TypeScript Axios client generation, React 19, SWR, Tailwind CSS.\n\n---\n\n## File Structure\n\n- Modify `lib/crates/fabro-automation/src/model.rs` for the core type and TOML shape.\n- Modify `lib/crates/fabro-automation/src/store.rs` for unit fixtures that create automation drafts/replacements.\n- Modify `lib/crates/fabro-server/src/server/handler/automations.rs` for API-trigger conflict wording.\n- Modify `lib/crates/fabro-server/tests/it/api/automations.rs` for HTTP fixtures and behavior tests.\n- Modify `docs/public/api-reference/fabro-api.yaml` and regenerate `lib/packages/fabro-api-client/src/**`.\n- Modify `lib/crates/fabro-api/tests/automation_round_trip.rs` for Rust/OpenAPI type parity.\n- Modify `apps/fabro-web/app/components/automation-form.tsx`, `apps/fabro-web/app/routes/automations-new.tsx`, `apps/fabro-web/app/routes/automations-edit.tsx`, `apps/fabro-web/app/routes/automation-detail.tsx`, and `apps/fabro-web/app/routes/automations.tsx` for UI state and trigger-derived run availability.\n\n## Task 1: Remove The Domain Master Gate\n\n**Files:**\n- Modify: `lib/crates/fabro-automation/src/model.rs`\n- Modify: `lib/crates/fabro-automation/src/store.rs`\n\n- [ ] Remove `pub enabled: bool` from `Automation`, `AutomationDraft`, `AutomationReplace`, and `PersistedAutomation`.\n- [ ] Remove `enabled` from every conversion between `AutomationDraft`, `AutomationReplace`, `PersistedAutomation`, and `Automation`.\n- [ ] Update `Automation::enabled_api_trigger()` to return an enabled API trigger without checking a top-level automation flag:\n\n```rust\n/// Returns the enabled API trigger if the automation has one.\n/// Returns `None` when the automation has no enabled API trigger.\n#[must_use]\npub fn enabled_api_trigger(&self) -> Option<&ApiTrigger> {\n self.triggers.iter().find_map(|trigger| match trigger {\n AutomationTrigger::Api(trigger) if trigger.enabled => Some(trigger),\n _ => None,\n })\n}\n```\n\n- [ ] Remove the now-unused `default_true()` helper if no other code in the file still uses it.\n- [ ] Update `persisted_toml_applies_defaults_and_canonicalizes_without_id_or_revision` so the fixture has no top-level `enabled = true`, does not assert `automation.enabled`, and asserts the canonical TOML has no top-level `enabled` line:\n\n```rust\nassert!(!top_level_lines(&toml).any(|line| line.starts_with(\"enabled = \")));\n```\n\n- [ ] Add a focused no-compatibility test in `lib/crates/fabro-automation/src/model.rs`:\n\n```rust\n#[test]\nfn persisted_toml_rejects_legacy_top_level_enabled() {\n let bytes = br#\"\nname = \"Legacy\"\nenabled = false\n\n[target]\nrepository = \"fabro-sh/fabro\"\nref = \"main\"\nworkflow = \"release\"\n\n[[triggers]]\ntype = \"api\"\nid = \"manual\"\nenabled = true\n\"#;\n\n let result = Automation::from_toml_bytes(AutomationId::new(\"legacy\").unwrap(), bytes);\n\n assert!(result.is_err());\n}\n```\n\n- [ ] Update `lib/crates/fabro-automation/src/store.rs` test helpers so `draft()` and `replacement()` no longer set top-level `enabled`.\n- [ ] Run:\n\n```bash\ncargo nextest run -p fabro-automation\n```\n\nExpected: all `fabro-automation` tests pass.\n\n## Task 2: Update Server Behavior And Tests\n\n**Files:**\n- Modify: `lib/crates/fabro-server/src/server/handler/automations.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/automations.rs`\n\n- [ ] Change the `create_automation_run` conflict detail from:\n\n```rust\n\"automation is disabled or has no enabled API trigger\"\n```\n\nto:\n\n```rust\n\"automation has no enabled API trigger\"\n```\n\nKeep the existing code `\"automation_api_trigger_disabled\"` for compatibility with current clients and tests.\n\n- [ ] Remove top-level `\"enabled\": true` from `automation_body()`.\n- [ ] Remove top-level `\"enabled\": false` from `replacement_body()`.\n- [ ] Delete `disabled_automation_run_endpoint_returns_conflict_code`; the master gate no longer exists.\n- [ ] Keep `disabled_api_trigger_run_endpoint_returns_conflict_code` and `missing_api_trigger_run_endpoint_returns_conflict_code` as the authoritative inactive-run tests.\n- [ ] Update any test that mutates `body[\"enabled\"]` or expects top-level enabled in automation JSON/TOML.\n- [ ] Run:\n\n```bash\ncargo nextest run -p fabro-server automations\n```\n\nExpected: automation integration tests pass.\n\n## Task 3: Update OpenAPI And Generated API Types\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/tests/automation_round_trip.rs`\n- Regenerate: `lib/packages/fabro-api-client/src/**`\n\n- [ ] In the `Automation` schema, remove top-level `enabled` from `required` and `properties`.\n- [ ] In `CreateAutomationRequest`, remove top-level `enabled` from `properties`.\n- [ ] In `ReplaceAutomationRequest`, remove top-level `enabled` from `required` and `properties`.\n- [ ] Keep `enabled` on `AutomationApiTrigger` and `AutomationScheduleTrigger`.\n- [ ] Update the `POST /api/v1/automations/{id}/runs` `409` description from:\n\n```yaml\ndescription: Automation is disabled or has no enabled API trigger\n```\n\nto:\n\n```yaml\ndescription: Automation has no enabled API trigger\n```\n\n- [ ] Update `lib/crates/fabro-api/tests/automation_round_trip.rs` so the `Automation`, `CreateAutomationRequest`, and `ReplaceAutomationRequest` JSON fixtures no longer include top-level `\"enabled\"`.\n- [ ] Run:\n\n```bash\ncargo build -p fabro-api\n```\n\nExpected: progenitor type generation succeeds.\n\n- [ ] Run:\n\n```bash\ncargo nextest run -p fabro-api automation_round_trip\n```\n\nExpected: automation type identity and JSON parity tests pass.\n\n- [ ] Regenerate the TypeScript client:\n\n```bash\ncd lib/packages/fabro-api-client && bun run generate\n```\n\nExpected: generated model files remove top-level `enabled` from `Automation`, `CreateAutomationRequest`, and `ReplaceAutomationRequest`.\n\n## Task 4: Remove The Web UI Master Toggle\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/automation-form.tsx`\n- Modify: `apps/fabro-web/app/routes/automations-new.tsx`\n- Modify: `apps/fabro-web/app/routes/automations-edit.tsx`\n- Modify: `apps/fabro-web/app/routes/automation-detail.tsx`\n- Modify: `apps/fabro-web/app/routes/automations.tsx`\n\n- [ ] Remove `enabled` from `AutomationFormValues` and `EMPTY_AUTOMATION_FORM`.\n- [ ] Remove `enabled: automation.enabled` from `automationToFormValues`.\n- [ ] Delete the `Row title=\"Enabled\"` block from `AutomationFormFields`.\n- [ ] Remove `enabled: values.enabled` from the create payload in `automations-new.tsx`.\n- [ ] Remove `enabled: values.enabled` from the replace payload in `automations-edit.tsx`.\n- [ ] In `isFormValid`, remove the requirement that at least one trigger is enabled. The final return should only require non-empty ID, name, repository, ref, and workflow:\n\n```ts\nreturn (\n values.id.trim() !== \"\" &&\n values.name.trim() !== \"\" &&\n values.repository.trim() !== \"\" &&\n values.ref.trim() !== \"\" &&\n values.workflow.trim() !== \"\"\n);\n```\n\n- [ ] In `automation-detail.tsx`, change run availability to:\n\n```ts\nconst canRun = apiTrigger?.enabled === true;\n```\n\n- [ ] In `automation-detail.tsx`, remove `StatusChip`, remove its use, and simplify the Run button `title` so only a missing/disabled API trigger explains the disabled state:\n\n```ts\ntitle={!apiTrigger?.enabled ? \"Enable the API trigger to run it\" : undefined}\n```\n\n- [ ] In `automations.tsx`, extend `AutomationRow` with `apiEnabled: boolean`, set it from the enabled API trigger in `mapAutomations`, and pass `disabled={deleting || !automation.apiEnabled || (runningId !== null && runningId !== automation.id)}` to the run button path.\n- [ ] In `AutomationCard`, make the run button title reflect trigger-disabled state:\n\n```tsx\ntitle={\n running\n ? \"Starting run...\"\n : automation.apiEnabled\n ? \"Run automation\"\n : \"Enable the API trigger to run it\"\n}\n```\n\nUse exactly this title text for the disabled/run states; do not change visible button copy.\n\n- [ ] Run:\n\n```bash\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: TypeScript passes with no `automation.enabled` references.\n\n## Task 5: Final Verification\n\n**Files:**\n- No additional source edits expected.\n\n- [ ] Run the focused backend checks:\n\n```bash\ncargo nextest run -p fabro-automation\ncargo nextest run -p fabro-api automation_round_trip\ncargo nextest run -p fabro-server automations\n```\n\nExpected: all focused Rust checks pass.\n\n- [ ] Run the focused frontend checks:\n\n```bash\ncd lib/packages/fabro-api-client && bun run typecheck\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: generated client and web app typecheck.\n\n- [ ] Search for leftover master-gate references:\n\n```bash\nrg -n \"automation\\\\.enabled|enabled_api_trigger\\\\(\\\\).*automation|Automation is disabled|automation is disabled|\\\\\\\"enabled\\\\\\\": true\" \\\n lib/crates/fabro-automation \\\n lib/crates/fabro-server/tests/it/api/automations.rs \\\n lib/crates/fabro-server/src/server/handler/automations.rs \\\n lib/crates/fabro-api/tests/automation_round_trip.rs \\\n apps/fabro-web/app/components/automation-form.tsx \\\n apps/fabro-web/app/routes/automations-new.tsx \\\n apps/fabro-web/app/routes/automations-edit.tsx \\\n apps/fabro-web/app/routes/automation-detail.tsx \\\n apps/fabro-web/app/routes/automations.tsx\n```\n\nExpected: no hits for the removed top-level automation enabled gate. Hits for trigger-level `enabled` are acceptable when they are clearly on `AutomationTrigger`, `ApiTrigger`, or `ScheduleTrigger`.\n\n- [ ] Optional formatting check:\n\n```bash\ncargo +nightly-2026-04-14 fmt --check --all\n```\n\nExpected: Rust formatting passes. If it fails on touched Rust files, run `cargo +nightly-2026-04-14 fmt --all` and re-run the focused Rust checks.\n\n## Explicit Assumptions\n\n- No migration or compatibility parser is added for top-level automation `enabled`.\n- Existing automation TOML files that still contain top-level `enabled` are obsolete and may fail startup until manually edited.\n- Trigger-level `enabled` remains public API and persisted TOML.\n- API/manual run creation remains controlled by the enabled `api` trigger only.\n- Schedule execution, cron semantics, and schedule-trigger preservation are outside this cleanup.\n",
+ "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
+ "last_stage": "implement",
+ "thread.toolchain.current_node": "preflight_compile",
+ "internal.work_dir": "/home/daytona/workspace/fabro",
+ "thread.preflight_lint.current_node": "implement",
+ "thread.preflight_compile.current_node": "preflight_lint",
+ "internal.retry_count.toolchain": 0,
+ "internal.retry_count.implement": 0,
+ "graph.rankdir": "LR",
+ "internal.fidelity": "compact",
+ "internal.retry_count.preflight_compile": 0,
+ "failure_class": "",
+ "internal.retry_count.preflight_lint": 0,
+ "internal.retry_count.start": 0
+ },
+ "node_outcomes": {
+ "preflight_lint": {
+ "status": "succeeded",
+ "context_updates": {
+ "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
+ },
+ "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
+ "usage": null,
+ "timing": {
+ "wall_time_ms": 0,
+ "inference_time_ms": 0,
+ "tool_time_ms": 145318,
+ "active_time_ms": 145318
+ }
+ },
+ "implement": {
+ "status": "succeeded",
+ "context_updates": {
+ "last_response": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as th",
+ "last_stage": "implement",
+ "response.implement": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as the only activation control.\n- Updated `enabled_api_trigger()` to only check for an enabled API trigger.\n- Added the no-compatibility TOML test rejecting legacy top-level `enabled`.\n- Updated server conflict detail to `automation has no enabled API trigger`.\n- Removed top-level `enabled` from server fixtures/tests and deleted the disabled-automation master-gate test.\n- Updated OpenAPI schemas and regenerated the TypeScript Axios client.\n- Removed the web UI master toggle and all create/replace payload usage.\n- Updated web run availability to derive from the API trigger only.\n- Used a subagent audit and a red/green cycle: the new legacy-TOML rejection test failed before implementation and passed after the model change.\n\nVerification run:\n- `cargo nextest run -p fabro-automation` ✅\n- `cargo build -p fabro-api` ✅\n- `cargo nextest run -p fabro-api --test automation_round_trip` ✅\n- `cargo nextest run -p fabro-server --features test-support 'api::automations'` ✅\n- `cd lib/packages/fabro-api-client && bun run generate` ✅\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `git diff --check` ✅\n\nReference scan:\n- No removed top-level `automation.enabled` / master-gate references remain.\n- Remaining `enabled` hits are trigger-level fields/usages or the intended `enabled_api_trigger()` call.\n\nEnvironment note:\n- Installed `default-jre-headless` in the container because OpenAPI Generator required `java` to regenerate the TypeScript client."
+ },
+ "notes": "Stage completed: implement",
+ "usage": {
+ "input": {
+ "usage": {
+ "model": {
+ "provider": "openai",
+ "model_id": "gpt-5.5"
+ },
+ "tokens": {
+ "input_tokens": 1280340,
+ "output_tokens": 11277,
+ "reasoning_tokens": 5723,
+ "cache_read_tokens": 4670976,
+ "cache_write_tokens": 0
+ }
+ },
+ "facts": {
+ "algorithm": "openai"
+ }
+ },
+ "total_usd_micros": 9247188
+ },
+ "timing": {
+ "wall_time_ms": 0,
+ "inference_time_ms": 463878,
+ "tool_time_ms": 315538,
+ "active_time_ms": 779416
+ }
+ },
+ "start": {
+ "status": "succeeded",
+ "usage": null
+ },
+ "preflight_compile": {
+ "status": "succeeded",
+ "context_updates": {
+ "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
+ },
+ "notes": "Script completed: cargo check -q --workspace 2>&1",
+ "usage": null,
+ "timing": {
+ "wall_time_ms": 0,
+ "inference_time_ms": 0,
+ "tool_time_ms": 135138,
+ "active_time_ms": 135138
+ }
+ },
+ "toolchain": {
+ "status": "succeeded",
+ "context_updates": {
+ "command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
+ },
+ "notes": "Script completed: 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",
+ "usage": null,
+ "timing": {
+ "wall_time_ms": 0,
+ "inference_time_ms": 0,
+ "tool_time_ms": 1604,
+ "active_time_ms": 1604
+ }
+ }
+ },
+ "next_node_id": "simplify_opus",
+ "git_commit_sha": "41744748ca3d379c783fafa2c22e97a4de156504",
+ "node_visits": {
+ "start": 1,
+ "implement": 1,
+ "toolchain": 1,
+ "preflight_lint": 1,
+ "preflight_compile": 1
+ }
+ },
+ "diff": {
+ "patch": "diff --git a/apps/fabro-web/app/components/automation-form.tsx b/apps/fabro-web/app/components/automation-form.tsx\nindex 5f79f4a78..5d623534c 100644\n--- a/apps/fabro-web/app/components/automation-form.tsx\n+++ b/apps/fabro-web/app/components/automation-form.tsx\n@@ -9,7 +9,6 @@ export interface AutomationFormValues {\n id: string;\n name: string;\n description: string;\n- enabled: boolean;\n repository: string;\n ref: string;\n workflow: string;\n@@ -22,7 +21,6 @@ export const EMPTY_AUTOMATION_FORM: AutomationFormValues = {\n id: \"\",\n name: \"\",\n description: \"\",\n- enabled: true,\n repository: \"\",\n ref: \"main\",\n workflow: \"\",\n@@ -45,7 +43,6 @@ export function automationToFormValues(automation: Automation): AutomationFormVa\n id: automation.id,\n name: automation.name,\n description: automation.description ?? \"\",\n- enabled: automation.enabled,\n repository: automation.target.repository,\n ref: automation.target.ref,\n workflow: automation.target.workflow,\n@@ -77,8 +74,7 @@ export function isFormValid(values: AutomationFormValues): boolean {\n values.name.trim() !== \"\" &&\n values.repository.trim() !== \"\" &&\n values.ref.trim() !== \"\" &&\n- values.workflow.trim() !== \"\" &&\n- (values.manualEnabled || values.scheduleEnabled)\n+ values.workflow.trim() !== \"\"\n );\n }\n \n@@ -187,13 +183,6 @@ export function AutomationFormFields({\n className={`${INPUT_CLASS} resize-y`}\n />\n \n- \n- patch({ enabled })}\n- label=\"Enable automation\"\n- />\n-
\n \n \n \ndiff --git a/apps/fabro-web/app/routes/automation-detail.tsx b/apps/fabro-web/app/routes/automation-detail.tsx\nindex d8c3021dc..57d2274f2 100644\n--- a/apps/fabro-web/app/routes/automation-detail.tsx\n+++ b/apps/fabro-web/app/routes/automation-detail.tsx\n@@ -92,7 +92,7 @@ function AutomationHeader({ automation }: { automation: Automation }) {\n \n const scheduleTrigger = automation.triggers.find((t) => t.type === \"schedule\");\n const apiTrigger = automation.triggers.find((t) => t.type === \"api\");\n- const canRun = apiTrigger?.enabled === true && automation.enabled;\n+ const canRun = apiTrigger?.enabled === true;\n \n async function onRun() {\n if (!canRun || running) return;\n@@ -137,7 +137,6 @@ function AutomationHeader({ automation }: { automation: Automation }) {\n {automation.id}\n \n \n-
\n
\n {automation.target.repository}\n · {automation.target.ref}\n@@ -165,13 +164,7 @@ function AutomationHeader({ automation }: { automation: Automation }) {\n type=\"button\"\n onClick={onRun}\n disabled={!canRun || running}\n- title={\n- !automation.enabled\n- ? \"Enable the automation to run it\"\n- : !apiTrigger?.enabled\n- ? \"Enable the API trigger to run it\"\n- : undefined\n- }\n+ title={!apiTrigger?.enabled ? \"Enable the API trigger to run it\" : undefined}\n className={PRIMARY_BUTTON_CLASS}\n >\n \n@@ -183,19 +176,6 @@ function AutomationHeader({ automation }: { automation: Automation }) {\n );\n }\n \n-function StatusChip({ enabled }: { enabled: boolean }) {\n- return (\n- \n- \n- \n- {enabled ? \"Enabled\" : \"Disabled\"}\n- \n- \n- );\n-}\n-\n function Chip({\n icon: Icon,\n children,\ndiff --git a/apps/fabro-web/app/routes/automations-edit.tsx b/apps/fabro-web/app/routes/automations-edit.tsx\nindex cb8e10324..e77a1e159 100644\n--- a/apps/fabro-web/app/routes/automations-edit.tsx\n+++ b/apps/fabro-web/app/routes/automations-edit.tsx\n@@ -85,7 +85,6 @@ function EditAutomationForm({ automation }: { automation: Automation }) {\n automationsApi.replaceAutomation(automation.id, automation.revision, {\n name: trimmedName,\n description: values.description.trim() || null,\n- enabled: values.enabled,\n target: {\n repository: values.repository.trim(),\n ref: values.ref.trim(),\ndiff --git a/apps/fabro-web/app/routes/automations-new.tsx b/apps/fabro-web/app/routes/automations-new.tsx\nindex 1ee221c35..02d56b11f 100644\n--- a/apps/fabro-web/app/routes/automations-new.tsx\n+++ b/apps/fabro-web/app/routes/automations-new.tsx\n@@ -47,7 +47,6 @@ export default function AutomationsNew() {\n id: values.id.trim(),\n name: trimmedName,\n description: values.description.trim() || null,\n- enabled: values.enabled,\n target: {\n repository: values.repository.trim(),\n ref: values.ref.trim(),\ndiff --git a/apps/fabro-web/app/routes/automations.tsx b/apps/fabro-web/app/routes/automations.tsx\nindex 7cb32b59f..f8dfb025f 100644\n--- a/apps/fabro-web/app/routes/automations.tsx\n+++ b/apps/fabro-web/app/routes/automations.tsx\n@@ -48,6 +48,7 @@ interface AutomationRow {\n workflow: string;\n repository: string;\n schedule?: string;\n+ apiEnabled: boolean;\n icon: ComponentType<{ className?: string }>;\n color: string;\n }\n@@ -81,6 +82,10 @@ function scheduleFor(automation: Automation): string | undefined {\n return schedule?.expression;\n }\n \n+function hasEnabledApiTrigger(automation: Automation): boolean {\n+ return automation.triggers.some((t) => t.type === \"api\" && t.enabled);\n+}\n+\n function mapAutomations(result: AutomationListResponse | undefined): AutomationRow[] {\n const automations = result?.data ?? [];\n return automations.map((a) => ({\n@@ -90,6 +95,7 @@ function mapAutomations(result: AutomationListResponse | undefined): AutomationR\n workflow: a.target.workflow,\n repository: a.target.repository,\n schedule: scheduleFor(a),\n+ apiEnabled: hasEnabledApiTrigger(a),\n icon: slugIconMap[a.target.workflow] ?? CodeBracketIcon,\n color: slugColorMap[a.target.workflow] ?? \"var(--color-teal-500)\",\n }));\n@@ -106,12 +112,14 @@ function PlayIcon({ className }: { className?: string }) {\n function AutomationCard({\n automation,\n disabled,\n+ menuDisabled,\n running,\n onRun,\n onDelete,\n }: {\n automation: AutomationRow;\n disabled: boolean;\n+ menuDisabled: boolean;\n running: boolean;\n onRun: () => void;\n onDelete: () => void;\n@@ -156,7 +164,13 @@ function AutomationCard({\n onClick={onRun}\n disabled={running || disabled}\n aria-label={running ? \"Starting run…\" : \"Run automation\"}\n- title={running ? \"Starting run…\" : \"Run automation\"}\n+ title={\n+ running\n+ ? \"Starting run...\"\n+ : automation.apiEnabled\n+ ? \"Run automation\"\n+ : \"Enable the API trigger to run it\"\n+ }\n className=\"flex size-8 shrink-0 items-center justify-center rounded-full border border-mint/20 text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-mint\"\n >\n {running ? (\n@@ -167,7 +181,7 @@ function AutomationCard({\n \n )}\n \n- \n+ \n \n );\n }\n@@ -242,7 +256,7 @@ export default function Automations() {\n const [runningId, setRunningId] = useState(null);\n \n async function runAutomation(automation: AutomationRow) {\n- if (runningId) return;\n+ if (runningId || !automation.apiEnabled) return;\n setRunningId(automation.id);\n try {\n const run = await apiData(() => automationsApi.createAutomationRun(automation.id));\n@@ -322,7 +336,12 @@ export default function Automations() {\n runAutomation(automation)}\n onDelete={() => setPendingDelete(automation)}\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 103ab4cad..b7c77775a 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -4206,7 +4206,7 @@ paths:\n schema:\n $ref: \"#/components/schemas/ErrorResponse\"\n \"409\":\n- description: Automation is disabled or has no enabled API trigger\n+ description: Automation has no enabled API trigger\n headers:\n x-request-id:\n $ref: \"#/components/headers/XRequestId\"\n@@ -5841,7 +5841,6 @@ components:\n - revision\n - name\n - description\n- - enabled\n - target\n - triggers\n properties:\n@@ -5860,9 +5859,6 @@ components:\n description:\n type: [\"string\", \"null\"]\n example: Keeps dependencies fresh.\n- enabled:\n- type: boolean\n- example: true\n target:\n $ref: \"#/components/schemas/AutomationTarget\"\n triggers:\n@@ -5970,9 +5966,6 @@ components:\n description:\n type: [\"string\", \"null\"]\n example: Keeps dependencies fresh.\n- enabled:\n- type: boolean\n- default: true\n target:\n $ref: \"#/components/schemas/AutomationTarget\"\n triggers:\n@@ -5986,7 +5979,6 @@ components:\n additionalProperties: false\n required:\n - name\n- - enabled\n - target\n - triggers\n properties:\n@@ -5996,8 +5988,6 @@ components:\n description:\n type: [\"string\", \"null\"]\n example: Keeps dependencies fresh.\n- enabled:\n- type: boolean\n target:\n $ref: \"#/components/schemas/AutomationTarget\"\n triggers:\ndiff --git a/lib/crates/fabro-api/tests/automation_round_trip.rs b/lib/crates/fabro-api/tests/automation_round_trip.rs\nindex 9984fbb39..db4a66d03 100644\n--- a/lib/crates/fabro-api/tests/automation_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/automation_round_trip.rs\n@@ -26,7 +26,6 @@ fn automation_response_round_trips_public_json_shape() {\n \"revision\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"name\": \"Nightly dependency update\",\n \"description\": null,\n- \"enabled\": true,\n \"target\": {\n \"repository\": \"fabro-sh/fabro\",\n \"ref\": \"main\",\n@@ -57,7 +56,6 @@ fn create_automation_request_round_trips_public_json_shape() {\n \"id\": \"nightly-deps\",\n \"name\": \"Nightly dependency update\",\n \"description\": \"Keep dependencies fresh\",\n- \"enabled\": true,\n \"target\": {\n \"repository\": \"fabro-sh/fabro\",\n \"ref\": \"main\",\n@@ -81,7 +79,6 @@ fn replace_automation_request_round_trips_public_json_shape() {\n let value = json!({\n \"name\": \"Nightly dependency update\",\n \"description\": \"Keep dependencies fresh\",\n- \"enabled\": true,\n \"target\": {\n \"repository\": \"fabro-sh/fabro\",\n \"ref\": \"main\",\ndiff --git a/lib/crates/fabro-automation/src/model.rs b/lib/crates/fabro-automation/src/model.rs\nindex 6e5cda0c1..90dc24eb7 100644\n--- a/lib/crates/fabro-automation/src/model.rs\n+++ b/lib/crates/fabro-automation/src/model.rs\n@@ -15,7 +15,6 @@ pub struct Automation {\n pub revision: AutomationRevision,\n pub name: String,\n pub description: Option,\n- pub enabled: bool,\n pub target: AutomationTarget,\n pub triggers: Vec,\n }\n@@ -54,7 +53,6 @@ impl Automation {\n PersistedAutomation {\n name: self.name.clone(),\n description: self.description.clone(),\n- enabled: self.enabled,\n target: self.target.clone(),\n triggers: self.triggers.clone(),\n }\n@@ -64,14 +62,10 @@ impl Automation {\n toml::to_string_pretty(&self.to_persisted()).map_err(AutomationStoreError::from)\n }\n \n- /// Returns the enabled API trigger if the automation itself is enabled and\n- /// has one. Returns `None` when the automation is disabled or has no\n- /// enabled API trigger.\n+ /// Returns the enabled API trigger if the automation has one.\n+ /// Returns `None` when the automation has no enabled API trigger.\n #[must_use]\n pub fn enabled_api_trigger(&self) -> Option<&ApiTrigger> {\n- if !self.enabled {\n- return None;\n- }\n self.triggers.iter().find_map(|trigger| match trigger {\n AutomationTrigger::Api(trigger) if trigger.enabled => Some(trigger),\n _ => None,\n@@ -98,7 +92,6 @@ impl Automation {\n revision,\n name: replace.name,\n description: replace.description,\n- enabled: replace.enabled,\n target: replace.target,\n triggers: replace.triggers,\n }\n@@ -161,8 +154,6 @@ pub struct AutomationDraft {\n pub name: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub description: Option,\n- #[serde(default = \"default_true\")]\n- pub enabled: bool,\n pub target: AutomationTarget,\n pub triggers: Vec,\n }\n@@ -172,7 +163,6 @@ impl From for (AutomationId, AutomationReplace) {\n (value.id, AutomationReplace {\n name: value.name,\n description: value.description,\n- enabled: value.enabled,\n target: value.target,\n triggers: value.triggers,\n })\n@@ -185,7 +175,6 @@ pub struct AutomationReplace {\n pub name: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub description: Option,\n- pub enabled: bool,\n pub target: AutomationTarget,\n pub triggers: Vec,\n }\n@@ -196,8 +185,6 @@ pub(crate) struct PersistedAutomation {\n name: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n description: Option,\n- #[serde(default = \"default_true\")]\n- enabled: bool,\n target: AutomationTarget,\n #[serde(default)]\n triggers: Vec,\n@@ -208,7 +195,6 @@ impl From for PersistedAutomation {\n Self {\n name: value.name,\n description: value.description,\n- enabled: value.enabled,\n target: value.target,\n triggers: value.triggers,\n }\n@@ -220,7 +206,6 @@ impl From for AutomationReplace {\n Self {\n name: value.name,\n description: value.description,\n- enabled: value.enabled,\n target: value.target,\n triggers: value.triggers,\n }\n@@ -390,10 +375,6 @@ fn validate_triggers(triggers: &[AutomationTrigger]) -> Result<(), AutomationVal\n Ok(())\n }\n \n-fn default_true() -> bool {\n- true\n-}\n-\n #[cfg(test)]\n mod tests {\n use crate::{\n@@ -450,30 +431,49 @@ expression = \"0 0 * * *\"\n Automation::from_toml_bytes(AutomationId::new(\"nightly\").unwrap(), bytes).unwrap();\n \n assert_eq!(automation.description, None);\n- assert!(automation.enabled);\n assert!(automation.triggers.iter().all(AutomationTrigger::enabled));\n \n let toml = automation.to_toml_string().unwrap();\n assert!(!top_level_lines(&toml).any(|line| line.starts_with(\"id = \")));\n assert!(!top_level_lines(&toml).any(|line| line.starts_with(\"revision = \")));\n- assert!(toml.contains(\"enabled = true\"));\n+ assert!(!top_level_lines(&toml).any(|line| line.starts_with(\"enabled = \")));\n assert!(toml.contains(\"type = \\\"api\\\"\"));\n }\n \n+ #[test]\n+ fn persisted_toml_rejects_legacy_top_level_enabled() {\n+ let bytes = br#\"\n+name = \"Legacy\"\n+enabled = false\n+\n+[target]\n+repository = \"fabro-sh/fabro\"\n+ref = \"main\"\n+workflow = \"release\"\n+\n+[[triggers]]\n+type = \"api\"\n+id = \"manual\"\n+enabled = true\n+\"#;\n+\n+ let result = Automation::from_toml_bytes(AutomationId::new(\"legacy\").unwrap(), bytes);\n+\n+ assert!(result.is_err());\n+ }\n+\n #[test]\n fn validation_rejects_invalid_inputs() {\n let cases = [\n AutomationReplace {\n name: \" \".to_string(),\n description: None,\n- enabled: true,\n target: target(),\n triggers: vec![api_trigger(\"manual\")],\n },\n AutomationReplace {\n name: \"Bad repo\".to_string(),\n description: None,\n- enabled: true,\n target: AutomationTarget {\n repository: \"not/github/slug\".to_string(),\n ref_selector: \"main\".to_string(),\n@@ -484,7 +484,6 @@ expression = \"0 0 * * *\"\n AutomationReplace {\n name: \"Bad ref\".to_string(),\n description: None,\n- enabled: true,\n target: AutomationTarget {\n repository: \"fabro-sh/fabro\".to_string(),\n ref_selector: \"main;rm\".to_string(),\n@@ -495,7 +494,6 @@ expression = \"0 0 * * *\"\n AutomationReplace {\n name: \"Bad workflow\".to_string(),\n description: None,\n- enabled: true,\n target: AutomationTarget {\n repository: \"fabro-sh/fabro\".to_string(),\n ref_selector: \"main\".to_string(),\n@@ -506,7 +504,6 @@ expression = \"0 0 * * *\"\n AutomationReplace {\n name: \"Duplicate trigger\".to_string(),\n description: None,\n- enabled: true,\n target: target(),\n triggers: vec![\n api_trigger(\"manual\"),\n@@ -516,21 +513,18 @@ expression = \"0 0 * * *\"\n AutomationReplace {\n name: \"Two API triggers\".to_string(),\n description: None,\n- enabled: true,\n target: target(),\n triggers: vec![api_trigger(\"one\"), api_trigger(\"two\")],\n },\n AutomationReplace {\n name: \"Six field cron\".to_string(),\n description: None,\n- enabled: true,\n target: target(),\n triggers: vec![schedule_trigger(\"nightly\", \"0 0 0 * * *\")],\n },\n AutomationReplace {\n name: \"Bad cron\".to_string(),\n description: None,\n- enabled: true,\n target: target(),\n triggers: vec![schedule_trigger(\"nightly\", \"99 0 * * *\")],\n },\ndiff --git a/lib/crates/fabro-automation/src/store.rs b/lib/crates/fabro-automation/src/store.rs\nindex 6c0541441..ccd6ea7a4 100644\n--- a/lib/crates/fabro-automation/src/store.rs\n+++ b/lib/crates/fabro-automation/src/store.rs\n@@ -290,7 +290,6 @@ mod tests {\n id: AutomationId::new(id).unwrap(),\n name: name.to_string(),\n description: None,\n- enabled: true,\n target: target(),\n triggers: vec![\n AutomationTrigger::Api(ApiTrigger {\n@@ -310,7 +309,6 @@ mod tests {\n AutomationReplace {\n name: name.to_string(),\n description: Some(\"updated\".to_string()),\n- enabled: false,\n target: target(),\n triggers: vec![AutomationTrigger::Api(ApiTrigger {\n id: AutomationTriggerId::new(\"manual\").unwrap(),\ndiff --git a/lib/crates/fabro-server/src/server/handler/automations.rs b/lib/crates/fabro-server/src/server/handler/automations.rs\nindex c29920d1d..70feedc7e 100644\n--- a/lib/crates/fabro-server/src/server/handler/automations.rs\n+++ b/lib/crates/fabro-server/src/server/handler/automations.rs\n@@ -133,7 +133,7 @@ async fn create_automation_run(\n let Some(api_trigger) = automation.enabled_api_trigger() else {\n return ApiError::with_code(\n StatusCode::CONFLICT,\n- \"automation is disabled or has no enabled API trigger\",\n+ \"automation has no enabled API trigger\",\n \"automation_api_trigger_disabled\",\n )\n .into_response();\ndiff --git a/lib/crates/fabro-server/tests/it/api/automations.rs b/lib/crates/fabro-server/tests/it/api/automations.rs\nindex f09f94f38..11ad21a28 100644\n--- a/lib/crates/fabro-server/tests/it/api/automations.rs\n+++ b/lib/crates/fabro-server/tests/it/api/automations.rs\n@@ -19,7 +19,6 @@ fn automation_body(id: &str, name: &str) -> Value {\n \"id\": id,\n \"name\": name,\n \"description\": \"Runs on a schedule.\",\n- \"enabled\": true,\n \"target\": {\n \"repository\": \"fabro-sh/fabro\",\n \"ref\": \"main\",\n@@ -45,7 +44,6 @@ fn replacement_body(name: &str) -> Value {\n json!({\n \"name\": name,\n \"description\": null,\n- \"enabled\": false,\n \"target\": {\n \"repository\": \"fabro-sh/fabro\",\n \"ref\": \"main\",\n@@ -289,6 +287,7 @@ async fn schedule_trigger_round_trips_through_create_list_get_and_toml() {\n assert_persisted_schedule_trigger(&persisted, \"0 3 * * *\", true);\n assert!(persisted.get(\"id\").is_none());\n assert!(persisted.get(\"revision\").is_none());\n+ assert!(persisted.get(\"enabled\").is_none());\n }\n \n #[tokio::test]\n@@ -692,21 +691,6 @@ async fn automations_routes_require_authenticated_user() {\n .await;\n }\n \n-#[tokio::test]\n-async fn disabled_automation_run_endpoint_returns_conflict_code() {\n- let (app, _temp_dir, _automation_dir) = automation_app_with_fake_materializer();\n- let mut body = automation_body(\"nightly\", \"Nightly\");\n- body[\"enabled\"] = json!(false);\n- create_automation_with_body(&app, &body).await;\n-\n- let error = create_automation_run(&app, \"nightly\", StatusCode::CONFLICT).await;\n-\n- assert_eq!(\n- error[\"errors\"][0][\"code\"],\n- \"automation_api_trigger_disabled\"\n- );\n-}\n-\n #[tokio::test]\n async fn missing_automation_run_endpoint_returns_not_found() {\n let (app, _temp_dir, _automation_dir) = automation_app_with_fake_materializer();\ndiff --git a/lib/packages/fabro-api-client/src/models/automation.ts b/lib/packages/fabro-api-client/src/models/automation.ts\nindex 575759e19..5c1c20bb1 100644\n--- a/lib/packages/fabro-api-client/src/models/automation.ts\n+++ b/lib/packages/fabro-api-client/src/models/automation.ts\n@@ -31,7 +31,6 @@ export interface Automation {\n 'revision': string;\n 'name': string;\n 'description': string | null;\n- 'enabled': boolean;\n 'target': AutomationTarget;\n 'triggers': Array;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/create-automation-request.ts b/lib/packages/fabro-api-client/src/models/create-automation-request.ts\nindex 5f21f795b..ce99d34db 100644\n--- a/lib/packages/fabro-api-client/src/models/create-automation-request.ts\n+++ b/lib/packages/fabro-api-client/src/models/create-automation-request.ts\n@@ -27,7 +27,6 @@ export interface CreateAutomationRequest {\n 'id': string;\n 'name': string;\n 'description'?: string | null;\n- 'enabled'?: boolean;\n 'target': AutomationTarget;\n 'triggers': Array;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/replace-automation-request.ts b/lib/packages/fabro-api-client/src/models/replace-automation-request.ts\nindex 4edb9df6b..49a5533f4 100644\n--- a/lib/packages/fabro-api-client/src/models/replace-automation-request.ts\n+++ b/lib/packages/fabro-api-client/src/models/replace-automation-request.ts\n@@ -26,7 +26,6 @@ import type { AutomationTrigger } from './automation-trigger';\n export interface ReplaceAutomationRequest {\n 'name': string;\n 'description'?: string | null;\n- 'enabled': boolean;\n 'target': AutomationTarget;\n 'triggers': Array;\n }\n",
+ "summary": {
+ "files_changed": 14,
+ "additions": 54,
+ "deletions": 108
+ }
+ }
+ },
+ {
+ "seq": 0,
+ "checkpoint": {
+ "timestamp": "2026-05-29T20:42:53.703466Z",
+ "current_node": "simplify_opus",
+ "completed_nodes": [
+ "start",
+ "toolchain",
+ "preflight_compile",
+ "preflight_lint",
+ "implement",
+ "simplify_opus"
+ ],
+ "node_retries": {},
+ "context_values": {
+ "last_stage": "simplify_opus",
+ "internal.work_dir": "/home/daytona/workspace/fabro",
+ "internal.fidelity": "compact",
+ "response.simplify_opus": "All checks pass: `fabro-automation`, `fabro-api automation_round_trip`, `fabro-server` automation integration tests, web typecheck, api-client typecheck, `cargo fmt --check`, and `clippy -D warnings`.\n\n## Summary of fixes applied\n\n**Reuse / quality (web):**\n- Created `apps/fabro-web/app/lib/automation.ts` with `findApiTrigger`, `findScheduleTrigger`, and `hasEnabledApiTrigger` — replaces five inline `triggers.find((t) => t.type === ...)` call sites across `automations.tsx`, `automation-detail.tsx`, and `automation-form.tsx`.\n- Collapsed the `AutomationCard` `disabled` + `menuDisabled` props into a single `busy` prop; the run-button `disabled` is derived inside the card from `busy || !automation.apiEnabled`.\n- Removed the redundant `!automation.apiEnabled` guard in `runAutomation` (already enforced by the button's `disabled`).\n- `automation-detail.tsx` now uses the local `canRun` for the tooltip instead of re-reading `apiTrigger?.enabled`.\n\n**Efficiency (web):**\n- Hoisted `query.toLowerCase()` out of the filter callback in `automations.tsx`.\n\n**Quality (Rust):**\n- Renamed `persisted_toml_rejects_legacy_top_level_enabled` → `persisted_toml_rejects_unknown_top_level_fields` (greenfield app — no \"legacy\" migration story).\n- Trimmed the redundant doc-comment on `Automation::enabled_api_trigger`.\n\n**Skipped (with reasons):**\n- Renaming the error code `automation_api_trigger_disabled` — the plan explicitly requires keeping it.\n- Restoring the \"at least one trigger enabled\" form check — the plan explicitly removed it.\n- Memoizing `mapAutomations`/`filtered` and lazy `CronParser` — micro-opts on bounded inputs, not worth the complexity.",
+ "response.implement": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as the only activation control.\n- Updated `enabled_api_trigger()` to only check for an enabled API trigger.\n- Added the no-compatibility TOML test rejecting legacy top-level `enabled`.\n- Updated server conflict detail to `automation has no enabled API trigger`.\n- Removed top-level `enabled` from server fixtures/tests and deleted the disabled-automation master-gate test.\n- Updated OpenAPI schemas and regenerated the TypeScript Axios client.\n- Removed the web UI master toggle and all create/replace payload usage.\n- Updated web run availability to derive from the API trigger only.\n- Used a subagent audit and a red/green cycle: the new legacy-TOML rejection test failed before implementation and passed after the model change.\n\nVerification run:\n- `cargo nextest run -p fabro-automation` ✅\n- `cargo build -p fabro-api` ✅\n- `cargo nextest run -p fabro-api --test automation_round_trip` ✅\n- `cargo nextest run -p fabro-server --features test-support 'api::automations'` ✅\n- `cd lib/packages/fabro-api-client && bun run generate` ✅\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `git diff --check` ✅\n\nReference scan:\n- No removed top-level `automation.enabled` / master-gate references remain.\n- Remaining `enabled` hits are trigger-level fields/usages or the intended `enabled_api_trigger()` call.\n\nEnvironment note:\n- Installed `default-jre-headless` in the container because OpenAPI Generator required `java` to regenerate the TypeScript client.",
+ "thread.preflight_compile.current_node": "preflight_lint",
+ "internal.retry_count.simplify_opus": 0,
+ "internal.retry_count.preflight_lint": 0,
+ "graph.rankdir": "LR",
+ "internal.thread_id": "implement",
+ "thread.start.current_node": "toolchain",
+ "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
+ "internal.retry_count.preflight_compile": 0,
+ "current_node": "simplify_opus",
+ "internal.retry_count.start": 0,
"graph.goal": "# Remove Automation Master Enabled Gate Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Remove the top-level automation `enabled` field so trigger-level `enabled` is the only activation control.\n\n**Architecture:** Automations keep their existing file-backed TOML store and REST API, but the top-level master gate disappears from the Rust domain model, persisted TOML, OpenAPI schemas, generated clients, and web UI. API/manual run creation checks only for an enabled `api` trigger. No migration or legacy parser path is added because automations are brand new; TOML that still contains top-level `enabled` is obsolete input.\n\n**Tech Stack:** Rust, serde/TOML, Axum, OpenAPI/progenitor, TypeScript Axios client generation, React 19, SWR, Tailwind CSS.\n\n---\n\n## File Structure\n\n- Modify `lib/crates/fabro-automation/src/model.rs` for the core type and TOML shape.\n- Modify `lib/crates/fabro-automation/src/store.rs` for unit fixtures that create automation drafts/replacements.\n- Modify `lib/crates/fabro-server/src/server/handler/automations.rs` for API-trigger conflict wording.\n- Modify `lib/crates/fabro-server/tests/it/api/automations.rs` for HTTP fixtures and behavior tests.\n- Modify `docs/public/api-reference/fabro-api.yaml` and regenerate `lib/packages/fabro-api-client/src/**`.\n- Modify `lib/crates/fabro-api/tests/automation_round_trip.rs` for Rust/OpenAPI type parity.\n- Modify `apps/fabro-web/app/components/automation-form.tsx`, `apps/fabro-web/app/routes/automations-new.tsx`, `apps/fabro-web/app/routes/automations-edit.tsx`, `apps/fabro-web/app/routes/automation-detail.tsx`, and `apps/fabro-web/app/routes/automations.tsx` for UI state and trigger-derived run availability.\n\n## Task 1: Remove The Domain Master Gate\n\n**Files:**\n- Modify: `lib/crates/fabro-automation/src/model.rs`\n- Modify: `lib/crates/fabro-automation/src/store.rs`\n\n- [ ] Remove `pub enabled: bool` from `Automation`, `AutomationDraft`, `AutomationReplace`, and `PersistedAutomation`.\n- [ ] Remove `enabled` from every conversion between `AutomationDraft`, `AutomationReplace`, `PersistedAutomation`, and `Automation`.\n- [ ] Update `Automation::enabled_api_trigger()` to return an enabled API trigger without checking a top-level automation flag:\n\n```rust\n/// Returns the enabled API trigger if the automation has one.\n/// Returns `None` when the automation has no enabled API trigger.\n#[must_use]\npub fn enabled_api_trigger(&self) -> Option<&ApiTrigger> {\n self.triggers.iter().find_map(|trigger| match trigger {\n AutomationTrigger::Api(trigger) if trigger.enabled => Some(trigger),\n _ => None,\n })\n}\n```\n\n- [ ] Remove the now-unused `default_true()` helper if no other code in the file still uses it.\n- [ ] Update `persisted_toml_applies_defaults_and_canonicalizes_without_id_or_revision` so the fixture has no top-level `enabled = true`, does not assert `automation.enabled`, and asserts the canonical TOML has no top-level `enabled` line:\n\n```rust\nassert!(!top_level_lines(&toml).any(|line| line.starts_with(\"enabled = \")));\n```\n\n- [ ] Add a focused no-compatibility test in `lib/crates/fabro-automation/src/model.rs`:\n\n```rust\n#[test]\nfn persisted_toml_rejects_legacy_top_level_enabled() {\n let bytes = br#\"\nname = \"Legacy\"\nenabled = false\n\n[target]\nrepository = \"fabro-sh/fabro\"\nref = \"main\"\nworkflow = \"release\"\n\n[[triggers]]\ntype = \"api\"\nid = \"manual\"\nenabled = true\n\"#;\n\n let result = Automation::from_toml_bytes(AutomationId::new(\"legacy\").unwrap(), bytes);\n\n assert!(result.is_err());\n}\n```\n\n- [ ] Update `lib/crates/fabro-automation/src/store.rs` test helpers so `draft()` and `replacement()` no longer set top-level `enabled`.\n- [ ] Run:\n\n```bash\ncargo nextest run -p fabro-automation\n```\n\nExpected: all `fabro-automation` tests pass.\n\n## Task 2: Update Server Behavior And Tests\n\n**Files:**\n- Modify: `lib/crates/fabro-server/src/server/handler/automations.rs`\n- Modify: `lib/crates/fabro-server/tests/it/api/automations.rs`\n\n- [ ] Change the `create_automation_run` conflict detail from:\n\n```rust\n\"automation is disabled or has no enabled API trigger\"\n```\n\nto:\n\n```rust\n\"automation has no enabled API trigger\"\n```\n\nKeep the existing code `\"automation_api_trigger_disabled\"` for compatibility with current clients and tests.\n\n- [ ] Remove top-level `\"enabled\": true` from `automation_body()`.\n- [ ] Remove top-level `\"enabled\": false` from `replacement_body()`.\n- [ ] Delete `disabled_automation_run_endpoint_returns_conflict_code`; the master gate no longer exists.\n- [ ] Keep `disabled_api_trigger_run_endpoint_returns_conflict_code` and `missing_api_trigger_run_endpoint_returns_conflict_code` as the authoritative inactive-run tests.\n- [ ] Update any test that mutates `body[\"enabled\"]` or expects top-level enabled in automation JSON/TOML.\n- [ ] Run:\n\n```bash\ncargo nextest run -p fabro-server automations\n```\n\nExpected: automation integration tests pass.\n\n## Task 3: Update OpenAPI And Generated API Types\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/tests/automation_round_trip.rs`\n- Regenerate: `lib/packages/fabro-api-client/src/**`\n\n- [ ] In the `Automation` schema, remove top-level `enabled` from `required` and `properties`.\n- [ ] In `CreateAutomationRequest`, remove top-level `enabled` from `properties`.\n- [ ] In `ReplaceAutomationRequest`, remove top-level `enabled` from `required` and `properties`.\n- [ ] Keep `enabled` on `AutomationApiTrigger` and `AutomationScheduleTrigger`.\n- [ ] Update the `POST /api/v1/automations/{id}/runs` `409` description from:\n\n```yaml\ndescription: Automation is disabled or has no enabled API trigger\n```\n\nto:\n\n```yaml\ndescription: Automation has no enabled API trigger\n```\n\n- [ ] Update `lib/crates/fabro-api/tests/automation_round_trip.rs` so the `Automation`, `CreateAutomationRequest`, and `ReplaceAutomationRequest` JSON fixtures no longer include top-level `\"enabled\"`.\n- [ ] Run:\n\n```bash\ncargo build -p fabro-api\n```\n\nExpected: progenitor type generation succeeds.\n\n- [ ] Run:\n\n```bash\ncargo nextest run -p fabro-api automation_round_trip\n```\n\nExpected: automation type identity and JSON parity tests pass.\n\n- [ ] Regenerate the TypeScript client:\n\n```bash\ncd lib/packages/fabro-api-client && bun run generate\n```\n\nExpected: generated model files remove top-level `enabled` from `Automation`, `CreateAutomationRequest`, and `ReplaceAutomationRequest`.\n\n## Task 4: Remove The Web UI Master Toggle\n\n**Files:**\n- Modify: `apps/fabro-web/app/components/automation-form.tsx`\n- Modify: `apps/fabro-web/app/routes/automations-new.tsx`\n- Modify: `apps/fabro-web/app/routes/automations-edit.tsx`\n- Modify: `apps/fabro-web/app/routes/automation-detail.tsx`\n- Modify: `apps/fabro-web/app/routes/automations.tsx`\n\n- [ ] Remove `enabled` from `AutomationFormValues` and `EMPTY_AUTOMATION_FORM`.\n- [ ] Remove `enabled: automation.enabled` from `automationToFormValues`.\n- [ ] Delete the `Row title=\"Enabled\"` block from `AutomationFormFields`.\n- [ ] Remove `enabled: values.enabled` from the create payload in `automations-new.tsx`.\n- [ ] Remove `enabled: values.enabled` from the replace payload in `automations-edit.tsx`.\n- [ ] In `isFormValid`, remove the requirement that at least one trigger is enabled. The final return should only require non-empty ID, name, repository, ref, and workflow:\n\n```ts\nreturn (\n values.id.trim() !== \"\" &&\n values.name.trim() !== \"\" &&\n values.repository.trim() !== \"\" &&\n values.ref.trim() !== \"\" &&\n values.workflow.trim() !== \"\"\n);\n```\n\n- [ ] In `automation-detail.tsx`, change run availability to:\n\n```ts\nconst canRun = apiTrigger?.enabled === true;\n```\n\n- [ ] In `automation-detail.tsx`, remove `StatusChip`, remove its use, and simplify the Run button `title` so only a missing/disabled API trigger explains the disabled state:\n\n```ts\ntitle={!apiTrigger?.enabled ? \"Enable the API trigger to run it\" : undefined}\n```\n\n- [ ] In `automations.tsx`, extend `AutomationRow` with `apiEnabled: boolean`, set it from the enabled API trigger in `mapAutomations`, and pass `disabled={deleting || !automation.apiEnabled || (runningId !== null && runningId !== automation.id)}` to the run button path.\n- [ ] In `AutomationCard`, make the run button title reflect trigger-disabled state:\n\n```tsx\ntitle={\n running\n ? \"Starting run...\"\n : automation.apiEnabled\n ? \"Run automation\"\n : \"Enable the API trigger to run it\"\n}\n```\n\nUse exactly this title text for the disabled/run states; do not change visible button copy.\n\n- [ ] Run:\n\n```bash\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: TypeScript passes with no `automation.enabled` references.\n\n## Task 5: Final Verification\n\n**Files:**\n- No additional source edits expected.\n\n- [ ] Run the focused backend checks:\n\n```bash\ncargo nextest run -p fabro-automation\ncargo nextest run -p fabro-api automation_round_trip\ncargo nextest run -p fabro-server automations\n```\n\nExpected: all focused Rust checks pass.\n\n- [ ] Run the focused frontend checks:\n\n```bash\ncd lib/packages/fabro-api-client && bun run typecheck\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: generated client and web app typecheck.\n\n- [ ] Search for leftover master-gate references:\n\n```bash\nrg -n \"automation\\\\.enabled|enabled_api_trigger\\\\(\\\\).*automation|Automation is disabled|automation is disabled|\\\\\\\"enabled\\\\\\\": true\" \\\n lib/crates/fabro-automation \\\n lib/crates/fabro-server/tests/it/api/automations.rs \\\n lib/crates/fabro-server/src/server/handler/automations.rs \\\n lib/crates/fabro-api/tests/automation_round_trip.rs \\\n apps/fabro-web/app/components/automation-form.tsx \\\n apps/fabro-web/app/routes/automations-new.tsx \\\n apps/fabro-web/app/routes/automations-edit.tsx \\\n apps/fabro-web/app/routes/automation-detail.tsx \\\n apps/fabro-web/app/routes/automations.tsx\n```\n\nExpected: no hits for the removed top-level automation enabled gate. Hits for trigger-level `enabled` are acceptable when they are clearly on `AutomationTrigger`, `ApiTrigger`, or `ScheduleTrigger`.\n\n- [ ] Optional formatting check:\n\n```bash\ncargo +nightly-2026-04-14 fmt --check --all\n```\n\nExpected: Rust formatting passes. If it fails on touched Rust files, run `cargo +nightly-2026-04-14 fmt --all` and re-run the focused Rust checks.\n\n## Explicit Assumptions\n\n- No migration or compatibility parser is added for top-level automation `enabled`.\n- Existing automation TOML files that still contain top-level `enabled` are obsolete and may fail startup until manually edited.\n- Trigger-level `enabled` remains public API and persisted TOML.\n- API/manual run creation remains controlled by the enabled `api` trigger only.\n- Schedule execution, cron semantics, and schedule-trigger preservation are outside this cleanup.\n",
"failure_class": "",
"internal.run_id": "01KSTNZTNTN8QP3QMKQZ6JCH3F",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
- "last_stage": "implement",
"internal.retry_count.implement": 0,
- "internal.work_dir": "/home/daytona/workspace/fabro",
- "last_response": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as th",
- "internal.fidelity": "compact",
+ "last_response": "All checks pass: `fabro-automation`, `fabro-api automation_round_trip`, `fabro-server` automation integration tests, web typecheck, api-client typecheck, `cargo fmt --check`, and `clippy -D warnings`.",
"internal.node_visit_count": 1,
"thread.toolchain.current_node": "preflight_compile",
- "response.implement": "Implemented the automation master `enabled` gate removal end-to-end.\n\nSummary:\n- Removed top-level `enabled` from Rust automation domain/persistence/request types.\n- Kept trigger-level `enabled` as the only activation control.\n- Updated `enabled_api_trigger()` to only check for an enabled API trigger.\n- Added the no-compatibility TOML test rejecting legacy top-level `enabled`.\n- Updated server conflict detail to `automation has no enabled API trigger`.\n- Removed top-level `enabled` from server fixtures/tests and deleted the disabled-automation master-gate test.\n- Updated OpenAPI schemas and regenerated the TypeScript Axios client.\n- Removed the web UI master toggle and all create/replace payload usage.\n- Updated web run availability to derive from the API trigger only.\n- Used a subagent audit and a red/green cycle: the new legacy-TOML rejection test failed before implementation and passed after the model change.\n\nVerification run:\n- `cargo nextest run -p fabro-automation` ✅\n- `cargo build -p fabro-api` ✅\n- `cargo nextest run -p fabro-api --test automation_round_trip` ✅\n- `cargo nextest run -p fabro-server --features test-support 'api::automations'` ✅\n- `cd lib/packages/fabro-api-client && bun run generate` ✅\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `git diff --check` ✅\n\nReference scan:\n- No removed top-level `automation.enabled` / master-gate references remain.\n- Remaining `enabled` hits are trigger-level fields/usages or the intended `enabled_api_trigger()` call.\n\nEnvironment note:\n- Installed `default-jre-headless` in the container because OpenAPI Generator required `java` to regenerate the TypeScript client.",
- "thread.preflight_compile.current_node": "preflight_lint",
"outcome": "succeeded",
"failure_signature": "",
"internal.retry_count.toolchain": 0,
- "graph.rankdir": "LR",
- "internal.thread_id": "preflight_lint",
- "thread.start.current_node": "toolchain",
- "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
- "internal.retry_count.preflight_compile": 0,
- "current_node": "implement",
- "internal.retry_count.start": 0,
- "internal.retry_count.preflight_lint": 0,
+ "thread.implement.current_node": "simplify_opus",
"thread.preflight_lint.current_node": "implement"
},
"node_outcomes": {
@@ -893,6 +1040,51 @@
"active_time_ms": 779416
}
},
+ "simplify_opus": {
+ "status": "succeeded",
+ "context_updates": {
+ "last_response": "All checks pass: `fabro-automation`, `fabro-api automation_round_trip`, `fabro-server` automation integration tests, web typecheck, api-client typecheck, `cargo fmt --check`, and `clippy -D warnings`.",
+ "response.simplify_opus": "All checks pass: `fabro-automation`, `fabro-api automation_round_trip`, `fabro-server` automation integration tests, web typecheck, api-client typecheck, `cargo fmt --check`, and `clippy -D warnings`.\n\n## Summary of fixes applied\n\n**Reuse / quality (web):**\n- Created `apps/fabro-web/app/lib/automation.ts` with `findApiTrigger`, `findScheduleTrigger`, and `hasEnabledApiTrigger` — replaces five inline `triggers.find((t) => t.type === ...)` call sites across `automations.tsx`, `automation-detail.tsx`, and `automation-form.tsx`.\n- Collapsed the `AutomationCard` `disabled` + `menuDisabled` props into a single `busy` prop; the run-button `disabled` is derived inside the card from `busy || !automation.apiEnabled`.\n- Removed the redundant `!automation.apiEnabled` guard in `runAutomation` (already enforced by the button's `disabled`).\n- `automation-detail.tsx` now uses the local `canRun` for the tooltip instead of re-reading `apiTrigger?.enabled`.\n\n**Efficiency (web):**\n- Hoisted `query.toLowerCase()` out of the filter callback in `automations.tsx`.\n\n**Quality (Rust):**\n- Renamed `persisted_toml_rejects_legacy_top_level_enabled` → `persisted_toml_rejects_unknown_top_level_fields` (greenfield app — no \"legacy\" migration story).\n- Trimmed the redundant doc-comment on `Automation::enabled_api_trigger`.\n\n**Skipped (with reasons):**\n- Renaming the error code `automation_api_trigger_disabled` — the plan explicitly requires keeping it.\n- Restoring the \"at least one trigger enabled\" form check — the plan explicitly removed it.\n- Memoizing `mapAutomations`/`filtered` and lazy `CronParser` — micro-opts on bounded inputs, not worth the complexity.",
+ "last_stage": "simplify_opus"
+ },
+ "notes": "Stage completed: simplify_opus",
+ "usage": {
+ "input": {
+ "usage": {
+ "model": {
+ "provider": "anthropic",
+ "model_id": "claude-opus-4-7"
+ },
+ "tokens": {
+ "input_tokens": 94252,
+ "output_tokens": 18742,
+ "reasoning_tokens": 0,
+ "cache_read_tokens": 3899131,
+ "cache_write_tokens": 526998
+ }
+ },
+ "facts": {
+ "algorithm": "anthropic",
+ "cache_write_5m_tokens": 526998,
+ "cache_write_1h_tokens": 0
+ }
+ },
+ "total_usd_micros": 6183112
+ },
+ "files_touched": [
+ "/home/daytona/workspace/fabro/apps/fabro-web/app/components/automation-form.tsx",
+ "/home/daytona/workspace/fabro/apps/fabro-web/app/lib/automation.ts",
+ "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/automation-detail.tsx",
+ "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/automations.tsx",
+ "/home/daytona/workspace/fabro/lib/crates/fabro-automation/src/model.rs"
+ ],
+ "timing": {
+ "wall_time_ms": 0,
+ "inference_time_ms": 292573,
+ "tool_time_ms": 289137,
+ "active_time_ms": 581710
+ }
+ },
"start": {
"status": "succeeded",
"usage": null
@@ -912,9 +1104,10 @@
}
}
},
- "next_node_id": "simplify_opus",
+ "next_node_id": "simplify_gpt",
"node_visits": {
"preflight_compile": 1,
+ "simplify_opus": 1,
"start": 1,
"preflight_lint": 1,
"toolchain": 1,
@@ -1002,7 +1195,12 @@
"first_event_seq": 52,
"prompt": null,
"response": null,
- "completion": null,
+ "completion": {
+ "outcome": "succeeded",
+ "notes": "Stage completed: implement",
+ "failure_reason": null,
+ "timestamp": "2026-05-29T20:33:04.674163Z"
+ },
"provider_used": {
"mode": "agent",
"provider": "openai",
@@ -1016,6 +1214,12 @@
"output": null,
"started_at": "2026-05-29T20:20:04.090401Z",
"handler": "agent",
+ "timing": {
+ "wall_time_ms": 780581,
+ "inference_time_ms": 463878,
+ "tool_time_ms": 315538,
+ "active_time_ms": 779416
+ },
"usage": {
"input_tokens": 1280340,
"output_tokens": 11277,
@@ -1210,37 +1414,310 @@
"provider": "openai",
"model": "gpt-5.5",
"context_window_tokens": 272000,
- "input_tokens": 121550,
- "usage_percent": 44.6875,
+ "input_tokens": 121841,
+ "usage_percent": 44.79448529411765,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
- "generated_at": "2026-05-29T20:32:45.965890Z",
- "event_seq": 371,
+ "generated_at": "2026-05-29T20:33:04.632756Z",
+ "event_seq": 377,
"breakdown": [
{
"category": "system_prompt",
- "tokens": 984,
- "usage_percent": 0.36176470588235293
+ "tokens": 982,
+ "usage_percent": 0.3610294117647059
},
{
"category": "tools",
- "tokens": 1388,
- "usage_percent": 0.5102941176470588
+ "tokens": 1385,
+ "usage_percent": 0.5091911764705882
},
{
"category": "memory",
- "tokens": 3304,
- "usage_percent": 1.214705882352941
+ "tokens": 3298,
+ "usage_percent": 1.2125
},
{
"category": "conversation",
- "tokens": 115868,
- "usage_percent": 42.59852941176471
+ "tokens": 116169,
+ "usage_percent": 42.70919117647059
},
{
"category": "other",
- "tokens": 6,
- "usage_percent": 0.0022058823529411764
+ "tokens": 7,
+ "usage_percent": 0.002573529411764706
+ }
+ ],
+ "warnings": []
+ },
+ "state": "succeeded"
+ },
+ "simplify_opus@1": {
+ "first_event_seq": 385,
+ "prompt": null,
+ "response": null,
+ "completion": null,
+ "provider_used": {
+ "mode": "agent",
+ "provider": "anthropic",
+ "model": "claude-opus-4-7"
+ },
+ "diff": null,
+ "script_invocation": null,
+ "script_timing": null,
+ "parallel_results": null,
+ "output": null,
+ "started_at": "2026-05-29T20:33:10.001727Z",
+ "handler": "agent",
+ "usage": {
+ "input_tokens": 94252,
+ "output_tokens": 18742,
+ "total_tokens": 4539123,
+ "reasoning_tokens": 0,
+ "cache_read_tokens": 3899131,
+ "cache_write_tokens": 526998,
+ "total_usd_micros": 6183112
+ },
+ "model": {
+ "provider": "anthropic",
+ "model_id": "claude-opus-4-7"
+ },
+ "todos": {
+ "kind": "anthropic_tasks",
+ "list_id": "anthropic_tasks:03f43f56-2ef8-4b73-af17-9cbd95794bfc",
+ "items": [
+ {
+ "id": "1",
+ "status": "completed",
+ "order": 0,
+ "subject": "Run three parallel code review agents",
+ "description": "Launch reuse, quality, and efficiency review agents on the diff (origin/HEAD~1 vs HEAD) for the automation master-enabled-gate removal.",
+ "active_form": "Running parallel review agents"
+ }
+ ]
+ },
+ "subagents": [
+ {
+ "agent_id": "370446b5",
+ "depth": 1,
+ "task": "You are a Code Reuse reviewer. Working dir: /home/daytona/workspace/fabro. Branch: HEAD vs HEAD~1.\n\nReview the diff from `git diff HEAD~1 HEAD` (already merged) for code reuse opportunities. The diff removed a top-level `enabled` master gate from the Automation domain model, API, and web UI. Trigger-level `enabled` remains.\n\nTouched files:\n- lib/crates/fabro-automation/src/model.rs (Automation, AutomationDraft, AutomationReplace, PersistedAutomation; removed `default_true` helper; added new `persisted_toml_rejects_legacy_top_level_enabled` test; updated `enabled_api_trigger()`)\n- lib/crates/fabro-automation/src/store.rs (test fixtures)\n- lib/crates/fabro-server/src/server/handler/automations.rs (changed conflict detail message)\n- lib/crates/fabro-server/tests/it/api/automations.rs (removed disabled_automation_run_endpoint_returns_conflict_code test; updated fixtures)\n- lib/crates/fabro-api/tests/automation_round_trip.rs (removed `enabled` from fixtures)\n- docs/public/api-reference/fabro-api.yaml (removed top-level enabled from Automation, CreateAutomationRequest, ReplaceAutomationRequest schemas)\n- lib/packages/fabro-api-client/src/models/{automation,create-automation-request,replace-automation-request}.ts (regenerated)\n- apps/fabro-web/app/components/automation-form.tsx (removed `enabled` field from AutomationFormValues)\n- apps/fabro-web/app/routes/automations-new.tsx, automations-edit.tsx (removed `enabled` from payload)\n- apps/fabro-web/app/routes/automation-detail.tsx (canRun simplified, StatusChip removed, title simplified)\n- apps/fabro-web/app/routes/automations.tsx (added apiEnabled to AutomationRow; AutomationCard now takes `disabled` and `menuDisabled` separately; added `hasEnabledApiTrigger` helper; title for run button)\n\nFor each change:\n1. Search for existing utilities/helpers that could replace newly written code. Use Grep to find similar patterns (e.g. `enabled_api_trigger`, `hasEnabledApiTrigger`, `triggers.find((t) => t.type === \"api\")`). Note common locations: utility dirs, shared modules.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use.\n3. Flag inline logic that could use an existing utility — hand-rolled string handling, manual path handling, custom env checks, ad-hoc type guards.\n\nGreenfield app — focus on simplicity, don't worry about backwards compat.\n\nOutput a numbered list of findings with: file/line, what is duplicated, suggested fix. Skip the diff itself — assume the reviewer already has it.\n\nTools: grep, glob, read_file, shell. Do not edit files. Only investigate and report.",
+ "status": {
+ "kind": "completed",
+ "success": true,
+ "turns_used": 31
+ }
+ },
+ {
+ "agent_id": "961efac3",
+ "depth": 1,
+ "task": "You are a Code Quality reviewer. Working dir: /home/daytona/workspace/fabro.\n\nReview the diff from `git diff HEAD~1 HEAD` for hacky patterns. The diff removed a top-level `enabled` master gate from Automation. Trigger-level `enabled` remains the only gate.\n\nTouched files:\n- lib/crates/fabro-automation/src/model.rs (Automation, AutomationDraft, AutomationReplace, PersistedAutomation; removed `default_true` helper; added new test `persisted_toml_rejects_legacy_top_level_enabled`; updated `enabled_api_trigger()` to check trigger.enabled only)\n- lib/crates/fabro-automation/src/store.rs (test fixtures)\n- lib/crates/fabro-server/src/server/handler/automations.rs (changed conflict detail to \"automation has no enabled API trigger\"; kept code `automation_api_trigger_disabled`)\n- lib/crates/fabro-server/tests/it/api/automations.rs (removed `disabled_automation_run_endpoint_returns_conflict_code` test; updated fixtures; added `persisted.get(\"enabled\").is_none()`)\n- lib/crates/fabro-api/tests/automation_round_trip.rs\n- docs/public/api-reference/fabro-api.yaml\n- lib/packages/fabro-api-client/src/models/* (auto-generated)\n- apps/fabro-web/app/components/automation-form.tsx\n- apps/fabro-web/app/routes/automations-new.tsx, automations-edit.tsx\n- apps/fabro-web/app/routes/automation-detail.tsx (canRun, removed StatusChip)\n- apps/fabro-web/app/routes/automations.tsx (added `apiEnabled` to AutomationRow; AutomationCard now takes both `disabled` and `menuDisabled`)\n\nReview for:\n1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls.\n2. Parameter sprawl: adding new parameters instead of generalizing/restructuring.\n3. Copy-paste with slight variation: near-duplicate code that should be unified.\n4. Leaky abstractions: exposing internals that should be encapsulated.\n5. Stringly-typed code: raw strings where constants/enums/branded types exist.\n\nSpecific things to consider:\n- In `automations.tsx`, the `AutomationCard` now receives both `disabled` and `menuDisabled` — is parameter sprawl warranted? Could one prop suffice or be derived inside?\n- The `apiEnabled` field on AutomationRow is computed in `mapAutomations` and used to gate things. Could it be derived inline at consumer instead?\n- `enabled_api_trigger()` Rust function semantics changed but the doc-comment matches new behavior cleanly.\n- The error code `\"automation_api_trigger_disabled\"` was kept \"for compatibility with current clients and tests\" per the plan — but this is greenfield. Should it be renamed to match the new message?\n- The conflict detail message changed but the error code didn't. Consistency?\n- Multiple files duplicate the `triggers.find((t) => t.type === \"api\")` pattern.\n\nGreenfield app — be aggressive on quality.\n\nOutput a numbered list of findings with: file/line, issue, suggested fix. Skip the diff itself.\n\nTools: grep, glob, read_file, shell. Do not edit files. Only investigate and report.",
+ "status": {
+ "kind": "completed",
+ "success": true,
+ "turns_used": 58
+ }
+ },
+ {
+ "agent_id": "b1d84d2c",
+ "depth": 1,
+ "task": "You are an Efficiency reviewer. Working dir: /home/daytona/workspace/fabro.\n\nReview the diff from `git diff HEAD~1 HEAD` for efficiency issues. The diff removed a top-level automation `enabled` master gate.\n\nTouched files (mostly removals of an unused field):\n- lib/crates/fabro-automation/src/model.rs\n- lib/crates/fabro-automation/src/store.rs\n- lib/crates/fabro-server/src/server/handler/automations.rs\n- lib/crates/fabro-server/tests/it/api/automations.rs\n- lib/crates/fabro-api/tests/automation_round_trip.rs\n- docs/public/api-reference/fabro-api.yaml\n- lib/packages/fabro-api-client/src/models/*\n- apps/fabro-web/app/components/automation-form.tsx\n- apps/fabro-web/app/routes/automations-{new,edit,detail}.tsx\n- apps/fabro-web/app/routes/automations.tsx (added `apiEnabled` and `hasEnabledApiTrigger`; `mapAutomations` now also iterates triggers per automation)\n\nReview for:\n1. Unnecessary work: redundant computations, repeated file reads, duplicate API calls, N+1 patterns\n2. Missed concurrency: sequential ops that could be parallel\n3. Hot-path bloat: blocking work in startup or per-request/per-render hot paths\n4. Unnecessary existence checks: TOCTOU patterns\n5. Memory: unbounded structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading whole files when only a portion is needed\n\nSpecific things to consider:\n- `automations.tsx`: `mapAutomations` now calls both `scheduleFor(a)` and `hasEnabledApiTrigger(a)` which both iterate the `triggers` array. Could one pass suffice? Is this a render hot path?\n- `automation-detail.tsx`: `automation.triggers.find((t) => t.type === \"schedule\")` and `automation.triggers.find((t) => t.type === \"api\")` — two scans of the same array on every render. Acceptable, or worth a single pass?\n- The `enabled_api_trigger()` Rust path is called once per run-creation request — acceptable.\n\nGreenfield app.\n\nOutput a numbered list of findings with: file/line, issue, suggested fix, and severity (low/medium/high). Skip the diff itself.\n\nTools: grep, glob, read_file, shell. Do not edit files. Only investigate and report.",
+ "status": {
+ "kind": "completed",
+ "success": true,
+ "turns_used": 20
+ }
+ }
+ ],
+ "permission_level": "full",
+ "agent_tools": [
+ {
+ "name": "AskUserQuestion",
+ "description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": false
+ },
+ {
+ "name": "TaskCreate",
+ "description": "Create pending tasks in the current session. Use concise subjects, descriptions, optional activeForm text, and metadata. Check TaskList first to avoid duplicate tasks.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": true
+ },
+ {
+ "name": "TaskGet",
+ "description": "Get one task by taskId, including subject, status, description, owner, blockedBy, and blocks.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": false
+ },
+ {
+ "name": "TaskList",
+ "description": "List tasks for the current session, including status, owner, and blocking dependencies. Use TaskGet with a taskId for full description and dependency details.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": false
+ },
+ {
+ "name": "TaskUpdate",
+ "description": "Update an existing task's status, text, owner, metadata, or dependencies. Valid statuses are pending, in_progress, completed, and deleted. After completing a task, call TaskList to find newly unblocked work.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": true
+ },
+ {
+ "name": "close_agent",
+ "description": "Close a running subagent that is no longer needed.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "subagent",
+ "invoked": false
+ },
+ {
+ "name": "edit_file",
+ "description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "write",
+ "invoked": true
+ },
+ {
+ "name": "glob",
+ "description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "read",
+ "invoked": true
+ },
+ {
+ "name": "grep",
+ "description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "read",
+ "invoked": true
+ },
+ {
+ "name": "read_file",
+ "description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "read",
+ "invoked": true
+ },
+ {
+ "name": "send_input",
+ "description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "subagent",
+ "invoked": false
+ },
+ {
+ "name": "shell",
+ "description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "shell",
+ "invoked": true
+ },
+ {
+ "name": "spawn_agent",
+ "description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "subagent",
+ "invoked": true
+ },
+ {
+ "name": "wait",
+ "description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "subagent",
+ "invoked": true
+ },
+ {
+ "name": "web_fetch",
+ "description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": false
+ },
+ {
+ "name": "web_search",
+ "description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "other",
+ "invoked": false
+ },
+ {
+ "name": "write_file",
+ "description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
+ "source": {
+ "kind": "native"
+ },
+ "category": "write",
+ "invoked": true
+ }
+ ],
+ "context_window": {
+ "provider": "anthropic",
+ "model": "claude-opus-4-7",
+ "context_window_tokens": 1000000,
+ "input_tokens": 106760,
+ "usage_percent": 10.676,
+ "count_method": "response_usage_scaled_breakdown",
+ "staleness": "live",
+ "generated_at": "2026-05-29T20:42:53.602288Z",
+ "event_seq": 733,
+ "breakdown": [
+ {
+ "category": "system_prompt",
+ "tokens": 2558,
+ "usage_percent": 0.2558
+ },
+ {
+ "category": "tools",
+ "tokens": 2895,
+ "usage_percent": 0.2895
+ },
+ {
+ "category": "memory",
+ "tokens": 6154,
+ "usage_percent": 0.6154
+ },
+ {
+ "category": "conversation",
+ "tokens": 95145,
+ "usage_percent": 9.5145
+ },
+ {
+ "category": "other",
+ "tokens": 8,
+ "usage_percent": 0.0008
}
],
"warnings": []
diff --git a/stages/005-implement@1/diff.patch b/stages/005-implement@1/diff.patch
new file mode 100644
index 000000000..88b409296
--- /dev/null
+++ b/stages/005-implement@1/diff.patch
@@ -0,0 +1,636 @@
+diff --git a/apps/fabro-web/app/components/automation-form.tsx b/apps/fabro-web/app/components/automation-form.tsx
+index 5f79f4a78..5d623534c 100644
+--- a/apps/fabro-web/app/components/automation-form.tsx
++++ b/apps/fabro-web/app/components/automation-form.tsx
+@@ -9,7 +9,6 @@ export interface AutomationFormValues {
+ id: string;
+ name: string;
+ description: string;
+- enabled: boolean;
+ repository: string;
+ ref: string;
+ workflow: string;
+@@ -22,7 +21,6 @@ export const EMPTY_AUTOMATION_FORM: AutomationFormValues = {
+ id: "",
+ name: "",
+ description: "",
+- enabled: true,
+ repository: "",
+ ref: "main",
+ workflow: "",
+@@ -45,7 +43,6 @@ export function automationToFormValues(automation: Automation): AutomationFormVa
+ id: automation.id,
+ name: automation.name,
+ description: automation.description ?? "",
+- enabled: automation.enabled,
+ repository: automation.target.repository,
+ ref: automation.target.ref,
+ workflow: automation.target.workflow,
+@@ -77,8 +74,7 @@ export function isFormValid(values: AutomationFormValues): boolean {
+ values.name.trim() !== "" &&
+ values.repository.trim() !== "" &&
+ values.ref.trim() !== "" &&
+- values.workflow.trim() !== "" &&
+- (values.manualEnabled || values.scheduleEnabled)
++ values.workflow.trim() !== ""
+ );
+ }
+
+@@ -187,13 +183,6 @@ export function AutomationFormFields({
+ className={`${INPUT_CLASS} resize-y`}
+ />
+
+-
+- patch({ enabled })}
+- label="Enable automation"
+- />
+-
+
+
+
+diff --git a/apps/fabro-web/app/routes/automation-detail.tsx b/apps/fabro-web/app/routes/automation-detail.tsx
+index d8c3021dc..57d2274f2 100644
+--- a/apps/fabro-web/app/routes/automation-detail.tsx
++++ b/apps/fabro-web/app/routes/automation-detail.tsx
+@@ -92,7 +92,7 @@ function AutomationHeader({ automation }: { automation: Automation }) {
+
+ const scheduleTrigger = automation.triggers.find((t) => t.type === "schedule");
+ const apiTrigger = automation.triggers.find((t) => t.type === "api");
+- const canRun = apiTrigger?.enabled === true && automation.enabled;
++ const canRun = apiTrigger?.enabled === true;
+
+ async function onRun() {
+ if (!canRun || running) return;
+@@ -137,7 +137,6 @@ function AutomationHeader({ automation }: { automation: Automation }) {
+ {automation.id}
+
+
+-
+
+ {automation.target.repository}
+ · {automation.target.ref}
+@@ -165,13 +164,7 @@ function AutomationHeader({ automation }: { automation: Automation }) {
+ type="button"
+ onClick={onRun}
+ disabled={!canRun || running}
+- title={
+- !automation.enabled
+- ? "Enable the automation to run it"
+- : !apiTrigger?.enabled
+- ? "Enable the API trigger to run it"
+- : undefined
+- }
++ title={!apiTrigger?.enabled ? "Enable the API trigger to run it" : undefined}
+ className={PRIMARY_BUTTON_CLASS}
+ >
+
+@@ -183,19 +176,6 @@ function AutomationHeader({ automation }: { automation: Automation }) {
+ );
+ }
+
+-function StatusChip({ enabled }: { enabled: boolean }) {
+- return (
+-
+-
+-
+- {enabled ? "Enabled" : "Disabled"}
+-
+-
+- );
+-}
+-
+ function Chip({
+ icon: Icon,
+ children,
+diff --git a/apps/fabro-web/app/routes/automations-edit.tsx b/apps/fabro-web/app/routes/automations-edit.tsx
+index cb8e10324..e77a1e159 100644
+--- a/apps/fabro-web/app/routes/automations-edit.tsx
++++ b/apps/fabro-web/app/routes/automations-edit.tsx
+@@ -85,7 +85,6 @@ function EditAutomationForm({ automation }: { automation: Automation }) {
+ automationsApi.replaceAutomation(automation.id, automation.revision, {
+ name: trimmedName,
+ description: values.description.trim() || null,
+- enabled: values.enabled,
+ target: {
+ repository: values.repository.trim(),
+ ref: values.ref.trim(),
+diff --git a/apps/fabro-web/app/routes/automations-new.tsx b/apps/fabro-web/app/routes/automations-new.tsx
+index 1ee221c35..02d56b11f 100644
+--- a/apps/fabro-web/app/routes/automations-new.tsx
++++ b/apps/fabro-web/app/routes/automations-new.tsx
+@@ -47,7 +47,6 @@ export default function AutomationsNew() {
+ id: values.id.trim(),
+ name: trimmedName,
+ description: values.description.trim() || null,
+- enabled: values.enabled,
+ target: {
+ repository: values.repository.trim(),
+ ref: values.ref.trim(),
+diff --git a/apps/fabro-web/app/routes/automations.tsx b/apps/fabro-web/app/routes/automations.tsx
+index 7cb32b59f..f8dfb025f 100644
+--- a/apps/fabro-web/app/routes/automations.tsx
++++ b/apps/fabro-web/app/routes/automations.tsx
+@@ -48,6 +48,7 @@ interface AutomationRow {
+ workflow: string;
+ repository: string;
+ schedule?: string;
++ apiEnabled: boolean;
+ icon: ComponentType<{ className?: string }>;
+ color: string;
+ }
+@@ -81,6 +82,10 @@ function scheduleFor(automation: Automation): string | undefined {
+ return schedule?.expression;
+ }
+
++function hasEnabledApiTrigger(automation: Automation): boolean {
++ return automation.triggers.some((t) => t.type === "api" && t.enabled);
++}
++
+ function mapAutomations(result: AutomationListResponse | undefined): AutomationRow[] {
+ const automations = result?.data ?? [];
+ return automations.map((a) => ({
+@@ -90,6 +95,7 @@ function mapAutomations(result: AutomationListResponse | undefined): AutomationR
+ workflow: a.target.workflow,
+ repository: a.target.repository,
+ schedule: scheduleFor(a),
++ apiEnabled: hasEnabledApiTrigger(a),
+ icon: slugIconMap[a.target.workflow] ?? CodeBracketIcon,
+ color: slugColorMap[a.target.workflow] ?? "var(--color-teal-500)",
+ }));
+@@ -106,12 +112,14 @@ function PlayIcon({ className }: { className?: string }) {
+ function AutomationCard({
+ automation,
+ disabled,
++ menuDisabled,
+ running,
+ onRun,
+ onDelete,
+ }: {
+ automation: AutomationRow;
+ disabled: boolean;
++ menuDisabled: boolean;
+ running: boolean;
+ onRun: () => void;
+ onDelete: () => void;
+@@ -156,7 +164,13 @@ function AutomationCard({
+ onClick={onRun}
+ disabled={running || disabled}
+ aria-label={running ? "Starting run…" : "Run automation"}
+- title={running ? "Starting run…" : "Run automation"}
++ title={
++ running
++ ? "Starting run..."
++ : automation.apiEnabled
++ ? "Run automation"
++ : "Enable the API trigger to run it"
++ }
+ className="flex size-8 shrink-0 items-center justify-center rounded-full border border-mint/20 text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-mint"
+ >
+ {running ? (
+@@ -167,7 +181,7 @@ function AutomationCard({
+
+ )}
+
+-
++
+
+ );
+ }
+@@ -242,7 +256,7 @@ export default function Automations() {
+ const [runningId, setRunningId] = useState(null);
+
+ async function runAutomation(automation: AutomationRow) {
+- if (runningId) return;
++ if (runningId || !automation.apiEnabled) return;
+ setRunningId(automation.id);
+ try {
+ const run = await apiData(() => automationsApi.createAutomationRun(automation.id));
+@@ -322,7 +336,12 @@ export default function Automations() {
+ runAutomation(automation)}
+ onDelete={() => setPendingDelete(automation)}
+diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml
+index 103ab4cad..b7c77775a 100644
+--- a/docs/public/api-reference/fabro-api.yaml
++++ b/docs/public/api-reference/fabro-api.yaml
+@@ -4206,7 +4206,7 @@ paths:
+ schema:
+ $ref: "#/components/schemas/ErrorResponse"
+ "409":
+- description: Automation is disabled or has no enabled API trigger
++ description: Automation has no enabled API trigger
+ headers:
+ x-request-id:
+ $ref: "#/components/headers/XRequestId"
+@@ -5841,7 +5841,6 @@ components:
+ - revision
+ - name
+ - description
+- - enabled
+ - target
+ - triggers
+ properties:
+@@ -5860,9 +5859,6 @@ components:
+ description:
+ type: ["string", "null"]
+ example: Keeps dependencies fresh.
+- enabled:
+- type: boolean
+- example: true
+ target:
+ $ref: "#/components/schemas/AutomationTarget"
+ triggers:
+@@ -5970,9 +5966,6 @@ components:
+ description:
+ type: ["string", "null"]
+ example: Keeps dependencies fresh.
+- enabled:
+- type: boolean
+- default: true
+ target:
+ $ref: "#/components/schemas/AutomationTarget"
+ triggers:
+@@ -5986,7 +5979,6 @@ components:
+ additionalProperties: false
+ required:
+ - name
+- - enabled
+ - target
+ - triggers
+ properties:
+@@ -5996,8 +5988,6 @@ components:
+ description:
+ type: ["string", "null"]
+ example: Keeps dependencies fresh.
+- enabled:
+- type: boolean
+ target:
+ $ref: "#/components/schemas/AutomationTarget"
+ triggers:
+diff --git a/lib/crates/fabro-api/tests/automation_round_trip.rs b/lib/crates/fabro-api/tests/automation_round_trip.rs
+index 9984fbb39..db4a66d03 100644
+--- a/lib/crates/fabro-api/tests/automation_round_trip.rs
++++ b/lib/crates/fabro-api/tests/automation_round_trip.rs
+@@ -26,7 +26,6 @@ fn automation_response_round_trips_public_json_shape() {
+ "revision": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ "name": "Nightly dependency update",
+ "description": null,
+- "enabled": true,
+ "target": {
+ "repository": "fabro-sh/fabro",
+ "ref": "main",
+@@ -57,7 +56,6 @@ fn create_automation_request_round_trips_public_json_shape() {
+ "id": "nightly-deps",
+ "name": "Nightly dependency update",
+ "description": "Keep dependencies fresh",
+- "enabled": true,
+ "target": {
+ "repository": "fabro-sh/fabro",
+ "ref": "main",
+@@ -81,7 +79,6 @@ fn replace_automation_request_round_trips_public_json_shape() {
+ let value = json!({
+ "name": "Nightly dependency update",
+ "description": "Keep dependencies fresh",
+- "enabled": true,
+ "target": {
+ "repository": "fabro-sh/fabro",
+ "ref": "main",
+diff --git a/lib/crates/fabro-automation/src/model.rs b/lib/crates/fabro-automation/src/model.rs
+index 6e5cda0c1..90dc24eb7 100644
+--- a/lib/crates/fabro-automation/src/model.rs
++++ b/lib/crates/fabro-automation/src/model.rs
+@@ -15,7 +15,6 @@ pub struct Automation {
+ pub revision: AutomationRevision,
+ pub name: String,
+ pub description: Option,
+- pub enabled: bool,
+ pub target: AutomationTarget,
+ pub triggers: Vec,
+ }
+@@ -54,7 +53,6 @@ impl Automation {
+ PersistedAutomation {
+ name: self.name.clone(),
+ description: self.description.clone(),
+- enabled: self.enabled,
+ target: self.target.clone(),
+ triggers: self.triggers.clone(),
+ }
+@@ -64,14 +62,10 @@ impl Automation {
+ toml::to_string_pretty(&self.to_persisted()).map_err(AutomationStoreError::from)
+ }
+
+- /// Returns the enabled API trigger if the automation itself is enabled and
+- /// has one. Returns `None` when the automation is disabled or has no
+- /// enabled API trigger.
++ /// Returns the enabled API trigger if the automation has one.
++ /// Returns `None` when the automation has no enabled API trigger.
+ #[must_use]
+ pub fn enabled_api_trigger(&self) -> Option<&ApiTrigger> {
+- if !self.enabled {
+- return None;
+- }
+ self.triggers.iter().find_map(|trigger| match trigger {
+ AutomationTrigger::Api(trigger) if trigger.enabled => Some(trigger),
+ _ => None,
+@@ -98,7 +92,6 @@ impl Automation {
+ revision,
+ name: replace.name,
+ description: replace.description,
+- enabled: replace.enabled,
+ target: replace.target,
+ triggers: replace.triggers,
+ }
+@@ -161,8 +154,6 @@ pub struct AutomationDraft {
+ pub name: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub description: Option,
+- #[serde(default = "default_true")]
+- pub enabled: bool,
+ pub target: AutomationTarget,
+ pub triggers: Vec,
+ }
+@@ -172,7 +163,6 @@ impl From for (AutomationId, AutomationReplace) {
+ (value.id, AutomationReplace {
+ name: value.name,
+ description: value.description,
+- enabled: value.enabled,
+ target: value.target,
+ triggers: value.triggers,
+ })
+@@ -185,7 +175,6 @@ pub struct AutomationReplace {
+ pub name: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub description: Option,
+- pub enabled: bool,
+ pub target: AutomationTarget,
+ pub triggers: Vec,
+ }
+@@ -196,8 +185,6 @@ pub(crate) struct PersistedAutomation {
+ name: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ description: Option,
+- #[serde(default = "default_true")]
+- enabled: bool,
+ target: AutomationTarget,
+ #[serde(default)]
+ triggers: Vec,
+@@ -208,7 +195,6 @@ impl From for PersistedAutomation {
+ Self {
+ name: value.name,
+ description: value.description,
+- enabled: value.enabled,
+ target: value.target,
+ triggers: value.triggers,
+ }
+@@ -220,7 +206,6 @@ impl From for AutomationReplace {
+ Self {
+ name: value.name,
+ description: value.description,
+- enabled: value.enabled,
+ target: value.target,
+ triggers: value.triggers,
+ }
+@@ -390,10 +375,6 @@ fn validate_triggers(triggers: &[AutomationTrigger]) -> Result<(), AutomationVal
+ Ok(())
+ }
+
+-fn default_true() -> bool {
+- true
+-}
+-
+ #[cfg(test)]
+ mod tests {
+ use crate::{
+@@ -450,30 +431,49 @@ expression = "0 0 * * *"
+ Automation::from_toml_bytes(AutomationId::new("nightly").unwrap(), bytes).unwrap();
+
+ assert_eq!(automation.description, None);
+- assert!(automation.enabled);
+ assert!(automation.triggers.iter().all(AutomationTrigger::enabled));
+
+ let toml = automation.to_toml_string().unwrap();
+ assert!(!top_level_lines(&toml).any(|line| line.starts_with("id = ")));
+ assert!(!top_level_lines(&toml).any(|line| line.starts_with("revision = ")));
+- assert!(toml.contains("enabled = true"));
++ assert!(!top_level_lines(&toml).any(|line| line.starts_with("enabled = ")));
+ assert!(toml.contains("type = \"api\""));
+ }
+
++ #[test]
++ fn persisted_toml_rejects_legacy_top_level_enabled() {
++ let bytes = br#"
++name = "Legacy"
++enabled = false
++
++[target]
++repository = "fabro-sh/fabro"
++ref = "main"
++workflow = "release"
++
++[[triggers]]
++type = "api"
++id = "manual"
++enabled = true
++"#;
++
++ let result = Automation::from_toml_bytes(AutomationId::new("legacy").unwrap(), bytes);
++
++ assert!(result.is_err());
++ }
++
+ #[test]
+ fn validation_rejects_invalid_inputs() {
+ let cases = [
+ AutomationReplace {
+ name: " ".to_string(),
+ description: None,
+- enabled: true,
+ target: target(),
+ triggers: vec![api_trigger("manual")],
+ },
+ AutomationReplace {
+ name: "Bad repo".to_string(),
+ description: None,
+- enabled: true,
+ target: AutomationTarget {
+ repository: "not/github/slug".to_string(),
+ ref_selector: "main".to_string(),
+@@ -484,7 +484,6 @@ expression = "0 0 * * *"
+ AutomationReplace {
+ name: "Bad ref".to_string(),
+ description: None,
+- enabled: true,
+ target: AutomationTarget {
+ repository: "fabro-sh/fabro".to_string(),
+ ref_selector: "main;rm".to_string(),
+@@ -495,7 +494,6 @@ expression = "0 0 * * *"
+ AutomationReplace {
+ name: "Bad workflow".to_string(),
+ description: None,
+- enabled: true,
+ target: AutomationTarget {
+ repository: "fabro-sh/fabro".to_string(),
+ ref_selector: "main".to_string(),
+@@ -506,7 +504,6 @@ expression = "0 0 * * *"
+ AutomationReplace {
+ name: "Duplicate trigger".to_string(),
+ description: None,
+- enabled: true,
+ target: target(),
+ triggers: vec![
+ api_trigger("manual"),
+@@ -516,21 +513,18 @@ expression = "0 0 * * *"
+ AutomationReplace {
+ name: "Two API triggers".to_string(),
+ description: None,
+- enabled: true,
+ target: target(),
+ triggers: vec![api_trigger("one"), api_trigger("two")],
+ },
+ AutomationReplace {
+ name: "Six field cron".to_string(),
+ description: None,
+- enabled: true,
+ target: target(),
+ triggers: vec![schedule_trigger("nightly", "0 0 0 * * *")],
+ },
+ AutomationReplace {
+ name: "Bad cron".to_string(),
+ description: None,
+- enabled: true,
+ target: target(),
+ triggers: vec![schedule_trigger("nightly", "99 0 * * *")],
+ },
+diff --git a/lib/crates/fabro-automation/src/store.rs b/lib/crates/fabro-automation/src/store.rs
+index 6c0541441..ccd6ea7a4 100644
+--- a/lib/crates/fabro-automation/src/store.rs
++++ b/lib/crates/fabro-automation/src/store.rs
+@@ -290,7 +290,6 @@ mod tests {
+ id: AutomationId::new(id).unwrap(),
+ name: name.to_string(),
+ description: None,
+- enabled: true,
+ target: target(),
+ triggers: vec![
+ AutomationTrigger::Api(ApiTrigger {
+@@ -310,7 +309,6 @@ mod tests {
+ AutomationReplace {
+ name: name.to_string(),
+ description: Some("updated".to_string()),
+- enabled: false,
+ target: target(),
+ triggers: vec![AutomationTrigger::Api(ApiTrigger {
+ id: AutomationTriggerId::new("manual").unwrap(),
+diff --git a/lib/crates/fabro-server/src/server/handler/automations.rs b/lib/crates/fabro-server/src/server/handler/automations.rs
+index c29920d1d..70feedc7e 100644
+--- a/lib/crates/fabro-server/src/server/handler/automations.rs
++++ b/lib/crates/fabro-server/src/server/handler/automations.rs
+@@ -133,7 +133,7 @@ async fn create_automation_run(
+ let Some(api_trigger) = automation.enabled_api_trigger() else {
+ return ApiError::with_code(
+ StatusCode::CONFLICT,
+- "automation is disabled or has no enabled API trigger",
++ "automation has no enabled API trigger",
+ "automation_api_trigger_disabled",
+ )
+ .into_response();
+diff --git a/lib/crates/fabro-server/tests/it/api/automations.rs b/lib/crates/fabro-server/tests/it/api/automations.rs
+index f09f94f38..11ad21a28 100644
+--- a/lib/crates/fabro-server/tests/it/api/automations.rs
++++ b/lib/crates/fabro-server/tests/it/api/automations.rs
+@@ -19,7 +19,6 @@ fn automation_body(id: &str, name: &str) -> Value {
+ "id": id,
+ "name": name,
+ "description": "Runs on a schedule.",
+- "enabled": true,
+ "target": {
+ "repository": "fabro-sh/fabro",
+ "ref": "main",
+@@ -45,7 +44,6 @@ fn replacement_body(name: &str) -> Value {
+ json!({
+ "name": name,
+ "description": null,
+- "enabled": false,
+ "target": {
+ "repository": "fabro-sh/fabro",
+ "ref": "main",
+@@ -289,6 +287,7 @@ async fn schedule_trigger_round_trips_through_create_list_get_and_toml() {
+ assert_persisted_schedule_trigger(&persisted, "0 3 * * *", true);
+ assert!(persisted.get("id").is_none());
+ assert!(persisted.get("revision").is_none());
++ assert!(persisted.get("enabled").is_none());
+ }
+
+ #[tokio::test]
+@@ -692,21 +691,6 @@ async fn automations_routes_require_authenticated_user() {
+ .await;
+ }
+
+-#[tokio::test]
+-async fn disabled_automation_run_endpoint_returns_conflict_code() {
+- let (app, _temp_dir, _automation_dir) = automation_app_with_fake_materializer();
+- let mut body = automation_body("nightly", "Nightly");
+- body["enabled"] = json!(false);
+- create_automation_with_body(&app, &body).await;
+-
+- let error = create_automation_run(&app, "nightly", StatusCode::CONFLICT).await;
+-
+- assert_eq!(
+- error["errors"][0]["code"],
+- "automation_api_trigger_disabled"
+- );
+-}
+-
+ #[tokio::test]
+ async fn missing_automation_run_endpoint_returns_not_found() {
+ let (app, _temp_dir, _automation_dir) = automation_app_with_fake_materializer();
+diff --git a/lib/packages/fabro-api-client/src/models/automation.ts b/lib/packages/fabro-api-client/src/models/automation.ts
+index 575759e19..5c1c20bb1 100644
+--- a/lib/packages/fabro-api-client/src/models/automation.ts
++++ b/lib/packages/fabro-api-client/src/models/automation.ts
+@@ -31,7 +31,6 @@ export interface Automation {
+ 'revision': string;
+ 'name': string;
+ 'description': string | null;
+- 'enabled': boolean;
+ 'target': AutomationTarget;
+ 'triggers': Array;
+ }
+diff --git a/lib/packages/fabro-api-client/src/models/create-automation-request.ts b/lib/packages/fabro-api-client/src/models/create-automation-request.ts
+index 5f21f795b..ce99d34db 100644
+--- a/lib/packages/fabro-api-client/src/models/create-automation-request.ts
++++ b/lib/packages/fabro-api-client/src/models/create-automation-request.ts
+@@ -27,7 +27,6 @@ export interface CreateAutomationRequest {
+ 'id': string;
+ 'name': string;
+ 'description'?: string | null;
+- 'enabled'?: boolean;
+ 'target': AutomationTarget;
+ 'triggers': Array;
+ }
+diff --git a/lib/packages/fabro-api-client/src/models/replace-automation-request.ts b/lib/packages/fabro-api-client/src/models/replace-automation-request.ts
+index 4edb9df6b..49a5533f4 100644
+--- a/lib/packages/fabro-api-client/src/models/replace-automation-request.ts
++++ b/lib/packages/fabro-api-client/src/models/replace-automation-request.ts
+@@ -26,7 +26,6 @@ import type { AutomationTrigger } from './automation-trigger';
+ export interface ReplaceAutomationRequest {
+ 'name': string;
+ 'description'?: string | null;
+- 'enabled': boolean;
+ 'target': AutomationTarget;
+ 'triggers': Array;
+ }
diff --git a/stages/005-implement@1/status.json b/stages/005-implement@1/status.json
new file mode 100644
index 000000000..e3526d0d5
--- /dev/null
+++ b/stages/005-implement@1/status.json
@@ -0,0 +1,6 @@
+{
+ "outcome": "succeeded",
+ "notes": "Stage completed: implement",
+ "failure_reason": null,
+ "timestamp": "2026-05-29T20:33:04.674163Z"
+}
\ No newline at end of file
diff --git a/stages/006-simplify_opus@1/prompt.md b/stages/006-simplify_opus@1/prompt.md
new file mode 100644
index 000000000..15b958f5f
--- /dev/null
+++ b/stages/006-simplify_opus@1/prompt.md
@@ -0,0 +1,353 @@
+Goal: # Remove Automation Master Enabled Gate Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Remove the top-level automation `enabled` field so trigger-level `enabled` is the only activation control.
+
+**Architecture:** Automations keep their existing file-backed TOML store and REST API, but the top-level master gate disappears from the Rust domain model, persisted TOML, OpenAPI schemas, generated clients, and web UI. API/manual run creation checks only for an enabled `api` trigger. No migration or legacy parser path is added because automations are brand new; TOML that still contains top-level `enabled` is obsolete input.
+
+**Tech Stack:** Rust, serde/TOML, Axum, OpenAPI/progenitor, TypeScript Axios client generation, React 19, SWR, Tailwind CSS.
+
+---
+
+## File Structure
+
+- Modify `lib/crates/fabro-automation/src/model.rs` for the core type and TOML shape.
+- Modify `lib/crates/fabro-automation/src/store.rs` for unit fixtures that create automation drafts/replacements.
+- Modify `lib/crates/fabro-server/src/server/handler/automations.rs` for API-trigger conflict wording.
+- Modify `lib/crates/fabro-server/tests/it/api/automations.rs` for HTTP fixtures and behavior tests.
+- Modify `docs/public/api-reference/fabro-api.yaml` and regenerate `lib/packages/fabro-api-client/src/**`.
+- Modify `lib/crates/fabro-api/tests/automation_round_trip.rs` for Rust/OpenAPI type parity.
+- Modify `apps/fabro-web/app/components/automation-form.tsx`, `apps/fabro-web/app/routes/automations-new.tsx`, `apps/fabro-web/app/routes/automations-edit.tsx`, `apps/fabro-web/app/routes/automation-detail.tsx`, and `apps/fabro-web/app/routes/automations.tsx` for UI state and trigger-derived run availability.
+
+## Task 1: Remove The Domain Master Gate
+
+**Files:**
+- Modify: `lib/crates/fabro-automation/src/model.rs`
+- Modify: `lib/crates/fabro-automation/src/store.rs`
+
+- [ ] Remove `pub enabled: bool` from `Automation`, `AutomationDraft`, `AutomationReplace`, and `PersistedAutomation`.
+- [ ] Remove `enabled` from every conversion between `AutomationDraft`, `AutomationReplace`, `PersistedAutomation`, and `Automation`.
+- [ ] Update `Automation::enabled_api_trigger()` to return an enabled API trigger without checking a top-level automation flag:
+
+```rust
+/// Returns the enabled API trigger if the automation has one.
+/// Returns `None` when the automation has no enabled API trigger.
+#[must_use]
+pub fn enabled_api_trigger(&self) -> Option<&ApiTrigger> {
+ self.triggers.iter().find_map(|trigger| match trigger {
+ AutomationTrigger::Api(trigger) if trigger.enabled => Some(trigger),
+ _ => None,
+ })
+}
+```
+
+- [ ] Remove the now-unused `default_true()` helper if no other code in the file still uses it.
+- [ ] Update `persisted_toml_applies_defaults_and_canonicalizes_without_id_or_revision` so the fixture has no top-level `enabled = true`, does not assert `automation.enabled`, and asserts the canonical TOML has no top-level `enabled` line:
+
+```rust
+assert!(!top_level_lines(&toml).any(|line| line.starts_with("enabled = ")));
+```
+
+- [ ] Add a focused no-compatibility test in `lib/crates/fabro-automation/src/model.rs`:
+
+```rust
+#[test]
+fn persisted_toml_rejects_legacy_top_level_enabled() {
+ let bytes = br#"
+name = "Legacy"
+enabled = false
+
+[target]
+repository = "fabro-sh/fabro"
+ref = "main"
+workflow = "release"
+
+[[triggers]]
+type = "api"
+id = "manual"
+enabled = true
+"#;
+
+ let result = Automation::from_toml_bytes(AutomationId::new("legacy").unwrap(), bytes);
+
+ assert!(result.is_err());
+}
+```
+
+- [ ] Update `lib/crates/fabro-automation/src/store.rs` test helpers so `draft()` and `replacement()` no longer set top-level `enabled`.
+- [ ] Run:
+
+```bash
+cargo nextest run -p fabro-automation
+```
+
+Expected: all `fabro-automation` tests pass.
+
+## Task 2: Update Server Behavior And Tests
+
+**Files:**
+- Modify: `lib/crates/fabro-server/src/server/handler/automations.rs`
+- Modify: `lib/crates/fabro-server/tests/it/api/automations.rs`
+
+- [ ] Change the `create_automation_run` conflict detail from:
+
+```rust
+"automation is disabled or has no enabled API trigger"
+```
+
+to:
+
+```rust
+"automation has no enabled API trigger"
+```
+
+Keep the existing code `"automation_api_trigger_disabled"` for compatibility with current clients and tests.
+
+- [ ] Remove top-level `"enabled": true` from `automation_body()`.
+- [ ] Remove top-level `"enabled": false` from `replacement_body()`.
+- [ ] Delete `disabled_automation_run_endpoint_returns_conflict_code`; the master gate no longer exists.
+- [ ] Keep `disabled_api_trigger_run_endpoint_returns_conflict_code` and `missing_api_trigger_run_endpoint_returns_conflict_code` as the authoritative inactive-run tests.
+- [ ] Update any test that mutates `body["enabled"]` or expects top-level enabled in automation JSON/TOML.
+- [ ] Run:
+
+```bash
+cargo nextest run -p fabro-server automations
+```
+
+Expected: automation integration tests pass.
+
+## Task 3: Update OpenAPI And Generated API Types
+
+**Files:**
+- Modify: `docs/public/api-reference/fabro-api.yaml`
+- Modify: `lib/crates/fabro-api/tests/automation_round_trip.rs`
+- Regenerate: `lib/packages/fabro-api-client/src/**`
+
+- [ ] In the `Automation` schema, remove top-level `enabled` from `required` and `properties`.
+- [ ] In `CreateAutomationRequest`, remove top-level `enabled` from `properties`.
+- [ ] In `ReplaceAutomationRequest`, remove top-level `enabled` from `required` and `properties`.
+- [ ] Keep `enabled` on `AutomationApiTrigger` and `AutomationScheduleTrigger`.
+- [ ] Update the `POST /api/v1/automations/{id}/runs` `409` description from:
+
+```yaml
+description: Automation is disabled or has no enabled API trigger
+```
+
+to:
+
+```yaml
+description: Automation has no enabled API trigger
+```
+
+- [ ] Update `lib/crates/fabro-api/tests/automation_round_trip.rs` so the `Automation`, `CreateAutomationRequest`, and `ReplaceAutomationRequest` JSON fixtures no longer include top-level `"enabled"`.
+- [ ] Run:
+
+```bash
+cargo build -p fabro-api
+```
+
+Expected: progenitor type generation succeeds.
+
+- [ ] Run:
+
+```bash
+cargo nextest run -p fabro-api automation_round_trip
+```
+
+Expected: automation type identity and JSON parity tests pass.
+
+- [ ] Regenerate the TypeScript client:
+
+```bash
+cd lib/packages/fabro-api-client && bun run generate
+```
+
+Expected: generated model files remove top-level `enabled` from `Automation`, `CreateAutomationRequest`, and `ReplaceAutomationRequest`.
+
+## Task 4: Remove The Web UI Master Toggle
+
+**Files:**
+- Modify: `apps/fabro-web/app/components/automation-form.tsx`
+- Modify: `apps/fabro-web/app/routes/automations-new.tsx`
+- Modify: `apps/fabro-web/app/routes/automations-edit.tsx`
+- Modify: `apps/fabro-web/app/routes/automation-detail.tsx`
+- Modify: `apps/fabro-web/app/routes/automations.tsx`
+
+- [ ] Remove `enabled` from `AutomationFormValues` and `EMPTY_AUTOMATION_FORM`.
+- [ ] Remove `enabled: automation.enabled` from `automationToFormValues`.
+- [ ] Delete the `Row title="Enabled"` block from `AutomationFormFields`.
+- [ ] Remove `enabled: values.enabled` from the create payload in `automations-new.tsx`.
+- [ ] Remove `enabled: values.enabled` from the replace payload in `automations-edit.tsx`.
+- [ ] In `isFormValid`, remove the requirement that at least one trigger is enabled. The final return should only require non-empty ID, name, repository, ref, and workflow:
+
+```ts
+return (
+ values.id.trim() !== "" &&
+ values.name.trim() !== "" &&
+ values.repository.trim() !== "" &&
+ values.ref.trim() !== "" &&
+ values.workflow.trim() !== ""
+);
+```
+
+- [ ] In `automation-detail.tsx`, change run availability to:
+
+```ts
+const canRun = apiTrigger?.enabled === true;
+```
+
+- [ ] In `automation-detail.tsx`, remove `StatusChip`, remove its use, and simplify the Run button `title` so only a missing/disabled API trigger explains the disabled state:
+
+```ts
+title={!apiTrigger?.enabled ? "Enable the API trigger to run it" : undefined}
+```
+
+- [ ] In `automations.tsx`, extend `AutomationRow` with `apiEnabled: boolean`, set it from the enabled API trigger in `mapAutomations`, and pass `disabled={deleting || !automation.apiEnabled || (runningId !== null && runningId !== automation.id)}` to the run button path.
+- [ ] In `AutomationCard`, make the run button title reflect trigger-disabled state:
+
+```tsx
+title={
+ running
+ ? "Starting run..."
+ : automation.apiEnabled
+ ? "Run automation"
+ : "Enable the API trigger to run it"
+}
+```
+
+Use exactly this title text for the disabled/run states; do not change visible button copy.
+
+- [ ] Run:
+
+```bash
+cd apps/fabro-web && bun run typecheck
+```
+
+Expected: TypeScript passes with no `automation.enabled` references.
+
+## Task 5: Final Verification
+
+**Files:**
+- No additional source edits expected.
+
+- [ ] Run the focused backend checks:
+
+```bash
+cargo nextest run -p fabro-automation
+cargo nextest run -p fabro-api automation_round_trip
+cargo nextest run -p fabro-server automations
+```
+
+Expected: all focused Rust checks pass.
+
+- [ ] Run the focused frontend checks:
+
+```bash
+cd lib/packages/fabro-api-client && bun run typecheck
+cd apps/fabro-web && bun run typecheck
+```
+
+Expected: generated client and web app typecheck.
+
+- [ ] Search for leftover master-gate references:
+
+```bash
+rg -n "automation\\.enabled|enabled_api_trigger\\(\\).*automation|Automation is disabled|automation is disabled|\\\"enabled\\\": true" \
+ lib/crates/fabro-automation \
+ lib/crates/fabro-server/tests/it/api/automations.rs \
+ lib/crates/fabro-server/src/server/handler/automations.rs \
+ lib/crates/fabro-api/tests/automation_round_trip.rs \
+ apps/fabro-web/app/components/automation-form.tsx \
+ apps/fabro-web/app/routes/automations-new.tsx \
+ apps/fabro-web/app/routes/automations-edit.tsx \
+ apps/fabro-web/app/routes/automation-detail.tsx \
+ apps/fabro-web/app/routes/automations.tsx
+```
+
+Expected: no hits for the removed top-level automation enabled gate. Hits for trigger-level `enabled` are acceptable when they are clearly on `AutomationTrigger`, `ApiTrigger`, or `ScheduleTrigger`.
+
+- [ ] Optional formatting check:
+
+```bash
+cargo +nightly-2026-04-14 fmt --check --all
+```
+
+Expected: Rust formatting passes. If it fails on touched Rust files, run `cargo +nightly-2026-04-14 fmt --all` and re-run the focused Rust checks.
+
+## Explicit Assumptions
+
+- No migration or compatibility parser is added for top-level automation `enabled`.
+- Existing automation TOML files that still contain top-level `enabled` are obsolete and may fail startup until manually edited.
+- Trigger-level `enabled` remains public API and persisted TOML.
+- API/manual run creation remains controlled by the enabled `api` trigger only.
+- Schedule execution, cron semantics, and schedule-trigger preservation are outside this cleanup.
+
+
+## Completed stages
+- **toolchain**: succeeded
+ - Script: `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`
+ - Output:
+ ```
+ cargo 1.95.0 (f2d3ce0bd 2026-03-21)
+ ```
+- **preflight_compile**: succeeded
+ - Script: `cargo check -q --workspace 2>&1`
+ - Output: (empty)
+- **preflight_lint**: succeeded
+ - Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1`
+ - Output: (empty)
+- **implement**: succeeded
+ - Model: gpt-5.5, 1.3m tokens in / 17.0k out
+
+
+# Simplify: Code Review and Cleanup
+
+Review changes vs. origin for reuse, quality, and efficiency. Fix any issues found.
+
+## Phase 1: Identify Changes
+
+Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
+
+## Phase 2: Launch Three Review Agents in Parallel
+
+Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
+
+### Agent 1: Code Reuse Review
+
+For each change:
+
+1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
+2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.
+3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
+
+Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.
+
+### Agent 2: Code Quality Review
+
+Review the same changes for hacky patterns:
+
+1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
+2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones
+3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction
+4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
+5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
+
+Note: This is a greenfield app, so be aggressive in optimizing quality.
+
+### Agent 3: Efficiency Review
+
+Review the same changes for efficiency:
+
+1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
+2. Missed concurrency: independent operations run sequentially when they could run in parallel
+3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths
+4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
+5. Memory: unbounded data structures, missing cleanup, event listener leaks
+6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one
+
+## Phase 3: Fix Issues
+
+Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.
+
+When done, briefly summarize what was fixed (or confirm the code was already clean).
\ No newline at end of file
diff --git a/stages/006-simplify_opus@1/provider_used.json b/stages/006-simplify_opus@1/provider_used.json
new file mode 100644
index 000000000..672cc50e5
--- /dev/null
+++ b/stages/006-simplify_opus@1/provider_used.json
@@ -0,0 +1,5 @@
+{
+ "mode": "agent",
+ "provider": "anthropic",
+ "model": "claude-opus-4-7"
+}
\ No newline at end of file
diff --git a/stages/006-simplify_opus@1/response.md b/stages/006-simplify_opus@1/response.md
new file mode 100644
index 000000000..20fbf6d5c
--- /dev/null
+++ b/stages/006-simplify_opus@1/response.md
@@ -0,0 +1,21 @@
+All checks pass: `fabro-automation`, `fabro-api automation_round_trip`, `fabro-server` automation integration tests, web typecheck, api-client typecheck, `cargo fmt --check`, and `clippy -D warnings`.
+
+## Summary of fixes applied
+
+**Reuse / quality (web):**
+- Created `apps/fabro-web/app/lib/automation.ts` with `findApiTrigger`, `findScheduleTrigger`, and `hasEnabledApiTrigger` — replaces five inline `triggers.find((t) => t.type === ...)` call sites across `automations.tsx`, `automation-detail.tsx`, and `automation-form.tsx`.
+- Collapsed the `AutomationCard` `disabled` + `menuDisabled` props into a single `busy` prop; the run-button `disabled` is derived inside the card from `busy || !automation.apiEnabled`.
+- Removed the redundant `!automation.apiEnabled` guard in `runAutomation` (already enforced by the button's `disabled`).
+- `automation-detail.tsx` now uses the local `canRun` for the tooltip instead of re-reading `apiTrigger?.enabled`.
+
+**Efficiency (web):**
+- Hoisted `query.toLowerCase()` out of the filter callback in `automations.tsx`.
+
+**Quality (Rust):**
+- Renamed `persisted_toml_rejects_legacy_top_level_enabled` → `persisted_toml_rejects_unknown_top_level_fields` (greenfield app — no "legacy" migration story).
+- Trimmed the redundant doc-comment on `Automation::enabled_api_trigger`.
+
+**Skipped (with reasons):**
+- Renaming the error code `automation_api_trigger_disabled` — the plan explicitly requires keeping it.
+- Restoring the "at least one trigger enabled" form check — the plan explicitly removed it.
+- Memoizing `mapAutomations`/`filtered` and lazy `CronParser` — micro-opts on bounded inputs, not worth the complexity.
\ No newline at end of file