mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
1934 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4546e29b5f
|
feat(llm): add input token counting
Add a fabro-llm input token counting API with provider-native counting for Anthropic, Gemini, and OpenAI. Include strict fallback behavior, deterministic local estimates, count-specific request filtering, and README documentation for privacy and billing semantics. |
||
|
|
29e2750a00
|
feat(agent): add Anthropic TaskGet and task reminders
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Expose TaskGet for Claude-style task inspection, refresh Anthropic prompt and tool guidance, and remind long-running sessions to use task tracking when those tools are available. |
||
|
|
eb4891b1b0
|
refactor(agent): simplify reviewed changes
Use raw sandbox reads for memory and skills, keep line-numbered reads focused on display, and share retry-delay handling across agent and LLM code. Trim task tool descriptions, bound multi-file read concurrency, restore Docker's text read path, and add the reviewed implementation plan docs. |
||
|
|
d1cc47324d
|
fix(agent): use raw sandbox reads for edits
Separate raw file reads from the line-numbered display API so apply_patch and edit_file operate on unformatted UTF-8 content. Keep read_file/read_many_files model-facing output numbered and cover regressions for prefix corruption. |
||
|
|
96fff07a84
|
fix(agent): retry retryable mid-stream LLM failures
Replay retryable stream failures from the last committed turn state, clear partial visible output before retry or terminal failure, and map Anthropic stream error events into structured provider errors. |
||
|
|
a308734e9b
|
feat(agent): align Anthropic task prompt guidance
Modularize the Anthropic system prompt into Claude-style sections and expand TaskCreate, TaskUpdate, and TaskList descriptions with task-management guidance adapted to Fabro's tool surface. |
||
|
|
bf7ce485e1
|
feat(fabro-types): promote transcript primitives and extend agent event… (#357)
## Summary
This is the foundational step of the unified agent transcript
implementation: it establishes one canonical set of replay types in
`fabro-types` and threads them into the existing `agent.message`,
`agent.tool.started`, and `agent.tool.completed` event shapes — without
breaking any existing producers or consumers.
## What changed
**New `fabro-types::transcript` module** owns `ContentPart`,
`ImageData`, `AudioData`, `DocumentData`, `ThinkingData`, `ToolCall`,
`ToolResult`, `MessageKind`, `MessageSource`, `PairMessageRef`,
`TranscriptMessage`, and `MessageId`. These were previously defined in
`fabro-llm::types`; they now live at the canonical layer.
**`fabro-llm::types`** drops its local definitions and re-exports from
`fabro-types` so every existing `fabro_llm::types::*` import keeps
compiling without change.
**`AgentMessageProps`** gains an optional `message:
Option<TranscriptMessage>` field; `AgentToolStartedProps` gains
`tool_call`, `turn_id`, and `parent_message_id`;
`AgentToolCompletedProps` gains `tool_result` and `turn_id`. All new
fields use `#[serde(default, skip_serializing_if = "Option::is_none")]`
so existing stored events deserialize cleanly.
**All current event emitters** (`fabro-workflow/event/convert.rs`, demo
fixtures, test helpers) are updated to set the new fields to `None` —
this is a mechanical compatibility update; actual enrichment comes in
later tasks.
### Design decisions worth noting
- `MessageKind` captures LLM role semantics (system / user / reasoning /
agent); `MessageSource` captures audit provenance (steer, pair,
loop_detection, …). They are intentionally kept separate so a steering
message can be `kind=user, source=steer` without collapsing the
distinction.
- `TranscriptMessage` is named with the `Transcript` prefix specifically
to avoid import ambiguity with `fabro_agent::Message` and
`fabro_llm::types::Message`.
- `ProviderAnswer` and `ProviderReasoning` are included as
`MessageSource` variants so committed model outputs carry a first-class
audit label distinct from user-originated inputs.
- The new fields are additive-only; no narrow legacy fields were
removed. Consumer migration is a separate step.
### Fabro Details
<details>
<summary>Ran 9 stages in 47m 27s for $12.79</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 20m 6s | $8.53 | 0 |
| simplify_opus | 13m 36s | $2.68 | 0 |
| simplify_gpt | 4m 17s | $1.58 | 0 |
| verify | 4m 7s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **47m 27s** | **$12.79** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
a2e2cbc7ed
|
Add agent context observability events (memory, skills, MCP tools) (#356)
## Summary
Adds three new durable run events — `agent.memory.loaded`,
`agent.skills.discovered`, and `agent.skill.activated` — and enriches
`agent.mcp.ready` with names-only tool summaries. Consumers can now
reconstruct what memory, skills, and MCP tools were active for any agent
run by reading the event stream, without needing to inspect session
state.
### Plan Summary
- **`fabro-types`**: New prop structs (`AgentMemoryLoadedProps`,
`AgentSkillsDiscoveredProps`, `AgentSkillActivatedProps`,
`AgentMcpToolSummary`) and three new `EventBody` variants with canonical
dot-name serialization. `AgentMcpReadyProps.tools` uses
`#[serde(default, skip_serializing_if = "Vec::is_empty")]` for backwards
compatibility.
- **`fabro-agent/memory.rs`**: `discover_memory` now returns
`Vec<MemoryDocument>` carrying path, byte counts, and truncation flag
alongside content. The content itself is never put in any event payload.
- **`fabro-agent/types.rs`**: Adds `MemoryLoaded`, `SkillsDiscovered`,
`SkillActivated`, and enriched `McpServerReady` internal variants.
Removes `SkillExpanded` (replaced by `SkillActivated { source: Slash
}`). New variants are **not** classified as streaming noise, so they
persist.
- **`fabro-agent/session.rs`**: Emits `MemoryLoaded` before skills init,
`SkillsDiscovered` after skill discovery, and enriches `McpServerReady`
with summaries from `McpConnectionManager::tool_summaries_for_server`.
Slash expansion now emits `SkillActivated { source: Slash }` instead of
`SkillExpanded`.
- **`fabro-agent/skills.rs`**: `make_use_skill_tool` emits
`SkillActivated { source: Tool }` on successful lookup only.
- **`fabro-mcp/connection_manager.rs`**: New `tool_summaries_for_server`
returns sorted `(qualified_name, original_name)` pairs without leaking
descriptions or schemas.
- **`fabro-workflow/event/convert.rs` + `names.rs`**: Converts all new
agent events to their typed `fabro-types` props, including `visit`
injection. Removes dead `SkillExpanded` arm.
- **`docs/internal/events.md`**: Documents all new event shapes with
full property tables; notes that `agent.skill.expanded` is replaced.
### Key design decisions
- Both `MemoryLoaded` and `SkillsDiscovered` are emitted even when the
result is empty. This lets consumers distinguish "no memory/skills
found" from "event not yet reported."
- Memory file **contents are never included** in any event payload —
only `path`, `byte_count`, `loaded_bytes`, and `truncated`.
- `agent.mcp.ready` `tools` field is omitted from JSON when empty
(`skip_serializing_if`), preserving wire compatibility with existing
stored events.
- `SkillActivated` is persisted (not filtered as streaming noise),
unlike the former internal-only `SkillExpanded`.
### Fabro Details
<details>
<summary>Ran 9 stages in 57m 15s for $27.15</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 17s | – | 0 |
| preflight_lint | 2m 30s | – | 0 |
| implement | 21m 18s | $16.27 | 0 |
| simplify_opus | 15m 21s | $6.52 | 0 |
| simplify_gpt | 10m 33s | $4.35 | 0 |
| verify | 4m 24s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **57m 15s** | **$27.15** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
0754f1ca4a
|
Add fabro_run_get read-only run inspection tool (#358)
## Summary
Adds a new `fabro_run_get` MCP tool that returns a run's summary,
resolved ID, projection, and pending questions without any mutation
capability. This separates read-only inspection from operational
control, allowing Ask Fabro sessions to inspect runs safely without
access to write operations.
## What changed and why
**New tool (`fabro-tool/src/get.rs`):** `FabroRunGetParams` /
`ValidatedRunGet` / `RunGetResult` follow the same
validation-and-dispatch pattern as other run tools. The implementation
resolves a selector, then fans out to three read-only API calls
(retrieve run, get state, list questions) and assembles them into a
single structured result.
**Tool registry and dispatch:** `FABRO_RUN_GET_TOOL_NAME` is exported
from `common.rs` and `lib.rs`, added to `TOOL_DEFINITIONS`, wired into
the MCP stdio server (`fabro-mcp-server/src/server.rs`), and dispatched
in the LLM agent executor (`fabro-workflow/src/handler/llm/api.rs`).
**Ask Fabro access policy (`fabro-server/.../sessions.rs`):** The
session now registers and allows only `fabro_run_events` +
`fabro_run_get` via the new `ASK_FABRO_RUN_TOOL_NAMES` constant.
`fabro_run_interact` is explicitly moved to the denied set, closing off
mutation from that session type. The policy match arm is refactored from
a hardcoded `|`-chain to a slice `contains` check so the constant is the
single source of truth.
**Docs:** `mcp.mdx` now lists `fabro_run_get` as the inspection tool and
redescribes `fabro_run_interact` as control-oriented.
**Backward compatibility:** `fabro_run_interact` (including `get` and
`get_questions` actions) is unchanged and still fully operational for
contexts that allow it.
### Plan Summary
- New `get.rs` module in `fabro-tool` with validation, async fetch, and
unit tests
- Constants + schema registration in `common.rs` / `lib.rs`
- MCP server and LLM dispatch branches added for
`FABRO_RUN_GET_TOOL_NAME`
- Ask Fabro session swaps `FABRO_RUN_INTERACT_TOOL_NAME` →
`FABRO_RUN_GET_TOOL_NAME` in registry and policy
- MCP integration tests: tool count constant, schema assertions, two new
end-to-end tests
(`mcp_get_resolves_selector_and_returns_summary_projection_and_questions`,
`mcp_get_rejects_blank_run_id_before_auth_or_network`)
- Public MCP docs updated
### Fabro Details
<details>
<summary>Ran 9 stages in 43m 30s for $13.42</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 2s | – | 0 |
| preflight_lint | 2m 14s | – | 0 |
| implement | 19m 43s | $8.85 | 0 |
| simplify_opus | 5m 52s | $1.45 | 0 |
| simplify_gpt | 10m 16s | $3.12 | 0 |
| verify | 2m 53s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **43m 30s** | **$13.42** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
94c657b92e
|
feat: add run.checkpoint.skip_git_hooks to bypass Git commit hooks (#355)
## Summary
Adds an opt-in `skip_git_hooks` boolean to `[run.checkpoint]` that
causes Fabro-managed run-branch checkpoint commits to pass `--no-verify`
to `git commit`, bypassing local hooks such as `pre-commit` and
`commit-msg`. Defaults to `false`. Metadata-branch snapshots and Fabro
`[[run.hooks]]` are unaffected.
```toml
[run.checkpoint]
skip_git_hooks = true
```
### Plan Summary
- `RunCheckpointSettings` (dense, in `fabro-types`) gains
`skip_git_hooks: bool` with `#[serde(default)]`.
- `RunCheckpointLayer` (sparse, in `fabro-config`) gains
`skip_git_hooks: Option<bool>` so layered config can distinguish unset
from explicit `false`.
- `RunCheckpointLayer::combine` is refactored from a wholesale-replace
to field-level merging: `exclude_globs` keeps its existing replace-wins
semantics; `skip_git_hooks` uses `.or()` (highest-priority layer that
sets it wins).
- `resolve_checkpoint` resolves `None → false`.
- `git_checkpoint` / `checked_git_checkpoint` in `sandbox_git.rs` accept
a new `skip_git_hooks: bool` and append `--no-verify` when true.
- `parallel_branch_commit_cmd` (new helper in `handler/parallel.rs`)
replaces the inline format string and accepts the same flag.
- `GitState` carries `checkpoint_skip_git_hooks`;
`RunOptions::checkpoint_skip_git_hooks()` exposes it; `execute.rs` and
`git.rs` thread it through.
- OpenAPI schema, TypeScript API client, and docs are updated.
### Key design decisions
**Field-level merging in `combine`**: the previous
`RunCheckpointLayer::combine` replaced the whole struct when
`self.exclude_globs` was non-empty. The refactor keeps that same replace
rule for `exclude_globs` while adding independent `Option::or` merging
for `skip_git_hooks`, so the two fields don't interfere.
**`--no-verify` only on run-branch commits**: the flag is injected only
in the two Git commit paths Fabro controls for run-branch checkpoints.
Metadata-branch snapshots use `git2` and never fire local hooks
regardless of this setting.
### Fabro Details
<details>
<summary>Ran 9 stages in 48m 3s for $13.91</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 3s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 24m 13s | $10.38 | 0 |
| simplify_opus | 10m 31s | $1.59 | 0 |
| simplify_gpt | 5m 8s | $1.93 | 0 |
| verify | 3m 4s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **48m 3s** | **$13.91** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
df6f62db6a
|
test(agent): pass root session id in tool execution tests
Update stale test-only call sites after execute_and_emit_one_tool gained an explicit root_session_id argument. |
||
|
|
511371bd9a
|
feat(mcp): support goal files in run create
Allow fabro_run_create object specs to pass goal_file, reject goal and goal_file together, and preserve file-sourced goal semantics when building run manifests. |
||
|
|
c2d950da38
|
feat: orient Ask Fabro turns with run snapshots | ||
|
|
4df9e483ac
|
Merge remote-tracking branch 'origin/main' into ask-fabro-welcome-prompts | ||
|
|
a42634cc54
|
Add event-sourced todo tools for OpenAI and Anthropic profiles (#353)
## Summary
Adds a shared todo/task engine behind two model-native tool surfaces,
with all mutations persisted as individual run events and replayed into
`RunProjection`.
### Plan Summary
- **New domain types** in `fabro-types`: `TodoStatus`, `TodoListKind`,
`TodoProjection`, `TodoListProjection`, and `todos_by_list` on
`RunProjection`.
- **New run events**: `todo.created`, `todo.updated`, `todo.deleted` —
mapped through Fabro's typed event pipeline and replayed by
`RunProjectionReducer`.
- **`TodoRuntime`** (`fabro-agent`): thread-safe in-memory projection
shared across tool closures within a profile instance; each mutation
emits the corresponding agent event.
- **`update_plan`** registered only in `OpenAiProfile`: reconciles
incoming steps by exact `step` text (sha256-derived ID), emitting
create/update/delete events to match the submitted plan.
- **`TaskCreate` / `TaskUpdate` / `TaskList`** registered only in
`AnthropicProfile`: numeric task IDs per list, metadata merge with
`null`-key deletion, `status: "deleted"` routes to `todo.deleted`.
- **Session identity threading**: `ToolContext` gains `session_id`,
`root_session_id`, `tool_call_id`, and `agent_event_emitter`;
`execute_tool_calls` threads these through to `execute_one_tool`;
`Session` tracks `root_session_id` and `spawn_agent` inherits it for
subagents.
- **Scoping**: OpenAI todos scope to `openai_plan:<session_id>`
(per-session); Anthropic todos scope to
`anthropic_tasks:<root_session_id>` (shared across subagents).
- **Web invalidation**: `todo.*` events invalidate `getRunState` and the
run events list; tested in `run-events.test.tsx`.
```mermaid
graph TB
subgraph OpenAI
UP[update_plan] -->|diff by step text| TR[TodoRuntime]
end
subgraph Anthropic
TC[TaskCreate] --> TR
TU[TaskUpdate] --> TR
TL[TaskList] -->|read-only snapshot| TR
end
TR -->|emit todo.created/updated/deleted| SE[SessionBoundEmitter]
SE --> EV[AgentEvent stream]
EV --> RP[RunProjection\ntodos_by_list]
```
### Key design decisions
- **Step identity by text, not position** (`update_plan`): a
sha256-derived ID from `list_id + step` means reordering without
renaming emits an update rather than a delete+create. Duplicate step
strings are rejected with a model-visible error because text is the
identity.
- **No plan-replace event**: the engine emits only individual mutation
events; bulk replacement is expressed as a set of create/update/delete
events produced by diffing the incoming plan against the projection
snapshot.
- **`TodoRuntime` per profile instance**: tools inside a single profile
share one runtime. OpenAI subagents each have their own `session_id` so
their plans are isolated; Anthropic subagents inherit `root_session_id`
so tasks are shared — matching upstream Codex/Claude behavior.
- **`AgentEventEmitter` trait on `ToolContext`**: a narrow interface
that lets tools publish typed events without taking a dependency on the
full `Emitter`. `SessionBoundEmitter` wraps `Emitter` and stamps
`session_id` + `tool_call_id` on each event.
### Fabro Details
<details>
<summary>Ran 9 stages in 93m 23s for $56.46</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 20s | – | 0 |
| implement | 41m 30s | $38.21 | 0 |
| simplify_opus | 29m 2s | $11.78 | 0 |
| simplify_gpt | 14m 24s | $6.46 | 0 |
| verify | 3m 2s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **93m 23s** | **$56.46** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
1930791802
|
feat(agent): gate tools by effective access policy
Expose only policy-allowed tool schemas to the model and block hidden tool calls before executor lookup. Give Ask Fabro a run-scoped read-only policy and prompt so it only advertises tools it can actually use. |
||
|
|
d52f3726fc
|
fix: hide Ask Fabro reasoning deltas | ||
|
|
4190e13a20
|
Collapsible run stage sidebar (#352)
## What Makes the run-detail stage sidebar (shown on the Overview and Stages tabs) collapsible with a slide animation. - A toggle button slides the panel between full width (`w-56`) and an icon-only rail (`w-12`), animating `width` over 300ms with the same easing as the Ask Fabro panel. - When collapsed, **stage status icons stay visible** — green check / red X / spinning teal for running — so run progress is still scannable at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to icons too so they remain reachable. - Labels and durations become `sr-only` with `title` tooltips for hover. - The open/closed choice persists to `localStorage` (`fabro:stage-sidebar-collapsed`), carrying across the Overview and Stages tabs and reloads. ## Layout - The collapse toggle is inline with the `STAGES` heading row (or `WORKFLOW` when a run has no stages yet), so it doesn't push the stage list down. - The stage sidebar's top padding on the Stages tab was reduced (`pt-6` → `pt-3`) so the heading aligns with the adjacent content column and sits closer to the tab nav. ## Notes Self-contained in `StageSidebar` — `run-overview.tsx` and `run-stages.tsx` render it inside flex layouts that already track its width, so the slide works in both with no parent changes (aside from the padding tweak). Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass (10/10). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro <noreply@fabro.sh> |
||
|
|
199cf0822e
|
feat: gate Fabro run tools by worker JWT scope (#351)
Fabro run tools are now gated behind an explicit per-run opt-in so workflow agents only receive those capabilities when the run requests them. ## What changed - **`fabro_tools` setting** (`run.agent.fabro_tools`, default `false`) is resolved through the existing TOML config layer stack. - **Worker JWT scope** adds `agent:run_tools` only when the resolved setting is `true`; default worker tokens carry only `run:worker`. - **Worker tool registration** derives from the worker JWT scope claim. The CLI worker locally decodes the token payload and registers `FabroRunToolServices` only when the scope includes both `run:worker` and `agent:run_tools`. - **Server-side authorization remains authoritative**. The worker-side decode is only a local tool-registration gate; the server still validates token signature and scopes before accepting run-tool API calls. > **Behavior change:** existing runs that relied on Fabro run tools being always available must add `[run.agent] fabro_tools = true` to their workflow config. ## Verification ```sh cargo +nightly-2026-04-14 fmt --all cargo test -p fabro-cli fabro_run_tools_enabled_token_requires_run_tools_scope cargo test -p fabro-server worker_command_ cargo test -p fabro-static cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-server -p fabro-static --all-targets -- -D warnings git diff --check ``` --------- Co-authored-by: Fabro <noreply@fabro.sh> Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
5d188dbe18
|
feat(agent): make OpenAI apply_patch Codex-compatible (#350)
## Summary OpenAI-profile agents now receive `apply_patch` as a Codex-compatible freeform custom tool instead of a JSON function with a `patch` field. The model sends raw patch text validated by the vendored Lark grammar, and Fabro round-trips OpenAI custom tool calls/results through the Responses API. ## What Changed - Added `fabro-llm` support for function and custom tool definitions while preserving existing JSON function behavior for normal tools. - Translated OpenAI custom tool calls, custom tool outputs, and streaming `response.custom_tool_call_input.delta` events into Fabro tool calls with raw arguments. - Ported the Codex apply-patch grammar and adapted Codex-style patch parsing/application semantics to Fabro's `Sandbox` trait, including strict envelopes, fuzzy context matching, move/delete/add/update behavior, trailing-newline normalization, and Codex-style summaries/errors. - Updated OpenAI agent prompt/tool registration so `apply_patch` is freeform, and skipped JSON-schema validation/repair for custom tool calls only. - Updated file tracking to read Codex-style `A`/`M` result lines from successful patch output. ## Verification - `cargo nextest run -p fabro-llm -p fabro-agent` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --no-deps -p fabro-llm -p fabro-agent --all-targets -- -D warnings` - `git diff --check` Note: the full non-`--no-deps` clippy command still surfaces an unrelated existing `fabro-sandbox` `large_enum_variant` warning in `lib/crates/fabro-sandbox/src/sandbox_spec.rs`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
95b45b5960
|
feat: wire Ask Fabro sidebar to real session API with run-control tools (#349)
## Summary
Ships the Ask Fabro sidebar on run pages end-to-end: the agent now has
live `fabro_run_interact` and `fabro_run_events` tools scoped to its
owning run, and the web sidebar talks to real session APIs instead of a
scripted adapter. The `?ask=1` prototype gate is dropped in favour of
server-reported `run.ask_fabro.available`.
## What changed and why
### Rust — run-control tools in Ask Fabro sessions (`fabro-server`,
`fabro-workflow`, `fabro-tool`)
**Tool registration** (`fabro-workflow`): `register_fabro_run_tools` is
now `pub`; a new `register_named_fabro_run_tools` variant accepts a name
allowlist so callers can register a subset without forking the catalog
loop. Unknown names are silently ignored.
**Run-scoped backend** (`fabro-tool`): `ClientBackend` gains a
`run_scope: Option<RunId>` field set via `.with_run_scope(run_id)`.
Every method checks the scope before delegating to the HTTP client,
returning an error before a network call is made. `list_store_runs`
returns a single-element vec of the owning run when scoped;
`resolve_run` rejects non-parseable selectors rather than forwarding
them.
**Session wiring** (`fabro-server`): `build_profile` now returns
`Box<dyn AgentProfile>` (mutably accessible) instead of `Arc`;
`build_agent_session` mints a same-run worker token, builds a
`ClientBackend::with_run_scope`, constructs `FabroRunToolServices`, and
calls `register_named_fabro_run_tools` for the two tools before freezing
into an `Arc`. `AppState::self_server_target()` reads the bound address
from the runtime daemon record for the loopback HTTP call.
**Approval gate**: `build_ask_fabro_tool_approval` now fast-paths
`fabro_run_interact` and `fabro_run_events` to `Ok(())`; all other tools
remain subject to the `ReadOnly` auto-approve check. File/shell tools
are still denied.
### Web — real session adapter and sidebar wiring (`fabro-web`)
**`ask-fabro-runtime.ts`** (new): a `ChatModelAdapter` that creates a
session lazily on the first turn (`sessionsApi.createRunSession`),
caches the session id in `sessionStorage` keyed by run id, and streams
turns via `streamSessionTurn`. `applyTurnEvent` maps `run.session.*` SSE
events to assistant-ui `ThreadAssistantMessagePart[]` incrementally
(text deltas, tool-call started/completed pairs). A 404 on stream clears
the cached id so the next turn starts fresh.
**`ask-fabro-sidebar.tsx`**: drops `scriptIndexRef`, `EMPTY_CHAT`, and
the scripted adapter import; accepts `runId` and `defaultModel` props;
constructs the real adapter via `createAskFabroAdapter`.
**`run-detail.tsx`**: removes `?ask=1` / `askEnabled`; reads
`run.ask_fabro.{available, default_model}` from the summary; always
renders an `AskFabroTriggerButton` (disabled with a tooltip when
unavailable); passes `runId` and `defaultModel` to `<AskFabroSidebar>`.
### Architecture
```mermaid
graph TB
Browser -->|SSE turn stream| SessionsHandler
SessionsHandler -->|spawn| AskFabroAgent
AskFabroAgent -->|fabro_run_interact\nfabro_run_events| ClientBackend
ClientBackend -->|HTTP + same-run\nworker token| RunsAPI[Runs API\n/runs/:id]
ClientBackend -->|run_scope check| ClientBackend
RunsAPI -->|403 cross-run| ClientBackend
```
### Design decisions
- **Same-run scoping is double-enforced**: the `ClientBackend` scope
check fires before the HTTP call; the worker token's run scope causes a
403 at the API layer if the check were somehow bypassed.
- **`build_profile` → `Box` not `Arc`**: the profile needs mutable
access for tool registration after construction, so the `Arc` wrapping
is deferred until registration is complete.
- **`sessionStorage` per-run**: one session is reused across sidebar
open/close cycles for the same run tab; a page reload or different run
always starts clean.
- **Mutating actions included**: `interact` exposes
start/cancel/steer/archive/answer. This is intentional per the locked
decisions; the worker-token scope prevents cross-run blast radius.
### Fabro Details
<details>
<summary>Ran 9 stages in 74m 52s for $44.11</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 22s | – | 0 |
| implement | 38m 37s | $32.02 | 0 |
| simplify_opus | 17m 40s | $6.55 | 0 |
| simplify_gpt | 9m 32s | $5.55 | 0 |
| verify | 3m 43s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **74m 52s** | **$44.11** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro <fabro@example.com>
Co-authored-by: fabro <fabro@fabro.sh>
|
||
|
|
f5f921aa3c
|
feat: add [run.agent] fabro_tools opt-in for worker run tools (#348)
## Summary
Workflow agents previously always received the `agent:run_tools` JWT
scope and had Fabro run tools registered unconditionally. This PR makes
Fabro run tool access an explicit per-run opt-in via `[run.agent]
fabro_tools = true`, defaulting to `false`.
## What changed
**Config layer** (`fabro-types`, `fabro-config`): `RunAgentSettings`
gains a `fabro_tools: bool` field (serialized with `#[serde(default)]`
for backward compatibility). The corresponding `RunAgentLayer` gets an
optional `fabro_tools: Option<bool>` that resolves to `false` when
absent. Layer merging follows the existing `Combine` macro semantics, so
a workflow-level `false` overrides a user-level `true`.
**Server** (`fabro-server`): `worker_command` gains an
`agent_fabro_tools_enabled: bool` parameter. The server reads
`run_state.spec.settings.run.agent.fabro_tools` from the stored run
before spawning `__run-worker`, then issues the worker JWT with either
`["run:worker"]` or `["run:worker", "agent:run_tools"]` accordingly.
`WorkerScopeSet::run_worker()` loses its `#[cfg(test)]` gate so it's
available in production paths.
**CLI worker** (`fabro-cli`): `FabroRunToolServices` construction is now
gated on `run_spec.settings.run.agent.fabro_tools` rather than being
unconditional. The resolved run spec already carries the setting, so no
env-var parsing is needed in the runner.
**Tests**: The single monolithic
`worker_command_always_sets_worker_token_env` test is replaced by two
focused tests — one confirming the default scope is `["run:worker"]`
only, and one confirming the opt-in scope includes `agent:run_tools`.
Shared assertion logic is extracted into
`assert_worker_command_passes_token_only_by_env` and
`worker_token_claims` helpers. Config resolver tests cover default,
explicit true/false, and layer-override behavior.
**Docs**: `[run.agent]` description and reference tables are updated; a
new `run-configuration.mdx` section explains the opt-in semantics before
the existing `[run.agent.mcps]` section.
### Plan Summary
- 1. Add `fabro_tools` to resolved and layered run config, with resolver
tests.
- 2. Gate worker JWT scope and pass setting from stored run state into
`worker_command`.
- 3. Gate CLI `FabroRunToolServices` construction on the resolved
setting.
- 4. Update docs generator sample and public reference/execution docs.
- 5. Full verification pass (nextest, fmt, clippy).
### Fabro Details
<details>
<summary>Ran 9 stages in 54m 4s for $15.21</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 28m 5s | $10.58 | 0 |
| simplify_opus | 12m 4s | $2.86 | 0 |
| simplify_gpt | 5m 19s | $1.77 | 0 |
| verify | 3m 37s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **54m 4s** | **$15.21** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-agent <agent@fabro.local>
|
||
|
|
c6356cbd77
|
feat: improve run board, thread, and MCP create flows (#347)
## Summary This branch improves several run-management surfaces that agents and users rely on: archived runs now stay visible and ordered correctly in the board view, pair-session messages appear in the stage Thread tab, and the `fabro_run_create` MCP tool accepts the workflow-string shorthand it advertises. ## Changes - Updates the web board cache invalidation and archived-column handling so archive/unarchive actions refresh both active and archived board queries and keep archived runs in a predictable column position. - Adds pair user/system message events to stage activity parsing, Thread rendering, search, details, and DNA timeline items. - Aligns `fabro_run_create` MCP runtime deserialization and `tools/list` schema so each run entry may be either a workflow string or a full create spec object. ## Test Plan - `cargo nextest run -p fabro-tool -p fabro-mcp-server` - `cargo nextest run -p fabro-cli stdio_server_initializes_and_lists_run_tools mcp_create_string_shorthand_deserializes_before_auth mcp_create_validation_errors_happen_before_auth_or_network mcp_create_and_search_manage_real_runs_with_cli_auth` - `cargo +nightly-2026-04-14 fmt --check --all` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f5ec711a2c
|
Stage-based pairing API and fabro_run_pair MCP tool (#344)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
## Summary
Run pairing previously required callers to supply an opaque
`agent_session_id` alongside a `stage_id` to start or target a pair
session. This leaked an internal runtime identifier across the public
HTTP API, generated TypeScript client, and would have bled into any MCP
tooling. This PR removes that coupling: the public pair API now
identifies targets by `StageId` alone, the server resolves the live
session internally, and a new `fabro_run_pair` MCP tool exposes the full
pair lifecycle without ever seeing session identifiers.
### What changed
**Public contract simplification** (`fabro-types`, OpenAPI, generated TS
client)
- `PairTarget` is now `{ stage_id, node_label }` — `node_id`, `visit`,
`agent_session_id`, `provider`, and `model` are removed.
- `PairStartRequest` accepts `{ stage_id }` instead of `{ target:
PairTargetSelector }`.
- `PairTargetSelector` and `PairTranscriptModel` types are deleted
entirely.
- `PairMessageRecord.target` (selector) replaced by
`PairMessageRecord.stage_id`.
- `PairTranscriptAssistantMessage.model` field removed.
- `MAX_PAIR_MESSAGE_BYTES` extracted as a public constant shared between
the server handler and the MCP tool.
**Internal session binding** (`SteeringHub`, server projection)
- `ActivePair` now carries `session_id: String` separately from the
public `PairRecord`. This preserves the stale-session protection that
previously relied on `target.agent_session_id`.
- Transcript matching changed from `(session_id AND stage_id)` to
`stage_id` within the already-scoped pair window sequence range —
simpler and sufficient.
- `active_api_targets` deactivation no longer does a per-target
`agent_session_id` check; it relies on the `active_steerable_stages`
lease already doing that guard.
**New `fabro_run_pair` MCP tool** (`fabro-mcp-server`)
- Actions: `status`, `start`, `get`, `message`, `end`, `transcript`.
- Validation happens before any network call; missing `run_id`, missing
`stage_id` for `start`, missing/invalid `pair_id` for other actions, and
overlong message text all return clean tool-level errors.
- `strum::IntoStaticStr` on `RunPairAction` enables the
`parse_pair_id_for_action` helper to embed the action name in error
messages without a `match`.
- MCP result schema and serialized results are covered by leakage
assertions confirming none of the removed fields surface.
**Tests**
- Negative leakage assertions added to pair DTO tests, event round-trip
tests, control-protocol tests, server handler tests, MCP validation
tests, and MCP schema test.
- Tool count updated from 5 → 6 in all CLI MCP integration tests.
- Steering hub test renamed:
`pair_start_rejects_non_selected_or_missing_target` →
`pair_start_rejects_missing_target` (session-mismatch rejection is now
an internal concern).
### Fabro Details
<details>
<summary>Ran 9 stages in 60m 29s for $31.78</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 26s | – | 0 |
| implement | 33m 14s | $25.51 | 0 |
| simplify_opus | 15m 7s | $4.18 | 0 |
| simplify_gpt | 3m 46s | $2.08 | 0 |
| verify | 3m 9s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **60m 29s** | **$31.78** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
296fbddec9
|
feat(api): add ask fabro session endpoints (#342)
## Summary Adds the run-backed API surface needed for a real Ask Fabro sidebar: run readiness metadata, detailed session projections, session-scoped event listing/attach streaming, and turn control that exposes durable turn IDs and machine-readable failures. ## What Changed - Extended the OpenAPI contract and regenerated Rust/TypeScript clients for `Run.ask_fabro`, `SessionDetail`, `SessionTurn`, paginated run sessions, session event APIs, and optional client-supplied `turn_id` values. - Updated `fabro-types` and `fabro-store` so durable `run.session.*` events project active turn state, transcript messages, and the latest owning run event sequence. - Implemented server routing for session details, `/events`, `/attach`, turn conflict headers, typed turn failure codes, and cheap run readiness decoration across run responses. - Added browser helpers for POST turn streaming and session attach SSE parsing, plus an exported generated `sessionsApi`. ## Verification - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cargo build --workspace` - `cargo test -p fabro-types run_session_turn_failed_defaults_code_for_old_events` - `cargo test -p fabro-store run_sessions::tests` - `cargo test -p fabro-api` - `cargo test -p fabro-server --features test-support --test it api::sessions` - `cargo test -p fabro-server --features test-support --test it api::runs` - `cd apps/fabro-web && bun test app/lib/session-stream.test.ts` - `cd apps/fabro-web && bun run typecheck` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 Codex (context unknown, medium reasoning) via [Codex](https://openai.com/codex/) |
||
|
|
54bc67017e
|
feat: Replace duration/elapsed fields with wall_time_ms and StageTiming (#343)
## Summary
Replaces the ambiguous `runtime_secs`, `elapsed_secs`, and `duration_ms`
timing fields on run/stage public API surfaces with explicit
`wall_time_ms` (elapsed clock time) and a `StageTiming` value object
that also carries `inference_time_ms`, `tool_time_ms`, and
`active_time_ms`.
This is a greenfield breaking change — no compatibility shims are
preserved.
### What changed
**API shape**
- `RunBillingStage.runtime_secs` → `RunBillingStage.timing: StageTiming`
- `RunBillingTotals.runtime_secs` → `RunBillingTotals.timing:
StageTiming`
- `RunSummary.timestamps.duration_ms` / `elapsed_secs` removed; a
top-level `timing: StageTiming | null` field added
- Stage list item `duration_secs` → `wall_time_ms`
**Web app (`apps/fabro-web`)**
- `run-billing.tsx`: `liveRuntimeSecs` → `liveWallTimeMs`; live ticking
now returns milliseconds and the footer total sums `wallTimeMs` across
rows
- `stage-sidebar.ts`: `duration_secs` → `wall_time_ms` for the per-stage
duration display
- `runs.ts`: `elapsed_secs` lookup replaced with `timing.wall_time_ms`
- `formatElapsedSecs` / `formatDurationSecs` call sites replaced with
`formatDurationMs`
**Lockfile / tooling**
- `@openapitools/openapi-generator-cli@2.20.2` added as a dev dependency
to `@qltysh/fabro-api-client` to support regenerating the TypeScript
client after schema edits; several transitive deps pulled in alongside
it.
### Design notes
- **Units are now consistent**: every timing value on run/stage surfaces
is in milliseconds; the old API mixed seconds (`runtime_secs`,
`elapsed_secs`) with milliseconds (`duration_ms`).
- **Live ticking** still works correctly: the in-flight billing row
computes `now - startedAt` in ms and sums across rows for the footer,
avoiding a server round-trip during a running stage.
- **`StageTiming.active_time_ms = inference_time_ms + tool_time_ms`** —
parallel work is summed, so run active time can exceed wall time.
- Subsystem-internal `duration_ms` fields (sandbox setup, devcontainer
lifecycle, hooks) are intentionally left unchanged; only public
run/stage timing surfaces are affected.
### Fabro Details
<details>
<summary>Ran 9 stages in 115m 53s for $108.50</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 80m 53s | $101.97 | 0 |
| simplify_opus | 21m 35s | $4.09 | 0 |
| simplify_gpt | 5m 1s | $2.44 | 0 |
| verify | 3m 11s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **115m 53s** | **$108.50** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
fb2174c7d0
|
feat(agent): expose Fabro run tools in sessions (#339)
## Summary API-backed Fabro agent sessions can now use the same five run-control tools that were previously MCP-only, while the server uses a single scoped `FABRO_WORKER_TOKEN` path for worker authorization. This lets server-dispatched API agents create, gather, inspect, and interact with runs without reintroducing a separate delegated run-agent token or leaking credentials into sandboxed child environments. ## Changes - Moved the reusable run-tool implementation into the new `fabro-tool` crate so MCP and API agent backends share the same tool schemas and client behavior. - Registered the five `fabro_run_*` tools for API-mode agents, with ACP sessions continuing to omit those tools. - Replaced `FABRO_RUN_AGENT_TOKEN` with scoped worker-token auth: base worker tokens keep same-run access, and `run:worker agent:run_tools` tokens can call the run-control API across runs. - Added server auth guards for run-tool actors and run-scoped-or-run-tools routes, then applied them only to the routes used by the run-tool client backend. - Kept `FABRO_WORKER_TOKEN` scrubbed from sandbox commands, hooks, ACP subprocesses, MCP env providers, and other child tool environments. ## Testing - `cargo nextest run -p fabro-server worker_token principal_middleware spawn_env --no-fail-fast` - `cargo nextest run -p fabro-cli runner --no-fail-fast` - `cargo nextest run -p fabro-workflow agent_run --no-fail-fast` - `cargo nextest run -p fabro-static --no-fail-fast` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-server -p fabro-cli -p fabro-static --all-targets -- -D warnings` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
2b168b4588
|
Compute LLM cost on-read for in-flight billing stages (#345)
## Summary
The run billing page showed `—` for dollar cost on any active
(in-flight) stage because `total_usd_micros` is only computed at stage
completion. This PR prices stages whose cost is `None` at read time,
using the model and token counts already present in the projection.
## What changed
**`fabro-model/src/billing.rs`** gains two new methods on existing
types:
- `BilledTokenCounts::token_counts()` — extracts the five disjoint token
buckets, dropping the derived sum and optional cost field.
- `Catalog::price_tokens(model, tokens)` — mirrors the cost computation
from `billed_model_usage_from_llm` but callable outside the completion
path. Returns `None` for unknown models or providers with no billing
policy.
**`fabro-workflow/src/billing_rollup.rs`** —
`billing_rollup_from_projection` gains an `Option<&Catalog>` parameter.
A new private helper `stage_usage_with_cost` fills in `total_usd_micros`
on-the-fly for any stage where it is `None` and both a catalog and model
are available. All four accumulator call sites (`is_zero` check,
per-stage row, `totals`, and `by_model`) use the priced copy, keeping
the page internally consistent.
**Call sites** — the read handler (`handler/billing.rs`) passes
`Some(&catalog)` so active stages get priced. The aggregate-billing
sites in `server.rs` and the four finalization sites in `finalize.rs`
pass `None` — completed stages are already priced, and we deliberately
exclude running estimates from org-wide totals to avoid double-counting
when a run later finalizes.
## Design decisions
- **Price any stage with `total_usd_micros == None`**, not just
explicitly in-flight ones. Stages with no billing policy return `None`
again — harmless.
- **No "estimated" label** — cost-so-far is exact for tokens consumed so
far, consistent with the already-unlabeled live token count and runtime.
- In-flight **prompt** stages still show `—` because `stage.model` isn't
set until `PromptCompleted`. This is acceptable; the reported bug
concerns agent stages.
### Fabro Details
<details>
<summary>Ran 9 stages in 44m 13s for $7.17</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 6s | – | 0 |
| preflight_lint | 4m 58s | – | 0 |
| implement | 13m 24s | $3.84 | 0 |
| simplify_opus | 10m 34s | $1.69 | 0 |
| simplify_gpt | 5m 58s | $1.64 | 0 |
| verify | 3m 40s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **44m 13s** | **$7.17** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
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 -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
12e665c341
|
fix(sandbox): derive daytona network policies (#341)
Fixes Daytona sandbox network policy rendering so absent allow lists with `networkBlockAll=false` are reported as open egress instead of unknown, while Daytona ingress is always reported as blocked. The mapper still reports blocked egress when Daytona blocks all networking and CIDR allow-list egress when `networkAllowList` is present. Tests cover blocked, allow-list, empty allow-list, and default Daytona network data. Verified with `cargo nextest run -p fabro-sandbox --features daytona` and `cargo +nightly-2026-04-14 fmt --check --all`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
d53cf1eb76
|
chore(api-client): make client generation idempotent
`bun run generate` (openapi-generator typescript-axios) emits trailing spaces and extra blank lines, so every regeneration produced a noisy whitespace diff that masked real spec/client drift. Add a normalize-generated.ts post-generation pass that strips trailing whitespace and ends each file with exactly one newline, and chain it into the `generate` script. Establishes the normalized baseline across the generated client; running `generate` twice now yields no diff. No content changes — the entire diff is whitespace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5095873ddd
|
feat(web): add detail popovers to the run header
Hovering the run header items now reveals a popover with extra context: - Run status: failure reason and error message for failed runs; archived timestamp for archived runs (no popover otherwise) - Repository: full owner/repo name and the cloned branch - Workflow: node and edge counts plus run labels - PR: live GitHub details fetched lazily on hover — title, an open/draft/merged/closed badge, and the head -> base branch arrow Workflow node/edge counts are new: WorkflowRef now carries node_count/edge_count, computed in build_summary from the parsed graph that is already in hand there. Adds a HoverCard primitive alongside Tooltip (shared useHoverAnchor hook) for rich, viewport-aware popovers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
37d6b3dbcd
|
fix(server): count whole storage tree in Fabro-managed bytes
build_disk_usage_response only summed scratch/ run dirs and logs/*.log, omitting objects/ (SlateDB + artifacts), sessions/, and vaults/ — a ~30x undercount of "Fabro managed" storage on the resources page. Measure the whole storage_dir tree for total_size_bytes so it can't drift as new subdirectories are added. Reclaimable stays a curated estimate that matches what `fabro system prune` actually frees. A residual "other" summary row keeps `fabro system df` totals consistent and surfaces as a "Database & artifacts" table row. Also add a KiB tier to formatBytesAsMemory so small storage values render human-readably instead of raw byte counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
52da187e65
|
fix(workflow): follow symlinked artifact roots (#338)
Fixes https://github.com/fabro-sh/fabro/issues/335 ## Summary - Run artifact discovery with `find -H` so a symlinked sandbox working-directory root is traversed. - Keep existing behavior for symlinks discovered inside the tree by preserving the current `-not -type l -type f` filter. - Add workflow integration coverage for artifact collection when the local sandbox working directory itself is a symlink. ## Test plan - `cargo nextest run -p fabro-workflow asset_collection_local_sandbox_symlink_working_directory` - `cargo nextest run -p fabro-workflow artifact_snapshot` - `cargo nextest run -p fabro-workflow asset_collection_local_sandbox` - `cargo +nightly-2026-04-14 fmt --check --all` |
||
|
|
86b1fbef7f
|
feat(api): bind sessions to runs (#336)
## Summary
Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.
## Changes
- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.
## Validation
- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && cd
../../../apps/fabro-web && bun run typecheck`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
|
||
|
|
02fabb7b47
|
fix(workflow): capture configured artifacts once (#337)
## Summary Fixes artifact promotion for configured `[run.artifacts].include` globs by collecting matching files after each stage regardless of mtime and surfacing failed discovery commands as collection failures. The workflow lifecycle now keeps a per-run ledger keyed by `(path, content_sha256)`, rebuilt from existing `artifact.captured` events, so unchanged files are persisted and emitted once while changed content at the same path can still be captured again. Fixes fabro-sh/fabro#335. ## Tests - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-workflow artifact_snapshot` - `cargo nextest run -p fabro-cli unchanged_matching_artifact_is_captured_once_across_stages` - `cargo nextest run -p fabro-cli acp_artifacts_are_listed_when_touched_file_mtime_precedes_attempt_start` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com> |
||
|
|
9837a08929
|
refactor(install): share persistence pipeline (#332)
## Summary Unifies installer persistence so CLI and web install paths share the same file/env/vault primitives, while preserving the CLI's server-API secret persistence and auth bootstrap behavior. ## What Changed - Added shared `fabro-install` config writers for installer-owned tagged enum tables, replacing `server.listen` and `cli.target` atomically so stale variant fields cannot survive. - Added `InstallPersistencePlan` for disk-backed settings, server env, and vault writes/removals with the existing rollback semantics for settings and vault failures. - Refactored `fabro install`, `fabro install github`, and `/install/finish` to use the shared persistence plan where their disk behavior overlaps. - Preserved full-install ordering: settings/env first, workflow-visible secrets through the server API second, and CLI `auth.json` only after API secret persistence succeeds. - Preserved web installer failure response fields for leftover and removed env keys, plus the post-success finish hook/shutdown behavior. ## Test Plan - `cargo nextest run -p fabro-install` - `cargo nextest run -p fabro-cli commands::install::tests` - `cargo nextest run -p fabro-cli --test it cmd::install` - `cargo nextest run -p fabro-server --features test-support --test it api::install` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
15ff38fa53
|
feat(template): resolve template error locations (#333)
## Summary Fixes `fabro-sh/fabro#330` by making template partials that reference missing inputs validate structurally with a warning instead of failing the validate command. The template crate now owns MiniJinja semantic error classification and source-location mapping, so workflow diagnostics can consume already-resolved template locations instead of remapping fragment spans itself. ## What Changed - Added `TemplateErrorLocation` and `TemplateSourceOrigin` APIs to report source name, line, column, and span from `fabro-template`. - Classified wrapped MiniJinja errors by their deepest semantic cause, preserving the original source chain for renderer context. - Added fragment-origin rendering paths so attribute fragments embedded in full workflow source report locations in the original source text. - Removed workflow-side source span remapping from template diagnostics; workflow now only adds owner, node/edge, severity, rule, and fix context. - Added regression coverage for include/import/from/extends undefined variables and the CLI `fabro validate` partial fixture. ## Test Plan - `cargo nextest run -p fabro-template` - `cargo nextest run -p fabro-workflow transforms::variable_expansion transforms::file_inlining` - `cargo nextest run -p fabro-cli --test it cmd::validate` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-template -p fabro-workflow -p fabro-cli --all-targets -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context not reported, reasoning not reported) via [Codex](https://openai.com/codex) |
||
|
|
bf96baa9f0
|
feat(server): add run pairing API (#312)
## Summary
Adds the server-side run pairing surface for joining one active API-mode
agent session, sending pair messages, reading a compact transcript, and
ending pairing explicitly before workflow release continues.
This PR wires the feature end to end:
- adds OpenAPI paths and shared `fabro-types` DTOs for pair lifecycle,
messages, transcript entries, and run event details
- adds typed `RunEvent` variants for pair lifecycle and pair-scoped
user/system messages
- extends the workflow steering hub and agent session drain path with
typed pair control items, single-target validation, pair parking, and
pair end/resume behavior
- extends worker JSONL control and server transports for pair
start/message/end while preserving existing
steer/interrupt/answer/cancel behavior
- adds Axum handlers for `/api/v1/runs/{id}/pair`, pair messages, pair
transcript, and `/api/v1/runs/{id}/events/{seq}`
- adds `fabro-client` helpers for the new endpoints
## Notes
The subprocess path does not add a bidirectional worker ack channel in
this PR. Instead, the HTTP pair handlers only return lifecycle/message
success after the corresponding durable runtime event is observed, so
mpsc enqueue success alone is not treated as API success.
The plan checklist in
`docs/superpowers/plans/2026-05-18-server-side-run-pairing-api-events.md`
is included with that distinction left visible.
## Verification
- `cargo build -p fabro-api`
- `cargo check -p fabro-api -p fabro-client -p fabro-agent -p
fabro-workflow -p fabro-interview -p fabro-server`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-api pair
run_event_round_trips_pair_lifecycle_events
run_event_round_trips_agent_pair_messages`
- `cargo nextest run -p fabro-workflow pair`
- `cargo nextest run -p fabro-interview pair`
- `cargo nextest run -p fabro-server pair
subprocess_answer_transport_pair_commands_enqueue_control_messages steer
interrupt`
|
||
|
|
412f57f5ae
|
feat(web): add secrets management settings page (#327)
## What Adds a **Settings → Secrets** page so secrets can be managed from the browser, backed by the existing secrets HTTP API and generated TS client. - **`/settings/secrets`** — lists stored secrets (name, type badge, description, last-updated) and deletes them through the shared confirm dialog. - **`/settings/secrets/new`** — the create form for **token** and **file** secrets. OAuth secrets still list and delete here, but are created by provider sign-in flows, not typed by hand (matching the CLI's `secret set`). - The **Secrets** entry is added to the settings sidebar nav. ## How - `secretsApi` wired into `api-client.ts`; `useSecrets()` SWR hook + `secrets` query key. - New sibling routes `secrets` and `secrets/new` under `settings` (same pattern as `runs` / `runs/:id`). - The settings layout gains optional **handle-driven** `description` and `headerAction`. When a page declares them, the layout renders title + subheading + a vertically-centered header action button as one unified header. Other settings pages are unaffected — they fall back to the existing title-only header. ## Notes - Values are write-only: the API never returns secret values, and the UI never displays them. - Reuses existing primitives throughout (`Panel`, `Badge`, `ConfirmDialog`, `useToast`, button/input classes) — no new shared components. - Verified: `bun run typecheck` and `bun run build` pass; routes serve 200. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
40ed64c1c2
|
Add system resources settings page (#328)
## Summary Adds server-visible resource reporting and a compact Resources settings tab for CPU, memory, and the filesystem that contains Fabro storage. ## Changes - Adds `GET /api/v1/system/resources` backed by `sysinfo`, including CPU sampling, cgroup-aware memory reporting, storage filesystem matching, and Fabro-managed disk byte totals. - Extends the OpenAPI contract and regenerates the Rust and TypeScript API clients. - Adds a deterministic demo-mode resources route. - Adds `/settings/resources` with 5 second polling and panels for overview, CPU, memory, disk, and notes. - Adds server integration/unit coverage and web route/render coverage. ## Screenshot  ## Verification - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cargo nextest run -p fabro-server --features test-support --test it api::system` - `cargo test -p fabro-server resource_sampler::tests` - `cd apps/fabro-web && bun test` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun run build` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` --- [](https://github.com/compound-engineering) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5eb874b55c
|
feat(sandbox): label Daytona sandboxes as managed (#326)
## Summary Fabro-created Daytona sandboxes now carry the same managed-resource labels Docker containers already use: `sh.fabro.managed=true` and `sh.fabro.run_id=<run-id>` when a run id is available. This moves the Docker label constants into a shared sandbox helper, keeps Docker behavior unchanged, and applies the helper when Daytona create params are built. User-provided Daytona labels are preserved, but Fabro's reserved keys are authoritative on collisions. Daytona snapshot behavior is unchanged because the snapshot API does not expose labels. ## Testing - `cargo test -p fabro-sandbox managed_labels --no-default-features --features docker,daytona` - `cargo test -p fabro-sandbox docker::tests::real_run_container_gets_name_and_labels --no-default-features --features docker` - `cargo test -p fabro-sandbox daytona::tests::base_params --no-default-features --features daytona` - `cargo test -p fabro-sandbox daytona_managed_labels_live_smoke --no-default-features --features daytona` - `cargo test -p fabro-sandbox --no-default-features --features docker,daytona` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 clippy -p fabro-sandbox --all-targets --no-default-features --features docker,daytona -- -D warnings` The live Daytona smoke test remains ignored; it compiles under the Daytona feature but was not run against live credentials. ## Post-Deploy Monitoring & Validation - Log queries/search terms: `Failed to create Daytona sandbox`, `Daytona`, `labels`, `sh.fabro.managed`, `sh.fabro.run_id`, and sandbox initialization errors for `provider=daytona`. - Metrics or dashboards: Daytona sandbox creation success/error rate, Fabro run initialization failures for Daytona runs, and Daytona resource inventory filtered by `sh.fabro.managed=true`. - Expected healthy signals: new Fabro-created Daytona sandboxes include `sh.fabro.managed=true`, run-owned sandboxes include the matching `sh.fabro.run_id`, user labels remain visible, and Daytona sandbox creation failure rates stay at baseline. - Failure signals and rollback trigger: any sustained increase in Daytona sandbox creation failures, API validation errors around labels, or missing managed labels on newly created sandboxes. Roll back this PR or hotfix the label merge to omit Daytona labels if Daytona rejects the keys in production. - Validation window and owner: release owner watches the first 24 hours after deploy, with an immediate manual Daytona dashboard/API spot-check after the first managed Daytona run. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context unknown, reasoning enabled) via [Codex](https://openai.com/codex) |
||
|
|
5bfd115339
|
[codex] Add ACP steering support (#329)
## Summary - Adds a backend-neutral live control abstraction so steering, interrupt, and interrupt+steer no longer depend on API-only session handles. - Reworks ACP sessions into a live protocol loop that uses ACP `session/prompt` for follow-up steers and ACP `session/cancel` for interrupts without restarting the process. - Registers ACP sessions as steerable, removes the stale non-steerable server/UI/API path, preserves ACP projection metadata, and keeps unsupported backends out of the steerability gate. ## Validation - `LC_ALL=C cargo nextest run --workspace --no-fail-fast` (5,833 passed, 178 skipped) - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1 ASDF_BUN_VERSION=1.3.11 bun test` (396 passed) - `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1 ASDF_BUN_VERSION=1.3.11 bun run typecheck` - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && LC_ALL=C ASDF_BUN_VERSION=1.3.11 bun run typecheck` --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
9bdf30ad86
|
refactor(hooks): centralize run location handling (#325)
## Summary - Fix host command hooks to use the submitter/source directory instead of a sandbox-only working directory. - Introduce `HookExecutionContext` and `RunLocations` so host source, sandbox work, and run scratch paths are explicit. - Route lifecycle hooks and tool hooks through the shared hook execution context instead of rebuilding cwd pairs at call sites. ## Test Plan - `cargo nextest run -p fabro-hooks` - `cargo check -p fabro-workflow --tests` - `cargo +nightly-2026-04-14 fmt --package fabro-hooks --package fabro-workflow --check` - `cargo +nightly-2026-04-14 clippy -p fabro-hooks -p fabro-workflow --all-targets -- -D warnings` --------- Co-authored-by: Jess Martin <jessmartin@gmail.com> |
||
|
|
32015b2226
|
fix(graph): support dotted Fabro graph attributes (#324)
## Summary Graph rendering now accepts documented Fabro dotted DOT attributes end to end while keeping raw Graphviz calls behind `fabro-graphviz`. The new `RenderableDot` boundary applies Fabro render styling and normalization before raw SVG rendering, and both the CLI subprocess and server path now route through that typed boundary instead of calling `graphviz_sys` directly outside the graphviz crate. The branch also adds a small curated DOT compatibility corpus covering ACP agent attributes, human default choices, and subworkflow manager attributes. Those fixtures are exercised by both render and validation tests, and the run overview now shows graph render errors directly instead of falling through to the empty graph state. ## Verification - `cargo nextest run -p fabro-graphviz` - `cargo nextest run -p fabro-validate` - `cargo nextest run -p fabro-cli render_graph` - `cargo nextest run -p fabro-server render_graph_from_manifest_accepts_fabro_dotted_attributes` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-graphviz -p fabro-cli -p fabro-server -p fabro-validate --all-targets -- -D warnings` - `rg -n "graphviz_sys" lib/crates/fabro-cli lib/crates/fabro-server` - `cd apps/fabro-web && bun test` - `cd apps/fabro-web && bun run typecheck` --- [](https://github.com/EveryInc/compound-engineering-plugin) Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com> |
||
|
|
9f6823b10d
|
fix(workflow): honor human gate timeout defaults (#323)
## Summary - add a regression test for unanswered human gate timeouts with `human.default_choice` - route human gate `timeout` through the interview timeout path so it emits `interview.timeout` and selects the default target - make timeout ownership explicit for handlers that consume `node.timeout()`, including command and ACP handlers Fixes #317 ## Testing - cargo nextest run -p fabro-workflow --test it human_gate_timeout_routes_to_default_choice_when_unanswered - cargo nextest run -p fabro-workflow timeout_policy built_in_handlers_that_consume_node_timeout_manage_it_themselves agent_handler_delegates_timeout_policy_to_backend - cargo nextest run -p fabro-workflow script_handler_timeout script_handler_timeout_error_includes_output_tails writes_script_timing_json_on_timeout timeout_causes_fail_status_record - cargo nextest run -p fabro-workflow wait_human - cargo +nightly-2026-04-14 fmt --check --all - cargo +nightly-2026-04-14 clippy -p fabro-workflow --all-targets -- -D warnings --------- Co-authored-by: Jess Martin <jessmartin@gmail.com> |
||
|
|
21c408d647
|
fix(workflow): allow workflow-root template partials (#322)
## Summary - Add a CLI validation regression for workflow-root prompt partials included from nested prompt files. - Allow bundled prompt templates to resolve sibling partials from the workflow root instead of jailing each prompt file to its own directory. - Refactor include handling so manifest discovery and runtime rendering share rooted template sources, include normalization, root containment checks, and FileResolver-backed TemplateStore loading. ## Testing - `cargo nextest run -p fabro-template` - `cargo nextest run -p fabro-manifest` - `cargo nextest run -p fabro-workflow` - `cargo nextest run -p fabro-cli cmd::validate` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-template -p fabro-manifest -p fabro-workflow -p fabro-cli --all-targets -- -D warnings` --------- Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com> |
||
|
|
d9b859d11d
|
chore: simplify | ||
|
|
d17599898e
|
feat(web): show creator avatar on run "Created by" cell (#319)
## Summary The run Overview tab's "Created by" cell rendered every user as a colored circle with the first letter of their login. Reviewers and run owners expected the same GitHub avatar shown on `/profile` and in the top-right nav. The cell only had `login` to work with — the `PrincipalUser` schema carried no avatar URL. This threads an optional `avatar_url` through `UserPrincipal` end-to-end: schema, server auth, and frontend. The avatar is captured at action time from the request's auth context and persisted with the run's `created_by` principal — a point-in-time snapshot, the same pattern as audit logs and chat apps. ## What changed - **`fabro-types`** — `UserPrincipal` gains `avatar_url: Option<String>` with `#[serde(default, skip_serializing_if)]`, plus a `Principal::user_with_avatar` constructor. The existing `Principal::user` constructor is unchanged (sets `None`), so test fixtures and CLI/replay call sites need no edits. - **OpenAPI** — `PrincipalUser` gains an optional nullable `avatar_url`; Rust (progenitor) and TypeScript clients regenerated. - **`fabro-server`** — `auth_context_from_session` (cookie auth) and `classify_user_token` (JWT auth) populate the principal's avatar from the session/JWT, treating an empty string as `None`. - **`fabro-web`** — the `run-summary-panel` "Created by" cell renders an `<img>` when `avatar_url` is present, falling back to the initial circle otherwise. ## Compatibility The field is optional with serde defaults, so old persisted runs and `RunEvent.actor` payloads deserialize unchanged — they show the initial-circle fallback. No migration or backfill. ## Known gap CLI-initiated runs (`fabro run ...`) still show the initial circle: the CLI auth flow hardcodes an empty `avatar_url` in the JWT subject (`cli_flow.rs:508`). Wiring the avatar through CLI login (`~/.fabro/auth.json`, JWT claims, refresh-token chain) is a deliberate follow-up. Web-initiated runs get the avatar today. ## Test plan - `cargo nextest run --workspace` — 5,832 tests pass, including new `principal.rs` and `principal_round_trip.rs` cases covering avatar serialization and legacy-JSON (no-field) deserialization. - `cd apps/fabro-web && bun test run-summary-panel` — 13 tests pass, including a new case asserting the `<img>` renders with the avatar src. - `bun run typecheck`, `cargo +nightly-2026-04-14 fmt --check --all`, and `clippy --workspace --all-targets -- -D warnings` all clean. - Manual: restart `fabro server`, create a run from the web UI, confirm the real avatar renders on the Overview tab; confirm an older run falls back to the initial circle. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fbe8b50a16
|
feat(server): add GET /api/v1/providers and /settings/models page (#321)
## Summary Operators had no UI surface to see which LLM providers their Fabro server has configured — provider state was only inferable indirectly via the per-model `configured` flag on `GET /api/v1/models`. This adds a dedicated **Models** settings tab backed by a new providers endpoint. - **`fabro_model::Provider`** — a public projection of the internal `CatalogProvider` that *structurally* excludes credential-bearing fields (`auth`, `extra_headers`, `billing_policy`, `agent_profile`). Reused by the generated API client via progenitor `with_replacement`, mirroring the existing `Model` pattern — no parallel API DTO. - **`GET /api/v1/providers`** — lists catalog providers with effective config and a `configured` status stamped per request from `ready_llm_provider_ids()`. Sorted by the catalog's existing `provider_order`. No write endpoints. - **`/settings/models` web page** — new route + nav entry (`CpuChipIcon`, between Integrations and Security) rendering each provider with model count, default model, configured status, and a "Get API key" link for unconfigured providers. ## Key decisions - Provider sort: reuse catalog `provider_order` (priority desc, id asc) — zero extra code. - `adapter` is hidden in the UI row (noisy for first-party providers); the OpenAPI `adapter` field is pinned to an enum matching the closed `AdapterKind` type. - `configured` reflects credential resolution **at the time of the response**, not a frozen startup snapshot — doc/spec wording corrected to match. ## Testing - `fabro-model`: `From<&CatalogProvider>` + serde `skip_serializing_if` unit tests. - `fabro-api`: `Provider` type-identity + JSON-parity tests, including the required/optional field split. - `fabro-server`: handler tests for configured vs unconfigured providers, exact `model_count`/`default_model` against catalog truth, and credential-omission (asserts internal field names *and* the injected credential value never reach the wire). - OpenAPI route conformance test covers `GET /api/v1/providers`. - `cargo build --workspace`, `fmt --check`, `clippy -D warnings` clean; 935 Rust tests pass; web `tsc` typecheck passes. - Reviewed via a 10-persona `ce:review` (autofix) — no P0/P1 in shipped code; 8 safe fixes applied. Not done: manual UI screenshots — the `apps/fabro-web` build is blocked in this environment by an unrelated missing `@assistant-ui/react` dependency. Run `bun install` in `apps/fabro-web` to verify `/settings/models` manually. ## Post-Deploy Monitoring & Validation - **What to watch:** request logs for `GET /api/v1/providers` — expect `200`s for authenticated users, `401` for unauthenticated. The handler resolves LLM credentials per request via `ready_llm_provider_ids()` (the same path the existing `list_models` handler already uses). - **Healthy signals:** `/settings/models` renders the provider list; `configured` matches each provider's actual credential state; no credential strings appear in any response body or log line. - **Failure signals / rollback trigger:** any provider object in the response containing `auth`, `extra_headers`, or a raw key/token value → roll back immediately (the projection type makes this structurally impossible, but treat any occurrence as P0). 5xx spikes on the new route. - **Validation window / owner:** first 24h after deploy, owned by the deploying engineer. Pre-existing note (not introduced here): credential resolution can refresh OAuth tokens and write the vault as a side effect of this read — shared with `list_models`; flagged for a future caching pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ef70dbc5be
|
fix(cli): print full run id on rm (#315)
When `fabro rm` succeeds, the confirmation output should identify exactly which run was removed. Today the human-readable path prints a shortened run ID, which is less precise than the JSON output and less useful for copy/paste confirmation. ## Summary - print the full run ID after successful `fabro rm` removal - keep `--json` behavior unchanged - update CLI snapshots to expect full IDs on success paths ## Testing - cargo test -p fabro-cli rm_ -- --nocapture - cargo +nightly-2026-04-14 fmt --check --all |