commit 7f7a7e5bee541a95658d73ac982cf573cadae149 Author: Fabro Date: Tue Jul 7 13:16:51 2026 +0000 init run ⚒️ Generated with [Fabro](https://fabro.sh) diff --git a/graph.fabro b/graph.fabro new file mode 100644 index 000000000..88c31b4cb --- /dev/null +++ b/graph.fabro @@ -0,0 +1,35 @@ +digraph ImplementPlan { + graph [ + goal="Implement and simplify", + model_stylesheet=" + * { model: claude-opus-4-8; } + " + ] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + toolchain [label="Toolchain", shape=parallelogram, 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", max_retries=0] + preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0] + preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0] + fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3] + implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"] + simplify_fable [label="Simplify (Fable)", prompt="@prompts/simplify.md", model="claude-fable-5", reasoning_effort="xhigh"] + simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"] + verify [label="Verify", shape=parallelogram, timeout="1800s", script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"] + fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3] + + start -> toolchain + toolchain -> preflight_compile [condition="outcome=succeeded"] + toolchain -> exit + preflight_compile -> preflight_lint [condition="outcome=succeeded"] + preflight_compile -> exit + preflight_lint -> implement [condition="outcome=succeeded"] + preflight_lint -> fix_lints + fix_lints -> preflight_lint + implement -> simplify_fable -> simplify_gpt -> verify + verify -> exit [condition="outcome=succeeded"] + verify -> fixup + fixup -> verify +} diff --git a/run.json b/run.json new file mode 100644 index 000000000..59ecb9430 --- /dev/null +++ b/run.json @@ -0,0 +1,528 @@ +{ + "title": "automation \"Workflow slug\" field must use kebab-case (dashes), not snake_case", + "spec": { + "run_id": "01KWYBJBY8F2W85RSH0ZH0F090", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": { + "type": "inline", + "value": "# Plan: automation \"Workflow slug\" field must use kebab-case (dashes), not snake_case\n\n## Summary\n\nThe web UI's New/Edit Automation form silently rewrites the **Workflow slug**\nfield to snake_case on every keystroke (`patch-cves` → `patch_cves`). The rest\nof the system uses kebab-case (dashes) for workflow slugs, and workflow\ndirectories on disk are dash-separated (`.fabro/workflows/patch-cves/`). So a\nsnake_cased selector points at a non-existent directory, and scheduled\nautomations targeting any multi-word workflow **silently never fire**.\n\nFix: change the Workflow slug field (and the \"create from run\" fallback) to use\nthe form's existing `kebabify()` helper instead of `snakeify()`, update the\nfield copy, remove the now-unused `snakeify()`, and update the test. This is a\n**frontend-only** change; the backend already accepts dashes and needs no edit.\n\n## Root cause (verified, with file:line)\n\nAll in `apps/fabro-web/app/components/automation-form.tsx`:\n\n- `snakeify()` (lines 127-133) — `.toLowerCase().replace(/[^a-z0-9_]+/g, \"_\")…`\n converts any dash to an underscore.\n- The Workflow slug input (lines 302-311) applies it on every keystroke:\n ```tsx\n // line 303 help text:\n help=\"Snake-case identifier used in the workflow file name (e.g. fix_build.fabro).\"\n // line 310:\n onChange={(e) => patch({ workflow: snakeify(e.target.value) })}\n // line 311:\n placeholder=\"fix_build\"\n ```\n- The \"create automation from run\" prefill (line 89) also snake-cases the\n fallback name:\n ```tsx\n workflow: run.workflow.slug?.trim() || snakeify(workflowName),\n ```\n- `kebabify()` (lines 119-126) already exists and is used for the automation's\n own id field (lines 85, 214, 220). It is the correct normalizer to reuse.\n\n`snakeify` is referenced in exactly two places (lines 89 and 310); once both\nmove to `kebabify`, the `snakeify` definition (lines 127-133) is dead code.\n\n## Why kebab-case is correct (justification)\n\nThe snake_case rule is a UI-only outlier. Every other layer treats workflow\nslugs as kebab-case:\n\n- Backend `validate_workflow_selector` (`lib/crates/fabro-automation/src/model.rs:366`)\n allows both `-` and `_`, so it will accept a dashed value with no change.\n- Real workflow directories are dash-separated: `patch-cves`, `gh-triage`,\n `implement-plan`, `card-game`, `implement-issue`, etc. Only single-word ones\n (`smoke`, `hello`) survive `snakeify` unchanged.\n- `workflow_slug_from_path` (`lib/crates/fabro-config/src/project.rs:111`)\n preserves dashes, and `lib/crates/fabro-workflow/src/run_options.rs:31`\n documents the slug as the **literal workflow directory name**.\n- `AutomationId` (`lib/crates/fabro-automation/src/id.rs`) actually **forbids\n underscores** — dashes only.\n\nSo kebab is the established convention; the form is simply wrong. No backend\nchange is needed or wanted.\n\n### Two distinct names — do not conflate (important)\n\nA workflow has two separate identifiers, and only one is relevant here:\n\n- **Workflow slug** — derived from the *directory* name, kebab-case\n (e.g. `patch-cves` from `.fabro/workflows/patch-cves/`). This is what the\n automation `workflow` selector resolves against, and what this fix must\n produce. (Seen as `workflow_slug: \"patch-cves\"` on runs.)\n- **Graph name** — the identifier in the DOT source, `digraph PatchCves { … }`,\n typically CamelCase. This is internal graph identity (`name: \"PatchCves\"` on\n runs); it is **not** used to resolve the automation target.\n\nThe Workflow slug field must be the kebab directory slug. Do **not** change it\ntoward the CamelCase graph name, and do not touch graph names anywhere. This\nalso explains the original mistake: the author treated the file/dir name as\nsnake_case when it is actually kebab-case.\n\n## Failure mode this fixes (for context)\n\n1. User types `patch-cves` → form stores `patch_cves`.\n2. Server stores it verbatim (no normalization; validator accepts underscores).\n3. On each cron tick, `automation_materializer` resolves the selector literally\n → looks for `.fabro/workflows/patch_cves/` → `WorkflowNotFound`\n (`lib/crates/fabro-server/src/automation_materializer.rs` ~219-229).\n4. `lib/crates/fabro-server/src/server/automation_scheduler.rs:221-239` logs\n `\"Failed to materialize scheduled automation run\"` and returns — no run\n created, waits for the next tick. The automation appears to do nothing.\n\n## Where it was introduced (provenance, informational)\n\n- `snakeify` + the Workflow slug field + \"Snake-case identifier…\" help text:\n commit `2e85a1ec4` \"Add New Automation form and refresh Secrets form layout\"\n (2026-05-24). Present from the form's first draft; no commit message explains\n why the workflow field is snake-cased.\n- Carried into the shared component: `87516c25c` (2026-05-28).\n- Prefill fallback snake-cases the name: `a65473f21` / PR #454 (2026-05-29).\n- Backend validator that (correctly) allows dashes: `e13de9faa` / PR #428.\n\n## Implementation steps\n\nFrontend only. Use red/green TDD.\n\n1. **Update the test first** — `apps/fabro-web/app/routes/automations-new.test.tsx`:\n - The assertion at ~line 278 currently expects the snake output\n `expect(fieldValue(renderer, \"Workflow slug\")).toBe(\"fix_ci\")`. Change the\n expected value to the kebab form (`\"fix-ci\"`) to match the new behavior.\n - Add a regression assertion: typing `patch-cves` into the \"Workflow slug\"\n field leaves it as `patch-cves` (dashes preserved, NOT converted to\n underscores). Also confirm `Patch CVEs` → `patch-cves`.\n - Run the test and confirm it fails against current code (red).\n\n2. **Fix the field** — `apps/fabro-web/app/components/automation-form.tsx`:\n - Line 310: `onChange={(e) => patch({ workflow: snakeify(e.target.value) })}`\n → use `kebabify(e.target.value)`.\n - Line 89: `run.workflow.slug?.trim() || snakeify(workflowName)`\n → `run.workflow.slug?.trim() || kebabify(workflowName)`.\n - Line 303 help text → describe dash-separated, e.g.\n `\"Dash-separated identifier matching the workflow directory name (e.g. patch-cves).\"`\n - Line 311 placeholder `\"fix_build\"` → `\"patch-cves\"` (or `\"fix-build\"`).\n - Remove the now-unused `snakeify()` function (lines 127-133). Keep\n `kebabify()` (still used for the id field and now the workflow field).\n - Verify no references remain: `grep -rn \"snakeify\" apps/fabro-web/app`\n should return nothing.\n\n3. **Verify** in `apps/fabro-web`:\n - `bun run typecheck` passes.\n - `bun test` passes (the updated + new assertions go green).\n\n## Files to touch\n\n- `apps/fabro-web/app/components/automation-form.tsx` — the fix (lines 89, 303,\n 310, 311; remove 127-133).\n- `apps/fabro-web/app/routes/automations-new.test.tsx` — update the snake\n assertion (~278) and add the dash-preservation regression test.\n- No other files. `automations-new.tsx` / `automations-edit.tsx` submit\n `values.workflow.trim()` unchanged and need no edit.\n\n## Acceptance criteria\n\n- Typing `patch-cves` in \"Workflow slug\" yields `patch-cves` (not `patch_cves`).\n- Typing `Patch CVEs` yields `patch-cves`.\n- The \"create from run\" prefill uses the run's dashed slug when present and\n kebab-cases the fallback name.\n- Help text and placeholder reflect dash-separated slugs.\n- No `snakeify` references remain (dead code removed).\n- `bun run typecheck` and `bun test` pass in `apps/fabro-web`.\n- No backend/Rust changes.\n\n## Out of scope\n\n- **Backend changes.** `validate_workflow_selector` already accepts dashes;\n leave it. Do not add snake↔dash conversion on the server.\n- **`kebabify()` itself** — it is correct for the id field; do not modify it.\n- **Migrating already-created automations** whose stored `workflow` is already\n snake_cased. That is an operational fix (edit the automation via\n `PUT /api/v1/automations/{id}` with the corrected dashed `workflow`), not part\n of this code change.\n- The unrelated sandbox `glob` consistency fix (tracked in a separate plan).\n\n## Design decision (stated so it isn't re-litigated)\n\nKeep normalizing the field — do not make it freeform. Normalize to **kebab-case**\n(reusing the existing `kebabify`) so it matches the id field's behavior, the\nbackend, and the on-disk workflow directory convention.\n\n## Conventions to follow\n\n- Match the surrounding TypeScript/React style in `apps/fabro-web`.\n- This is an `onChange`/string-helper change only — no new React effects (the\n repo avoids direct `useEffect`; none is needed here).\n- Keep the change minimal and focused; do not refactor unrelated form code.\n" + }, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": "anthropic", + "name": "claude-sonnet-4-6", + "fallbacks": [], + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "commands": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false + }, + "clone": { + "enabled": true + }, + "run_branch": { + "enabled": true, + "push": true + }, + "meta_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "fabro-dev", + "provider": "daytona", + "image": { + "docker": null, + "dockerfile": { + "type": "inline", + "value": "FROM ubuntu:24.04\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n curl git ripgrep ca-certificates build-essential pkg-config libssl-dev unzip python3 \\\n xvfb xfce4 xfce4-terminal x11vnc novnc dbus-x11 \\\n libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 libxss1 libxtst6 libxi6 \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install real Chromium (not the snap stub) via xtradeb PPA\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n software-properties-common curl gnupg \\\n && add-apt-repository -y ppa:xtradeb/apps \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends chromium \\\n && rm -rf /var/lib/apt/lists/*\n\n# Wrapper: Chromium needs --no-sandbox when running as root in a container,\n# and --disable-dev-shm-usage avoids crashes from small /dev/shm\nRUN printf '#!/bin/bash\\nexec /usr/bin/chromium --no-sandbox --disable-dev-shm-usage \"$@\"\\n' \\\n > /usr/local/bin/chromium-wrapper \\\n && chmod +x /usr/local/bin/chromium-wrapper\n\n# Make the wrapper the default in the system .desktop file and via alternatives\nRUN sed -i 's|^Exec=.*|Exec=/usr/local/bin/chromium-wrapper %U|' \\\n /usr/share/applications/chromium.desktop \\\n && update-alternatives --install /usr/bin/x-www-browser x-www-browser \\\n /usr/local/bin/chromium-wrapper 100\n\n# Tell XFCE's exo-open that Chromium is the WebBrowser helper (system-wide)\nRUN mkdir -p /etc/xdg/xfce4 /usr/share/xfce4/helpers \\\n && printf 'WebBrowser=custom-WebBrowser\\n' > /etc/xdg/xfce4/helpers.rc \\\n && printf '[Desktop Entry]\\n\\\nVersion=1.0\\n\\\nType=X-XFCE-Helper\\n\\\nName=Chromium\\n\\\nIcon=chromium\\n\\\nX-XFCE-Category=WebBrowser\\n\\\nX-XFCE-CommandsWithParameter=/usr/local/bin/chromium-wrapper \"%%s\"\\n\\\nX-XFCE-Commands=/usr/local/bin/chromium-wrapper\\n' \\\n > /usr/share/xfce4/helpers/custom-WebBrowser.desktop\n\n# GitHub CLI\nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\\n | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\\n && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" \\\n | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\\n && apt-get update && apt-get install -y --no-install-recommends gh \\\n && rm -rf /var/lib/apt/lists/*\n\n# Rust\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN rustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt\nRUN cargo install cargo-nextest --locked\nENV CARGO_INCREMENTAL=0\n\n# Bun\nRUN curl -fsSL https://bun.sh/install | bash\nENV PATH=\"/root/.bun/bin:${PATH}\"\n\nWORKDIR /root\n" + } + }, + "resources": { + "cpu": 8, + "memory": "16GB", + "disk": "20GB" + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": "30m" + }, + "labels": { + "repo": "fabro-sh/fabro" + }, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "permissions": null, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": { + "enabled": true, + "draft": false, + "auto_merge": false, + "merge_strategy": "squash" + }, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "ImplementPlan", + "nodes": { + "simplify_fable": { + "id": "simplify_fable", + "attrs": { + "label": { + "String": "Simplify (Fable)" + }, + "model": { + "String": "claude-fable-5" + }, + "reasoning_effort": { + "String": "xhigh" + }, + "prompt": { + "String": "# Simplify: Code Review and Cleanup\n\nReview all changes for reuse, quality, and efficiency. Fix any issues found. Feel free to use any sub agents you need.\n\n## Phase 1: Identify Changes\n\nRun 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. (You may already have the changes in context, if so, feel free to skip this part)\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse 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.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. 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.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. 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.\n\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\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 to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait 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.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean).\n" + }, + "provider": { + "String": "anthropic" + } + } + }, + "fixup": { + "id": "fixup", + "attrs": { + "label": { + "String": "Fixup" + }, + "model": { + "String": "claude-opus-4-8" + }, + "prompt": { + "String": "The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures." + }, + "max_visits": { + "Integer": 3 + }, + "provider": { + "String": "anthropic" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + }, + "label": { + "String": "Exit" + }, + "model": { + "String": "claude-opus-4-8" + }, + "provider": { + "String": "anthropic" + } + } + }, + "implement": { + "id": "implement", + "attrs": { + "provider": { + "String": "openai" + }, + "label": { + "String": "Implement" + }, + "reasoning_effort": { + "String": "xhigh" + }, + "prompt": { + "String": "Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD." + }, + "model": { + "String": "gpt-5.5" + } + } + }, + "simplify_gpt": { + "id": "simplify_gpt", + "attrs": { + "model": { + "String": "gpt-5.5" + }, + "label": { + "String": "Simplify (GPT-55)" + }, + "prompt": { + "String": "# Simplify: Code Review and Cleanup\n\nReview all changes for reuse, quality, and efficiency. Fix any issues found. Feel free to use any sub agents you need.\n\n## Phase 1: Identify Changes\n\nRun 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. (You may already have the changes in context, if so, feel free to skip this part)\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse 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.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. 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.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. 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.\n\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\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 to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait 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.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean).\n" + }, + "provider": { + "String": "openai" + } + } + }, + "start": { + "id": "start", + "attrs": { + "provider": { + "String": "anthropic" + }, + "model": { + "String": "claude-opus-4-8" + }, + "label": { + "String": "Start" + }, + "shape": { + "String": "Mdiamond" + } + } + }, + "verify": { + "id": "verify", + "attrs": { + "goal_gate": { + "Boolean": true + }, + "provider": { + "String": "anthropic" + }, + "model": { + "String": "claude-opus-4-8" + }, + "timeout": { + "Duration": { + "secs": 1800, + "nanos": 0 + } + }, + "shape": { + "String": "parallelogram" + }, + "label": { + "String": "Verify" + }, + "retry_target": { + "String": "fixup" + }, + "script": { + "String": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1" + } + } + }, + "preflight_compile": { + "id": "preflight_compile", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "provider": { + "String": "anthropic" + }, + "max_retries": { + "Integer": 0 + }, + "model": { + "String": "claude-opus-4-8" + }, + "script": { + "String": "cargo check -q --workspace 2>&1" + }, + "label": { + "String": "Preflight Compile" + } + } + }, + "preflight_lint": { + "id": "preflight_lint", + "attrs": { + "model": { + "String": "claude-opus-4-8" + }, + "shape": { + "String": "parallelogram" + }, + "label": { + "String": "Preflight Lint" + }, + "provider": { + "String": "anthropic" + }, + "script": { + "String": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1" + }, + "max_retries": { + "Integer": 0 + } + } + }, + "toolchain": { + "id": "toolchain", + "attrs": { + "model": { + "String": "claude-opus-4-8" + }, + "max_retries": { + "Integer": 0 + }, + "provider": { + "String": "anthropic" + }, + "label": { + "String": "Toolchain" + }, + "script": { + "String": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1" + }, + "shape": { + "String": "parallelogram" + } + } + }, + "fix_lints": { + "id": "fix_lints", + "attrs": { + "model": { + "String": "claude-opus-4-8" + }, + "label": { + "String": "Fix Lints" + }, + "max_visits": { + "Integer": 3 + }, + "prompt": { + "String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings." + }, + "provider": { + "String": "anthropic" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "toolchain", + "attrs": {} + }, + { + "from": "toolchain", + "to": "preflight_compile", + "attrs": { + "condition": { + "String": "outcome=succeeded" + } + } + }, + { + "from": "toolchain", + "to": "exit", + "attrs": {} + }, + { + "from": "preflight_compile", + "to": "preflight_lint", + "attrs": { + "condition": { + "String": "outcome=succeeded" + } + } + }, + { + "from": "preflight_compile", + "to": "exit", + "attrs": {} + }, + { + "from": "preflight_lint", + "to": "implement", + "attrs": { + "condition": { + "String": "outcome=succeeded" + } + } + }, + { + "from": "preflight_lint", + "to": "fix_lints", + "attrs": {} + }, + { + "from": "fix_lints", + "to": "preflight_lint", + "attrs": {} + }, + { + "from": "implement", + "to": "simplify_fable", + "attrs": {} + }, + { + "from": "simplify_fable", + "to": "simplify_gpt", + "attrs": {} + }, + { + "from": "simplify_gpt", + "to": "verify", + "attrs": {} + }, + { + "from": "verify", + "to": "exit", + "attrs": { + "condition": { + "String": "outcome=succeeded" + } + } + }, + { + "from": "verify", + "to": "fixup", + "attrs": {} + }, + { + "from": "fixup", + "to": "verify", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "# Plan: automation \"Workflow slug\" field must use kebab-case (dashes), not snake_case\n\n## Summary\n\nThe web UI's New/Edit Automation form silently rewrites the **Workflow slug**\nfield to snake_case on every keystroke (`patch-cves` → `patch_cves`). The rest\nof the system uses kebab-case (dashes) for workflow slugs, and workflow\ndirectories on disk are dash-separated (`.fabro/workflows/patch-cves/`). So a\nsnake_cased selector points at a non-existent directory, and scheduled\nautomations targeting any multi-word workflow **silently never fire**.\n\nFix: change the Workflow slug field (and the \"create from run\" fallback) to use\nthe form's existing `kebabify()` helper instead of `snakeify()`, update the\nfield copy, remove the now-unused `snakeify()`, and update the test. This is a\n**frontend-only** change; the backend already accepts dashes and needs no edit.\n\n## Root cause (verified, with file:line)\n\nAll in `apps/fabro-web/app/components/automation-form.tsx`:\n\n- `snakeify()` (lines 127-133) — `.toLowerCase().replace(/[^a-z0-9_]+/g, \"_\")…`\n converts any dash to an underscore.\n- The Workflow slug input (lines 302-311) applies it on every keystroke:\n ```tsx\n // line 303 help text:\n help=\"Snake-case identifier used in the workflow file name (e.g. fix_build.fabro).\"\n // line 310:\n onChange={(e) => patch({ workflow: snakeify(e.target.value) })}\n // line 311:\n placeholder=\"fix_build\"\n ```\n- The \"create automation from run\" prefill (line 89) also snake-cases the\n fallback name:\n ```tsx\n workflow: run.workflow.slug?.trim() || snakeify(workflowName),\n ```\n- `kebabify()` (lines 119-126) already exists and is used for the automation's\n own id field (lines 85, 214, 220). It is the correct normalizer to reuse.\n\n`snakeify` is referenced in exactly two places (lines 89 and 310); once both\nmove to `kebabify`, the `snakeify` definition (lines 127-133) is dead code.\n\n## Why kebab-case is correct (justification)\n\nThe snake_case rule is a UI-only outlier. Every other layer treats workflow\nslugs as kebab-case:\n\n- Backend `validate_workflow_selector` (`lib/crates/fabro-automation/src/model.rs:366`)\n allows both `-` and `_`, so it will accept a dashed value with no change.\n- Real workflow directories are dash-separated: `patch-cves`, `gh-triage`,\n `implement-plan`, `card-game`, `implement-issue`, etc. Only single-word ones\n (`smoke`, `hello`) survive `snakeify` unchanged.\n- `workflow_slug_from_path` (`lib/crates/fabro-config/src/project.rs:111`)\n preserves dashes, and `lib/crates/fabro-workflow/src/run_options.rs:31`\n documents the slug as the **literal workflow directory name**.\n- `AutomationId` (`lib/crates/fabro-automation/src/id.rs`) actually **forbids\n underscores** — dashes only.\n\nSo kebab is the established convention; the form is simply wrong. No backend\nchange is needed or wanted.\n\n### Two distinct names — do not conflate (important)\n\nA workflow has two separate identifiers, and only one is relevant here:\n\n- **Workflow slug** — derived from the *directory* name, kebab-case\n (e.g. `patch-cves` from `.fabro/workflows/patch-cves/`). This is what the\n automation `workflow` selector resolves against, and what this fix must\n produce. (Seen as `workflow_slug: \"patch-cves\"` on runs.)\n- **Graph name** — the identifier in the DOT source, `digraph PatchCves { … }`,\n typically CamelCase. This is internal graph identity (`name: \"PatchCves\"` on\n runs); it is **not** used to resolve the automation target.\n\nThe Workflow slug field must be the kebab directory slug. Do **not** change it\ntoward the CamelCase graph name, and do not touch graph names anywhere. This\nalso explains the original mistake: the author treated the file/dir name as\nsnake_case when it is actually kebab-case.\n\n## Failure mode this fixes (for context)\n\n1. User types `patch-cves` → form stores `patch_cves`.\n2. Server stores it verbatim (no normalization; validator accepts underscores).\n3. On each cron tick, `automation_materializer` resolves the selector literally\n → looks for `.fabro/workflows/patch_cves/` → `WorkflowNotFound`\n (`lib/crates/fabro-server/src/automation_materializer.rs` ~219-229).\n4. `lib/crates/fabro-server/src/server/automation_scheduler.rs:221-239` logs\n `\"Failed to materialize scheduled automation run\"` and returns — no run\n created, waits for the next tick. The automation appears to do nothing.\n\n## Where it was introduced (provenance, informational)\n\n- `snakeify` + the Workflow slug field + \"Snake-case identifier…\" help text:\n commit `2e85a1ec4` \"Add New Automation form and refresh Secrets form layout\"\n (2026-05-24). Present from the form's first draft; no commit message explains\n why the workflow field is snake-cased.\n- Carried into the shared component: `87516c25c` (2026-05-28).\n- Prefill fallback snake-cases the name: `a65473f21` / PR #454 (2026-05-29).\n- Backend validator that (correctly) allows dashes: `e13de9faa` / PR #428.\n\n## Implementation steps\n\nFrontend only. Use red/green TDD.\n\n1. **Update the test first** — `apps/fabro-web/app/routes/automations-new.test.tsx`:\n - The assertion at ~line 278 currently expects the snake output\n `expect(fieldValue(renderer, \"Workflow slug\")).toBe(\"fix_ci\")`. Change the\n expected value to the kebab form (`\"fix-ci\"`) to match the new behavior.\n - Add a regression assertion: typing `patch-cves` into the \"Workflow slug\"\n field leaves it as `patch-cves` (dashes preserved, NOT converted to\n underscores). Also confirm `Patch CVEs` → `patch-cves`.\n - Run the test and confirm it fails against current code (red).\n\n2. **Fix the field** — `apps/fabro-web/app/components/automation-form.tsx`:\n - Line 310: `onChange={(e) => patch({ workflow: snakeify(e.target.value) })}`\n → use `kebabify(e.target.value)`.\n - Line 89: `run.workflow.slug?.trim() || snakeify(workflowName)`\n → `run.workflow.slug?.trim() || kebabify(workflowName)`.\n - Line 303 help text → describe dash-separated, e.g.\n `\"Dash-separated identifier matching the workflow directory name (e.g. patch-cves).\"`\n - Line 311 placeholder `\"fix_build\"` → `\"patch-cves\"` (or `\"fix-build\"`).\n - Remove the now-unused `snakeify()` function (lines 127-133). Keep\n `kebabify()` (still used for the id field and now the workflow field).\n - Verify no references remain: `grep -rn \"snakeify\" apps/fabro-web/app`\n should return nothing.\n\n3. **Verify** in `apps/fabro-web`:\n - `bun run typecheck` passes.\n - `bun test` passes (the updated + new assertions go green).\n\n## Files to touch\n\n- `apps/fabro-web/app/components/automation-form.tsx` — the fix (lines 89, 303,\n 310, 311; remove 127-133).\n- `apps/fabro-web/app/routes/automations-new.test.tsx` — update the snake\n assertion (~278) and add the dash-preservation regression test.\n- No other files. `automations-new.tsx` / `automations-edit.tsx` submit\n `values.workflow.trim()` unchanged and need no edit.\n\n## Acceptance criteria\n\n- Typing `patch-cves` in \"Workflow slug\" yields `patch-cves` (not `patch_cves`).\n- Typing `Patch CVEs` yields `patch-cves`.\n- The \"create from run\" prefill uses the run's dashed slug when present and\n kebab-cases the fallback name.\n- Help text and placeholder reflect dash-separated slugs.\n- No `snakeify` references remain (dead code removed).\n- `bun run typecheck` and `bun test` pass in `apps/fabro-web`.\n- No backend/Rust changes.\n\n## Out of scope\n\n- **Backend changes.** `validate_workflow_selector` already accepts dashes;\n leave it. Do not add snake↔dash conversion on the server.\n- **`kebabify()` itself** — it is correct for the id field; do not modify it.\n- **Migrating already-created automations** whose stored `workflow` is already\n snake_cased. That is an operational fix (edit the automation via\n `PUT /api/v1/automations/{id}` with the corrected dashed `workflow`), not part\n of this code change.\n- The unrelated sandbox `glob` consistency fix (tracked in a separate plan).\n\n## Design decision (stated so it isn't re-litigated)\n\nKeep normalizing the field — do not make it freeform. Normalize to **kebab-case**\n(reusing the existing `kebabify`) so it matches the id field's behavior, the\nbackend, and the on-disk workflow directory convention.\n\n## Conventions to follow\n\n- Match the surrounding TypeScript/React style in `apps/fabro-web`.\n- This is an `onChange`/string-helper change only — no new React effects (the\n repo avoids direct `useEffect`; none is needed here).\n- Keep the change minimal and focused; do not refactor unrelated form code.\n" + }, + "rankdir": { + "String": "LR" + }, + "model_stylesheet": { + "String": "\n * { model: claude-opus-4-8; }\n " + } + } + }, + "graph_source": "digraph ImplementPlan {\n graph [\n goal=\"Implement and simplify\",\n model_stylesheet=\"\n * { model: claude-opus-4-8; }\n \"\n ]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n toolchain [label=\"Toolchain\", shape=parallelogram, 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\", max_retries=0]\n preflight_compile [label=\"Preflight Compile\", shape=parallelogram, script=\"cargo check -q --workspace 2>&1\", max_retries=0]\n preflight_lint [label=\"Preflight Lint\", shape=parallelogram, script=\"cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1\", max_retries=0]\n fix_lints [label=\"Fix Lints\", prompt=\"The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.\", max_visits=3]\n implement [label=\"Implement\", prompt=\"Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.\", model=\"gpt-55\", reasoning_effort=\"xhigh\"]\n simplify_fable [label=\"Simplify (Fable)\", prompt=\"@prompts/simplify.md\", model=\"claude-fable-5\", reasoning_effort=\"xhigh\"]\n simplify_gpt [label=\"Simplify (GPT-55)\", prompt=\"@prompts/simplify.md\", model=\"gpt-55\"]\n verify [label=\"Verify\", shape=parallelogram, timeout=\"1800s\", script=\"git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\\\"disabled\\\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1\", goal_gate=true, retry_target=\"fixup\"]\n fixup [label=\"Fixup\", prompt=\"The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.\", max_visits=3]\n\n start -> toolchain\n toolchain -> preflight_compile [condition=\"outcome=succeeded\"]\n toolchain -> exit\n preflight_compile -> preflight_lint [condition=\"outcome=succeeded\"]\n preflight_compile -> exit\n preflight_lint -> implement [condition=\"outcome=succeeded\"]\n preflight_lint -> fix_lints\n fix_lints -> preflight_lint\n implement -> simplify_fable -> simplify_gpt -> verify\n verify -> exit [condition=\"outcome=succeeded\"]\n verify -> fixup\n fixup -> verify\n}\n", + "workflow_slug": "implement-plan", + "source_directory": "/Users/swerner/Development/os/fabro-main/fabro", + "provenance": { + "server": { + "version": "0.278.0-nightly.0" + }, + "client": { + "user_agent": "fabro-cli/0.267.0-nightly.0", + "name": "fabro-cli", + "version": "0.267.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "https://github.com", + "subject": "138379" + }, + "login": "swerner", + "auth_method": "github", + "avatar_url": "https://avatars.githubusercontent.com/u/138379?v=4" + } + }, + "manifest_blob": "7b9a224453bb964b92cfd8cc1e0429115924fbc30c17bfc7752584c730ba94cc", + "definition_blob": "6b96c7e49a78bbae043d5e10b0ecc33b38d00daf26b3199934295061964b2871", + "git": { + "origin_url": "https://github.com/fabro-sh/fabro", + "branch": "main", + "sha": "8c7d5dc7d0375fb8e7d1d3f47e7e6639979d8517", + "dirty": "dirty", + "push_outcome": { + "type": "not_attempted" + } + } + }, + "web_url": "https://fabro-testing.walleye-rainbow.ts.net/runs/01KWYBJBY8F2W85RSH0ZH0F090", + "start": null, + "status": { + "kind": "starting" + }, + "status_updated_at": "2026-07-07T13:16:37.291386799Z", + "last_event_at": "2026-07-07T13:16:51.377919245Z", + "pending_control": null, + "checkpoints": [], + "conclusion": null, + "sandbox": { + "kind": "ready", + "plan": { + "provider": "daytona" + }, + "instance": { + "provider": "daytona", + "snapshot": "fabro-fdb28dec-1233-892c-b9d7-9f88f8353e7a", + "runtime": { + "id": "fabro-01KWYBJBY8F2W85RSH0ZH0F090", + "working_directory": "/home/daytona/workspace/fabro", + "repo_cloned": true, + "clone_origin_url": "https://github.com/fabro-sh/fabro", + "clone_branch": "main", + "workspace_root": "/home/daytona/workspace", + "repos_root": "/home/daytona/repos", + "primary_repo_path": "/home/daytona/repos/fabro-sh/fabro", + "primary_repo_link": "/home/daytona/workspace/fabro" + } + } + }, + "pull_request": null, + "superseded_by": null, + "pending_interviews": {}, + "stages": {} +} \ No newline at end of file