Commit graph

6 commits

Author SHA1 Message Date
fabro-sh-fabro[bot]
09d1a6036e
Fix workflow slug field to use kebab-case instead of snake_case (#556)
Multi-word workflow slugs typed into the New/Edit Automation form were
being silently converted to snake_case (e.g. `patch-cves` →
`patch_cves`), causing scheduled automations to resolve against a
non-existent directory and **silently never fire**.

## What changed

- `automation-form.tsx`: `onChange` for the Workflow slug field now
calls `kebabify()` instead of the removed `snakeify()`. The "create from
run" fallback prefill is updated the same way. Help text and placeholder
are updated to reflect dash-separated slugs.
- `snakeify()` is removed entirely (was only used in these two spots).
- `kebabify()` is unexported (it was `export function`; it's now only
used within the same file).
- `automations-new.test.tsx`: updates the pre-populate assertion from
`"fix_ci"` → `"fix-ci"`, adds a regression test that dashes are
preserved and `"Patch CVEs"` → `"patch-cves"`, and adds a unit test for
the `automationFormValuesFromRun` kebab fallback.

## Why kebab-case is correct

Workflow slugs are derived from on-disk directory names
(`.fabro/workflows/patch-cves/`), which are dash-separated by
convention. The backend validator already accepts dashes; `AutomationId`
actually forbids underscores. The snake_case behavior was a UI-only
outlier present since the form's first draft with no documented
rationale.

No backend changes are needed. Existing automations with a stored
snake_cased `workflow` selector will need a manual `PUT` to correct the
value — that is an operational fix, out of scope here.

### Fabro Details

<details>
<summary>Ran 8 stages in 31m 57s for $6.85</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 20s | – | 0 |
| preflight_lint | 2m 33s | – | 0 |
| implement | 4m 28s | $2.46 | 0 |
| simplify_fable | 8m 39s | $3.31 | 0 |
| simplify_gpt | 2m 25s | $1.07 | 0 |
| verify | 11m 2s | – | 0 |
| **Total** | **31m 57s** | **$6.85** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
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
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-07-07 14:08:32 -04:00
fabro-sh-0530[bot]
fe1d33c041
Remove top-level automation enabled master gate (#456)
The top-level `enabled` flag on automations created a confusing
two-level activation model (automation-level + trigger-level). Since
automations are brand new with no existing data to migrate, the master
gate is removed entirely — trigger-level `enabled` is now the sole
activation control.

## What changed

**Domain model (`fabro-automation`):** `enabled` removed from
`Automation`, `AutomationDraft`, `AutomationReplace`, and
`PersistedAutomation`. `enabled_api_trigger()` no longer short-circuits
on the automation flag. The `default_true()` helper is gone. A new test
asserts that TOML with a top-level `enabled` key is rejected (no silent
compatibility path).

**Server handler:** Conflict detail updated from `"automation is
disabled or has no enabled API trigger"` → `"automation has no enabled
API trigger"`. The
`disabled_automation_run_endpoint_returns_conflict_code` test is
deleted; the trigger-disabled and missing-trigger tests remain as the
authoritative inactive-run coverage.

**OpenAPI + generated clients:** `enabled` removed from `Automation`,
`CreateAutomationRequest`, and `ReplaceAutomationRequest` schemas and
from the generated TypeScript interfaces. Trigger-level `enabled` on
`AutomationApiTrigger` and `AutomationScheduleTrigger` is untouched.

**Web UI:** `AutomationFormValues.enabled` and the "Enabled" toggle row
are gone. `isFormValid` no longer requires at least one enabled trigger.
`canRun` in the detail view is now just `apiTrigger?.enabled === true`.
The `StatusChip` component is removed. The automations list uses a new
`apiEnabled` field (derived from `hasEnabledApiTrigger`) to drive
run-button state and tooltip copy. A shared `lib/automation.ts` helper
centralises `findApiTrigger`, `findScheduleTrigger`, and
`hasEnabledApiTrigger` to avoid repeated inline `.find()` calls across
routes.


### Fabro Details

<details>
<summary>Ran 8 stages in 41m 34s for $17.84</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 13m 0s | $9.25 | 0 |
| simplify_opus | 9m 43s | $6.18 | 0 |
| simplify_gpt | 3m 56s | $2.41 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **41m 34s** | **$17.84** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    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_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, 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_opus -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-29 22:18:14 -04:00
fabro-sh-0530[bot]
a65473f216
Add "Create automation from run" prefill flow (#454)
## Summary

Adds a frontend-only flow that lets users bootstrap a new automation
from an existing run's metadata. The run actions menu grows a
context-aware entry: ordinary runs get **Create automation from run**
(navigates to `/automations/new?from_run=<id>`); runs already tied to an
automation get **View automation** instead. The `/automations/new` route
reads the query param, loads the run + settings, and mounts a keyed form
child pre-populated with the inferred values — no `useEffect` needed.

### Plan Summary

- **`automation-form.tsx`** — new exported
`automationFormValuesFromRun(run, settings)` helper plus three narrow
private parsers (`githubRepositoryFromSettings`, `githubRepositoryName`,
`githubRepositoryFromOriginUrl`) that only produce `owner/repo` for
verifiable GitHub-style values and leave everything else blank.
- **`automations-new.tsx`** — route split into a wrapper that reads
`from_run`, calls `useRun`/`useRunSettings`, and either shows a loading
placeholder, a graceful error fallback, or a keyed
`<AutomationCreateForm>` so initial state is set once from props rather
than via effects.
- **`run-detail.tsx`** — `automationAction` computed from
`summary.automation?.id` and inserted into the `operations` group after
Preview.
- **Tests** — new `automations-new.test.tsx` covers empty form, prefill,
and error-fallback paths; `run-detail.test.ts` extended with automation
navigation assertions and refactored `makeRunSummary` to accept named
params.

### Key design decisions

| Decision | Rationale |
|---|---|
| Keyed child form (`key={`from-run:${id}`}`) | Lets React reset
`useState` from props without `useEffect`, per the effects policy |
| Wait for both queries before mounting | Prevents edits being
overwritten when settings arrive after the run |
| GitHub-only repository parsing | Narrow match avoids silently
populating wrong values for non-GitHub or unknown providers |
| No schedule inference | Prefilled automations default to manual/API
trigger enabled, schedule disabled |
| Navigation-only action | No disabled states for terminal/demo runs —
it's just a link |


### Fabro Details

<details>
<summary>Ran 8 stages in 31m 21s for $11.39</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 6s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 31s | – | 0 |
| implement | 10m 10s | $7.83 | 0 |
| simplify_opus | 4m 23s | $2.16 | 0 |
| simplify_gpt | 1m 34s | $1.40 | 0 |
| verify | 9m 42s | – | 0 |
| **Total** | **31m 21s** | **$11.39** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    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_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, 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_opus -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-29 22:18:04 -04:00
Bryan Helmkamp
0e224aa705
fix(web): remove slug field from automation edit page
The slug cannot be changed after creation, so showing it as a read-only
row on the edit page added noise without value. Keep the editable slug
input on the create page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:35:57 -04:00
Bryan Helmkamp
fee245d788
fix(web): make plural /automations/:id the canonical detail route
The list card linked to the singular /automation/:id, which mismatched
the rest of the new automations CRUD surface (/automations,
/automations/new, /automations/:id/edit). Switch the card link and the
slug-preview text on the create form to the plural form, and mount
/automations/:id in the router alongside the existing singular route
(kept as a back-compat alias for any older bookmarks).

Drive-by: fold two adjacent `use super::*` imports into one and reflow
a long `if let` line in the automations handler (linter cleanup; no
behavior change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:47:31 -04:00
Bryan Helmkamp
87516c25ce
feat(automations): wire UI to API and auto-start runs from API trigger
Make the Automations area in the web UI functional end-to-end against the
real Automation API, and fix the backend so runs created by an automation's
API trigger actually start instead of sitting in Submitted forever.

Web:
- Reveal the Automations nav tab outside demo mode; drop the now-empty
  demoOnly mechanism.
- List page: render via listAutomations (was workflows mock data); wire
  ellipsis menu to Edit and Delete, with ConfirmDialog + If-Match revision.
  Move Create Automation into the toolbar, switch the trigger select to a
  shared FilterButton, hide the redundant page-header title via a new
  hideTitle handle flag.
- Play button on each card fires createAutomationRun with spinner + toast
  and navigates to the new run.
- New automation form: drop the dead Goal panel and hardcoded repository
  list, post to createAutomation with real triggers.
- Edit automation: new /automations/:id/edit route reusing a shared
  AutomationFormFields component, PUT via replaceAutomation with If-Match.
- Show page: rebuild like a run detail page — breadcrumb, title, chips
  (enabled status, repo+ref, workflow, schedule), Edit + Run actions
  (Run hits createAutomationRun), and a Runs panel using RunsListView
  with URL-driven search/sort/pagination/column-picker like the Children
  sub-tab. Drop the obsolete Definition/Diagram/Runs child routes.

Backend (fabro-server):
- create_automation_run now calls lifecycle::queue_run_start after the
  run is persisted, so the run transitions Submitted → Runnable and the
  scheduler picks it up. Logs a warn and returns the created response if
  start fails (no worse than the prior always-stuck behavior).
- queue_run_start in lifecycle.rs is promoted to pub(super) so sibling
  handlers can reuse it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:24:09 -04:00