mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
7a475ebb5a
27 changed files with 1954 additions and 3436 deletions
|
|
@ -25,7 +25,6 @@ import * as RunStages from "./routes/run-stages";
|
|||
import * as RunSettings from "./routes/run-settings";
|
||||
import * as RunGraph from "./routes/run-graph";
|
||||
import * as RunFiles from "./routes/run-files";
|
||||
import * as RunVerification from "./routes/run-verification";
|
||||
import * as RunUsage from "./routes/run-usage";
|
||||
import * as RunRetro from "./routes/run-retro";
|
||||
import * as VerificationCriteria from "./routes/verification-criteria";
|
||||
|
|
@ -115,7 +114,6 @@ export const routes: RouteObject[] = [
|
|||
route("settings", RunSettings),
|
||||
route("graph", RunGraph),
|
||||
route("files", RunFiles),
|
||||
route("verification", RunVerification),
|
||||
route("usage", RunUsage),
|
||||
route("retro", RunRetro),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ const tabs = [
|
|||
{ name: "Overview", path: "", count: null },
|
||||
{ name: "Stages", path: "/stages/detect-drift", count: null },
|
||||
{ name: "Files Changed", path: "/files", count: null },
|
||||
{ name: "Verification", path: "/verification", count: null },
|
||||
{ name: "Retro", path: "/retro", count: null },
|
||||
{ name: "Usage", path: "/usage", count: null },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@headlessui/react";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
MinusCircleIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
statusConfig,
|
||||
typeConfig,
|
||||
getCriteriaSummary,
|
||||
} from "../data/verifications";
|
||||
import type {
|
||||
VerificationResult,
|
||||
VerificationType,
|
||||
VerificationCategory,
|
||||
} from "../data/verifications";
|
||||
import { apiJson } from "../api";
|
||||
import type { PaginatedRunVerificationList } from "@qltysh/fabro-api-client";
|
||||
|
||||
export async function loader({ request, params }: any) {
|
||||
const { data: apiCategories } = await apiJson<PaginatedRunVerificationList>(`/runs/${params.id}/verification`, { request });
|
||||
const categories: VerificationCategory[] = apiCategories.map((cat) => ({
|
||||
name: cat.name,
|
||||
question: cat.question,
|
||||
status: cat.status as VerificationResult,
|
||||
criteria: cat.controls.map((c) => ({
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
type: (c.type ?? null) as VerificationType | null,
|
||||
status: c.status as VerificationResult,
|
||||
})),
|
||||
}));
|
||||
return { categories };
|
||||
}
|
||||
|
||||
function StatusIcon({
|
||||
status,
|
||||
className = "size-5",
|
||||
}: {
|
||||
status: VerificationResult;
|
||||
className?: string;
|
||||
}) {
|
||||
switch (status) {
|
||||
case "pass":
|
||||
return <CheckCircleIcon className={`${className} text-mint`} />;
|
||||
case "fail":
|
||||
return <XCircleIcon className={`${className} text-coral`} />;
|
||||
case "na":
|
||||
return <MinusCircleIcon className={`${className} text-fg-muted`} />;
|
||||
}
|
||||
}
|
||||
|
||||
function TypeBadge({ type }: { type: VerificationType | null }) {
|
||||
if (type === null) return null;
|
||||
const config = typeConfig[type];
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.color} ${config.bg}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryCard({ category }: { category: VerificationCategory }) {
|
||||
const criteriaStats = getCriteriaSummary(category.criteria);
|
||||
const applicable = criteriaStats.total - criteriaStats.na;
|
||||
const config = statusConfig[category.status];
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={category.status === "fail"}
|
||||
className={`rounded-md border border-line overflow-hidden border-l-2 ${config.border}`}
|
||||
>
|
||||
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-overlay">
|
||||
<StatusIcon status={category.status} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="shrink-0 text-sm font-semibold text-fg">
|
||||
{category.name}
|
||||
</span>
|
||||
<span className="truncate text-xs text-fg-muted">
|
||||
{category.question}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-xs tabular-nums text-fg-muted">
|
||||
{criteriaStats.passing}/{applicable}
|
||||
</span>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-fg-muted transition-transform duration-200 group-data-open:rotate-90" />
|
||||
</DisclosureButton>
|
||||
|
||||
<DisclosurePanel
|
||||
transition
|
||||
className="origin-top transition duration-200 ease-out data-closed:-translate-y-1 data-closed:opacity-0"
|
||||
>
|
||||
<div className="border-t border-line">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{category.criteria.map((criterion) => (
|
||||
<tr
|
||||
key={criterion.name}
|
||||
className="border-b border-line last:border-b-0 cursor-pointer transition-colors hover:bg-overlay"
|
||||
>
|
||||
<td className="w-8 py-2.5 pl-5 pr-0">
|
||||
<span
|
||||
className={`inline-block size-2 rounded-full ${statusConfig[criterion.status].dot}`}
|
||||
/>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-fg-2">
|
||||
{criterion.name}
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-fg-muted">
|
||||
{criterion.description || (
|
||||
<span className="italic">Not configured</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-3 pr-4 text-right">
|
||||
<TypeBadge type={criterion.type} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RunVerifications({ loaderData }: any) {
|
||||
const { categories } = loaderData;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{categories.map((category) => (
|
||||
<CategoryCard key={category.name} category={category} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
apps/fabro-web/dist/assets/app.css
vendored
2
apps/fabro-web/dist/assets/app.css
vendored
File diff suppressed because one or more lines are too long
1944
apps/fabro-web/dist/assets/entry-k7vnt1v8.js
vendored
Normal file
1944
apps/fabro-web/dist/assets/entry-k7vnt1v8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1944
apps/fabro-web/dist/assets/entry-psq398et.js
vendored
1944
apps/fabro-web/dist/assets/entry-psq398et.js
vendored
File diff suppressed because one or more lines are too long
2
apps/fabro-web/dist/index.html
vendored
2
apps/fabro-web/dist/index.html
vendored
|
|
@ -61,7 +61,7 @@
|
|||
<script type="module" src="/assets/chunk-sadshphz.js"></script>
|
||||
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
|
||||
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
|
||||
<script type="module" src="/assets/entry-psq398et.js"></script>
|
||||
<script type="module" src="/assets/entry-k7vnt1v8.js"></script>
|
||||
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
|
||||
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
|
||||
<script type="module" src="/assets/chunk-9t57pdty.js"></script>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ tags:
|
|||
- name: Human-in-the-Loop
|
||||
description: Questions, answers, and steering for runs
|
||||
- name: Run Outputs
|
||||
description: Files and verifications produced by runs
|
||||
description: Files produced by runs
|
||||
- name: Run Internals
|
||||
description: Internal run details (stages, turns, context, configuration)
|
||||
- name: Workflows
|
||||
|
|
@ -875,30 +875,6 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/verification:
|
||||
get:
|
||||
operationId: retrieveRunVerification
|
||||
tags: [Run Outputs]
|
||||
summary: Retrieve Run Verification
|
||||
description: Returns verification results for a run, organized by criterion with individual control statuses.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
- $ref: "#/components/parameters/PageLimit"
|
||||
- $ref: "#/components/parameters/PageOffset"
|
||||
responses:
|
||||
"200":
|
||||
description: Array of verification criteria with controls
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PaginatedRunVerificationList"
|
||||
"404":
|
||||
description: Run not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/settings:
|
||||
get:
|
||||
operationId: retrieveRunSettings
|
||||
|
|
@ -2638,20 +2614,6 @@ components:
|
|||
meta:
|
||||
$ref: "#/components/schemas/PaginationMeta"
|
||||
|
||||
PaginatedRunVerificationList:
|
||||
description: Paginated list of run verification categories.
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RunVerification"
|
||||
meta:
|
||||
$ref: "#/components/schemas/PaginationMeta"
|
||||
|
||||
PaginatedVerificationCriterionList:
|
||||
description: Paginated list of verification criteria.
|
||||
type: object
|
||||
|
|
@ -4283,58 +4245,6 @@ components:
|
|||
- analysis
|
||||
- ai-analysis
|
||||
|
||||
RunVerificationControl:
|
||||
description: A verification control result within a run.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
- description
|
||||
- type
|
||||
- status
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Human-readable control name.
|
||||
example: Motivation
|
||||
slug:
|
||||
type: string
|
||||
description: URL-safe slug for linking to verification detail page.
|
||||
example: motivation
|
||||
description:
|
||||
type: string
|
||||
description: Short description of what the control verifies.
|
||||
example: Origin of proposal identified
|
||||
type:
|
||||
$ref: "#/components/schemas/VerificationType"
|
||||
status:
|
||||
$ref: "#/components/schemas/VerificationResult"
|
||||
|
||||
RunVerification:
|
||||
description: Verification results for a category within a run.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- question
|
||||
- status
|
||||
- controls
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Category name.
|
||||
example: Traceability
|
||||
question:
|
||||
type: string
|
||||
description: The guiding question for this verification category.
|
||||
example: Do we understand what this change is and why we're making it?
|
||||
status:
|
||||
$ref: "#/components/schemas/VerificationResult"
|
||||
controls:
|
||||
type: array
|
||||
description: Individual control results within this category.
|
||||
items:
|
||||
$ref: "#/components/schemas/RunVerificationControl"
|
||||
|
||||
SteerRequest:
|
||||
description: Request body for sending inline steering guidance to a running agent.
|
||||
type: object
|
||||
|
|
|
|||
|
|
@ -493,24 +493,6 @@ fabro graph run.toml --format svg
|
|||
| `-o, --output <FILE>` | Output file path. Defaults to stdout. |
|
||||
| `-d, --direction <DIR>` | Graph direction: `lr` or `tb`. If omitted, uses the Graphviz file's own `rankdir`. |
|
||||
|
||||
## `fabro skill install`
|
||||
|
||||
Install the built-in `fabro-create-workflow` skill for AI assistants (Claude Code, Codex). The skill teaches AI assistants Fabro's Graphviz syntax, node types, and run configuration format.
|
||||
|
||||
```bash
|
||||
# Install into the current project (.claude/skills/ or .agents/skills/)
|
||||
fabro skill install --for project --dir claude
|
||||
|
||||
# Install for all projects (user-level, ~/.claude/skills/)
|
||||
fabro skill install --for user --dir claude
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `--for <SCOPE>` | `user` (default) — installs to `~/<dir>/skills/`. `project` — installs to `./<dir>/skills/`. |
|
||||
| `--dir <DIR>` | Directory convention: `claude` (`.claude/skills/`) or `agents` (`.agents/skills/`). Required. |
|
||||
| `--force` | Overwrite an existing installation without prompting. |
|
||||
|
||||
## `fabro rewind`
|
||||
|
||||
Rewind a workflow run to an earlier checkpoint. This resets both the run branch and metadata branch refs so that `fabro resume` continues from the target checkpoint.
|
||||
|
|
|
|||
|
|
@ -648,33 +648,6 @@ pub(crate) struct SettingsArgs {
|
|||
pub(crate) workflow: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
pub(crate) enum SkillDir {
|
||||
Claude,
|
||||
Agents,
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
pub(crate) enum SkillScope {
|
||||
User,
|
||||
Project,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct SkillInstallArgs {
|
||||
/// Where to install: user-level or project-level
|
||||
#[arg(long = "for", default_value = "user")]
|
||||
pub(crate) scope: SkillScope,
|
||||
|
||||
/// Target directory convention
|
||||
#[arg(long)]
|
||||
pub(crate) dir: SkillDir,
|
||||
|
||||
/// Overwrite existing skill without prompting
|
||||
#[arg(long)]
|
||||
pub(crate) force: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct PrCreateArgs {
|
||||
#[command(flatten)]
|
||||
|
|
@ -945,9 +918,6 @@ pub(crate) enum Commands {
|
|||
Install(InstallArgs),
|
||||
/// Pull request operations
|
||||
Pr(PrNamespace),
|
||||
/// Skill management
|
||||
#[command(hide = true)]
|
||||
Skill(SkillNamespace),
|
||||
/// Manage server-owned secrets
|
||||
Secret(SecretNamespace),
|
||||
/// Inspect effective settings
|
||||
|
|
@ -1045,9 +1015,6 @@ impl Commands {
|
|||
WorkflowCommand::List(_) => "workflow list",
|
||||
WorkflowCommand::Create(_) => "workflow create",
|
||||
},
|
||||
Self::Skill(ns) => match &ns.command {
|
||||
SkillCommand::Install(_) => "skill install",
|
||||
},
|
||||
Self::Discord => "discord",
|
||||
Self::Docs => "docs",
|
||||
Self::Upgrade(_) => "upgrade",
|
||||
|
|
@ -1253,10 +1220,6 @@ pub(crate) enum RepoCommand {
|
|||
pub(crate) struct RepoInitArgs {
|
||||
#[command(flatten)]
|
||||
pub(crate) target: ServerTargetArgs,
|
||||
|
||||
/// Also install the fabro-create-workflow skill
|
||||
#[arg(long, hide = true)]
|
||||
pub(crate) skill: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -1296,15 +1259,3 @@ pub(crate) struct CompletionArgs {
|
|||
/// Shell to generate completions for
|
||||
pub shell: clap_complete::Shell,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct SkillNamespace {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: SkillCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum SkillCommand {
|
||||
/// Install a built-in skill
|
||||
Install(SkillInstallArgs),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ pub(crate) mod runs;
|
|||
pub(crate) mod sandbox;
|
||||
pub(crate) mod secret;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod skill;
|
||||
pub(crate) mod store;
|
||||
pub(crate) mod system;
|
||||
pub(crate) mod upgrade;
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ pub(crate) async fn dispatch(ns: RepoNamespace, globals: &GlobalArgs) -> Result<
|
|||
match ns.command {
|
||||
RepoCommand::Init(args) => {
|
||||
let created = init::run_init(&args, globals).await?;
|
||||
if args.skill {
|
||||
let base = std::env::current_dir()?.join(".claude").join("skills");
|
||||
super::skill::install_skill_to(&base)?;
|
||||
}
|
||||
if globals.json {
|
||||
print_json_pretty(&serde_json::json!({ "created": created }))?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::args::{GlobalArgs, SkillDir, SkillInstallArgs, SkillScope};
|
||||
use crate::shared::{absolute_or_current, print_json_pretty};
|
||||
|
||||
const SKILL_MD: &str = include_str!("../../../../../../skills/fabro-create-workflow/SKILL.md");
|
||||
const REF_DOT_LANGUAGE: &str =
|
||||
include_str!("../../../../../../skills/fabro-create-workflow/references/dot-language.md");
|
||||
const REF_EXAMPLE_WORKFLOWS: &str =
|
||||
include_str!("../../../../../../skills/fabro-create-workflow/references/example-workflows.md");
|
||||
const REF_RUN_CONFIGURATION: &str =
|
||||
include_str!("../../../../../../skills/fabro-create-workflow/references/run-configuration.md");
|
||||
|
||||
const SKILL_FILES: &[(&str, &str)] = &[
|
||||
("SKILL.md", SKILL_MD),
|
||||
("references/dot-language.md", REF_DOT_LANGUAGE),
|
||||
("references/example-workflows.md", REF_EXAMPLE_WORKFLOWS),
|
||||
("references/run-configuration.md", REF_RUN_CONFIGURATION),
|
||||
];
|
||||
|
||||
/// Install all skill files under `base_dir/fabro-create-workflow/`.
|
||||
pub(crate) fn install_skill_to(base_dir: &Path) -> Result<()> {
|
||||
let skill_dir = base_dir.join("fabro-create-workflow");
|
||||
|
||||
for (rel_path, content) in SKILL_FILES {
|
||||
let dest = skill_dir.join(rel_path);
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
debug!(file = %rel_path, "Writing skill file");
|
||||
std::fs::write(&dest, content)?;
|
||||
}
|
||||
|
||||
info!(path = %skill_dir.display(), "Skill installed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn run_skill_install(args: &SkillInstallArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let base_dir = resolve_base_dir(&args.scope, &args.dir)?;
|
||||
let skill_dir = base_dir.join("fabro-create-workflow");
|
||||
|
||||
if globals.json && skill_dir.exists() && !args.force {
|
||||
globals.require_no_json()?;
|
||||
}
|
||||
|
||||
if skill_dir.exists() && !args.force {
|
||||
let confirm = dialoguer::Confirm::new()
|
||||
.with_prompt(format!(
|
||||
"Skill directory already exists at {}. Overwrite?",
|
||||
skill_dir.display()
|
||||
))
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if !confirm {
|
||||
bail!("Aborted: skill directory already exists");
|
||||
}
|
||||
}
|
||||
|
||||
install_skill_to(&base_dir)?;
|
||||
|
||||
if globals.json {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"skill": "fabro-create-workflow",
|
||||
"path": absolute_or_current(&skill_dir),
|
||||
"files": SKILL_FILES.iter().map(|(rel_path, _)| (*rel_path).to_string()).collect::<Vec<_>>(),
|
||||
}))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_base_dir(scope: &SkillScope, dir: &SkillDir) -> Result<std::path::PathBuf> {
|
||||
let dir_name = match dir {
|
||||
SkillDir::Claude => ".claude",
|
||||
SkillDir::Agents => ".agents",
|
||||
};
|
||||
|
||||
let root = match scope {
|
||||
SkillScope::User => {
|
||||
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?
|
||||
}
|
||||
SkillScope::Project => std::env::current_dir()?,
|
||||
};
|
||||
|
||||
Ok(root.join(dir_name).join("skills"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn embedded_files_are_non_empty() {
|
||||
assert!(!SKILL_MD.is_empty());
|
||||
assert!(!REF_DOT_LANGUAGE.is_empty());
|
||||
assert!(!REF_EXAMPLE_WORKFLOWS.is_empty());
|
||||
assert!(!REF_RUN_CONFIGURATION.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_writes_all_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path().join("skills");
|
||||
|
||||
install_skill_to(&base).unwrap();
|
||||
|
||||
for (rel_path, content) in SKILL_FILES {
|
||||
let path = base.join("fabro-create-workflow").join(rel_path);
|
||||
assert!(path.exists(), "Missing file: {rel_path}");
|
||||
let written = std::fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(written, *content, "Content mismatch: {rel_path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_overwrites_existing_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path().join("skills");
|
||||
|
||||
install_skill_to(&base).unwrap();
|
||||
|
||||
let sentinel_path = base.join("fabro-create-workflow/SKILL.md");
|
||||
std::fs::write(&sentinel_path, "old content").unwrap();
|
||||
|
||||
install_skill_to(&base).unwrap();
|
||||
|
||||
let content = std::fs::read_to_string(&sentinel_path).unwrap();
|
||||
assert_eq!(content, SKILL_MD);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
mod install;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::args::{GlobalArgs, SkillCommand, SkillNamespace};
|
||||
|
||||
pub(crate) use install::install_skill_to;
|
||||
|
||||
pub(crate) fn dispatch(ns: SkillNamespace, globals: &GlobalArgs) -> Result<()> {
|
||||
match ns.command {
|
||||
SkillCommand::Install(args) => install::run_skill_install(&args, globals),
|
||||
}
|
||||
}
|
||||
|
|
@ -227,7 +227,6 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Commands::Secret(ns) => commands::secret::dispatch(ns, &globals).await?,
|
||||
Commands::Settings(args) => commands::config::execute(&args, &globals).await?,
|
||||
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &globals)?,
|
||||
Commands::Skill(ns) => commands::skill::dispatch(ns, &globals)?,
|
||||
Commands::Upgrade(args) => {
|
||||
commands::upgrade::run_upgrade(args, &globals).await?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,46 +72,3 @@ fn test_repo_deinit_fails_when_not_initialized() {
|
|||
error: not initialized — fabro.toml not found
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_init_skill_installs_skill_files() {
|
||||
let context = test_context!();
|
||||
context.git_init();
|
||||
|
||||
context.repo().args(["init", "--skill"]).assert().success();
|
||||
|
||||
// Skill files should be installed under .claude/skills/fabro-create-workflow/
|
||||
let skill_dir = context
|
||||
.temp_dir
|
||||
.join(".claude/skills/fabro-create-workflow");
|
||||
assert!(skill_dir.join("SKILL.md").exists(), "SKILL.md should exist");
|
||||
assert!(
|
||||
skill_dir.join("references/dot-language.md").exists(),
|
||||
"dot-language.md should exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_init_help_does_not_show_skill() {
|
||||
let context = test_context!();
|
||||
let mut cmd = context.repo();
|
||||
cmd.args(["init", "--help"]);
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Initialize a new project
|
||||
|
||||
Usage: fabro repo init [OPTIONS]
|
||||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||
-h, --help Print help
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,15 +117,6 @@ pub(crate) async fn get_run_usage(
|
|||
(StatusCode::OK, Json(runs::usage())).into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_run_verification(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(_id): Path<String>,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> Response {
|
||||
paginated_response(runs::verifications(), &pagination)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_run_settings(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
|
|
@ -1495,10 +1486,6 @@ mod runs {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn verifications() -> Vec<RunVerification> {
|
||||
super::verifications::run_verifications()
|
||||
}
|
||||
|
||||
pub(super) fn questions() -> Vec<ApiQuestion> {
|
||||
vec![
|
||||
ApiQuestion {
|
||||
|
|
@ -2037,7 +2024,7 @@ mod verifications {
|
|||
f1: Option<f64>,
|
||||
pass_at_1: Option<f64>,
|
||||
evaluations: &'static [VerificationResult],
|
||||
// Run-level status
|
||||
#[allow(dead_code)]
|
||||
run_status: VerificationResult,
|
||||
// Detail fields
|
||||
detail_description: &'static str,
|
||||
|
|
@ -2898,24 +2885,6 @@ mod verifications {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn category_run_status(cat: &CategoryDef) -> VerificationResult {
|
||||
if cat
|
||||
.controls
|
||||
.iter()
|
||||
.any(|c| c.run_status == VerificationResult::Fail)
|
||||
{
|
||||
VerificationResult::Fail
|
||||
} else if cat
|
||||
.controls
|
||||
.iter()
|
||||
.all(|c| c.run_status == VerificationResult::Na)
|
||||
{
|
||||
VerificationResult::Na
|
||||
} else {
|
||||
VerificationResult::Pass
|
||||
}
|
||||
}
|
||||
|
||||
fn slugify(name: &str) -> String {
|
||||
name.to_lowercase()
|
||||
.chars()
|
||||
|
|
@ -3051,28 +3020,6 @@ mod verifications {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn run_verifications() -> Vec<RunVerification> {
|
||||
ALL_CATEGORIES
|
||||
.iter()
|
||||
.map(|cat| RunVerification {
|
||||
name: cat.name.into(),
|
||||
question: cat.question.into(),
|
||||
status: category_run_status(cat),
|
||||
controls: cat
|
||||
.controls
|
||||
.iter()
|
||||
.map(|c| RunVerificationControl {
|
||||
name: c.name.into(),
|
||||
slug: c.slug.into(),
|
||||
description: c.description.into(),
|
||||
type_: c.type_,
|
||||
status: c.run_status,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
mod signoffs {
|
||||
|
|
|
|||
|
|
@ -420,7 +420,6 @@ fn demo_routes() -> Router<Arc<AppState>> {
|
|||
get(not_implemented),
|
||||
)
|
||||
.route("/runs/{id}/usage", get(demo::get_run_usage))
|
||||
.route("/runs/{id}/verification", get(demo::get_run_verification))
|
||||
.route("/runs/{id}/settings", get(demo::get_run_settings))
|
||||
.route("/runs/{id}/steer", post(demo::steer_run_stub))
|
||||
.route("/runs/{id}/preview", post(demo::generate_preview_url_stub))
|
||||
|
|
@ -531,7 +530,6 @@ fn real_routes() -> Router<Arc<AppState>> {
|
|||
get(get_stage_artifact),
|
||||
)
|
||||
.route("/runs/{id}/usage", get(not_implemented))
|
||||
.route("/runs/{id}/verification", get(not_implemented))
|
||||
.route("/runs/{id}/settings", get(not_implemented))
|
||||
.route("/runs/{id}/steer", post(not_implemented))
|
||||
.route("/runs/{id}/preview", post(generate_preview_url))
|
||||
|
|
|
|||
|
|
@ -85,10 +85,6 @@ const ENDPOINTS: &[PaginatedEndpoint] = &[
|
|||
path: "/api/v1/runs/run-1/stages",
|
||||
name: "listRunStages",
|
||||
},
|
||||
PaginatedEndpoint {
|
||||
path: "/api/v1/runs/run-1/verification",
|
||||
name: "retrieveRunVerification",
|
||||
},
|
||||
];
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ models/paginated-retro-list.ts
|
|||
models/paginated-run-file-list.ts
|
||||
models/paginated-run-list.ts
|
||||
models/paginated-run-stage-list.ts
|
||||
models/paginated-run-verification-list.ts
|
||||
models/paginated-saved-query-list.ts
|
||||
models/paginated-session-list.ts
|
||||
models/paginated-signoff-list.ts
|
||||
|
|
@ -173,8 +172,6 @@ models/run-status-response.ts
|
|||
models/run-status.ts
|
||||
models/run-timings.ts
|
||||
models/run-usage.ts
|
||||
models/run-verification-control.ts
|
||||
models/run-verification.ts
|
||||
models/sandbox-file-entry.ts
|
||||
models/sandbox-file-list-response.ts
|
||||
models/sandbox-resources.ts
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
|
|||
// @ts-ignore
|
||||
import type { ErrorResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedRunVerificationList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunUsage } from '../models';
|
||||
/**
|
||||
* RunOutputsApi - axios parameter creator
|
||||
|
|
@ -68,57 +66,6 @@ export const RunOutputsApiAxiosParamCreator = function (configuration?: Configur
|
|||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns verification results for a run, organized by criterion with individual control statuses.
|
||||
* @summary Retrieve Run Verification
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
retrieveRunVerification: async (id: string, pageLimit?: number, pageOffset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('retrieveRunVerification', 'id', id)
|
||||
const localVarPath = `/api/v1/runs/{id}/verification`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (pageLimit !== undefined) {
|
||||
localVarQueryParameter['page[limit]'] = pageLimit;
|
||||
}
|
||||
|
||||
if (pageOffset !== undefined) {
|
||||
localVarQueryParameter['page[offset]'] = pageOffset;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
|
|
@ -146,21 +93,6 @@ export const RunOutputsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.retrieveRunUsage']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns verification results for a run, organized by criterion with individual control statuses.
|
||||
* @summary Retrieve Run Verification
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async retrieveRunVerification(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunVerificationList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveRunVerification(id, pageLimit, pageOffset, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.retrieveRunVerification']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -180,18 +112,6 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas
|
|||
retrieveRunUsage(id: string, options?: RawAxiosRequestConfig): AxiosPromise<RunUsage> {
|
||||
return localVarFp.retrieveRunUsage(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns verification results for a run, organized by criterion with individual control statuses.
|
||||
* @summary Retrieve Run Verification
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
retrieveRunVerification(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunVerificationList> {
|
||||
return localVarFp.retrieveRunVerification(id, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -209,18 +129,5 @@ export class RunOutputsApi extends BaseAPI {
|
|||
public retrieveRunUsage(id: string, options?: RawAxiosRequestConfig) {
|
||||
return RunOutputsApiFp(this.configuration).retrieveRunUsage(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns verification results for a run, organized by criterion with individual control statuses.
|
||||
* @summary Retrieve Run Verification
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public retrieveRunVerification(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig) {
|
||||
return RunOutputsApiFp(this.configuration).retrieveRunVerification(id, pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ export * from './paginated-retro-list';
|
|||
export * from './paginated-run-file-list';
|
||||
export * from './paginated-run-list';
|
||||
export * from './paginated-run-stage-list';
|
||||
export * from './paginated-run-verification-list';
|
||||
export * from './paginated-saved-query-list';
|
||||
export * from './paginated-session-list';
|
||||
export * from './paginated-signoff-list';
|
||||
|
|
@ -150,8 +149,6 @@ export * from './run-status-record';
|
|||
export * from './run-status-response';
|
||||
export * from './run-timings';
|
||||
export * from './run-usage';
|
||||
export * from './run-verification';
|
||||
export * from './run-verification-control';
|
||||
export * from './sandbox-file-entry';
|
||||
export * from './sandbox-file-list-response';
|
||||
export * from './sandbox-resources';
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
---
|
||||
name: fabro-create-workflow
|
||||
description: Create Fabro workflow DOT graphs and TOML run configurations from natural language requirements. Use when the user wants to create a new workflow, build a pipeline, design a multi-step agent process, or write a .fabro or .toml file for Fabro. Covers topology selection, node types, model assignment, edge routing, and run configuration.
|
||||
---
|
||||
|
||||
# Fabro Create Workflow
|
||||
|
||||
Turn requirements into a runnable Fabro workflow: a `.fabro` graph file defining the pipeline structure and an optional `.toml` run configuration.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Fetch Current Model Catalog
|
||||
|
||||
Run `fabro model list` to get available models and providers. Never guess model IDs or provider names -- they change frequently.
|
||||
|
||||
### Step 2: Understand Requirements
|
||||
|
||||
Clarify what the workflow should accomplish:
|
||||
- What is the end goal?
|
||||
- What tools or languages are involved?
|
||||
- Does it need human approval gates?
|
||||
- Should multiple models or providers be used?
|
||||
- Is parallelism needed (e.g., multi-perspective review, ensemble)?
|
||||
- Does it need a verify/fix loop?
|
||||
- What sandbox environment is appropriate (local, docker, daytona)?
|
||||
|
||||
### Step 3: Choose Topology
|
||||
|
||||
Pick the simplest topology that satisfies the requirements. See `references/example-workflows.md` for complete examples of each pattern.
|
||||
|
||||
| Pattern | When to use |
|
||||
|---|---|
|
||||
| **Linear** | Simple sequential steps, no branching |
|
||||
| **Command-then-analyze** | Shell output feeds into LLM analysis |
|
||||
| **Implement-test-fix loop** | Code generation with validation cycle |
|
||||
| **Human approval gate** | Needs human review before proceeding |
|
||||
| **Plan-approve-implement** | Complex changes needing upfront planning |
|
||||
| **Parallel fan-out** | Independent analyses merged into synthesis |
|
||||
| **Multi-model ensemble** | Multiple providers give independent opinions |
|
||||
| **Production pipeline** | Toolchain checks + implement + verify + fixup loops |
|
||||
|
||||
Combine patterns as needed. For example, a production pipeline might include a human gate after planning and a parallel fan-out for review.
|
||||
|
||||
### Step 4: Write the DOT Graph
|
||||
|
||||
See `references/dot-language.md` for the full language reference.
|
||||
|
||||
**Required elements:**
|
||||
1. `digraph Name { ... }` wrapper
|
||||
2. `graph [goal="..."]` attribute
|
||||
3. `rankdir=LR` (preferred for readability)
|
||||
4. Exactly one `start [shape=Mdiamond, label="Start"]`
|
||||
5. Exactly one `exit [shape=Msquare, label="Exit"]`
|
||||
|
||||
**Choose node shapes by purpose:**
|
||||
- `box` (default) -- agent with tools (implement, fix, write code)
|
||||
- `tab` -- single LLM call without tools (analyze, plan, review, synthesize)
|
||||
- `parallelogram` -- shell command (build, test, lint)
|
||||
- `diamond` -- conditional routing (no prompt, only conditions on edges)
|
||||
- `hexagon` -- human decision gate
|
||||
- `component` -- parallel fan-out
|
||||
- `tripleoctagon` -- merge parallel results
|
||||
|
||||
**Prompt guidelines:**
|
||||
- Be specific and actionable in prompts
|
||||
- Tell the agent exactly what to do, what files to create/modify, what output to produce
|
||||
- Use `shape=tab` for nodes that only need to think, not act
|
||||
- Use `prompt="@path/to/file.md"` for long prompts (path relative to DOT file)
|
||||
- Set `reasoning_effort="low"` on simple analysis or summary nodes
|
||||
|
||||
**Edge routing:**
|
||||
- Use `condition="outcome=success"` and unconditional fallback for check gates
|
||||
- Diamond nodes must have multiple outgoing edges with conditions
|
||||
- Use `max_visits` on fix/retry nodes to prevent infinite loops (typically 3)
|
||||
- Use `goal_gate=true` on verification nodes that must pass for the workflow to succeed
|
||||
- Set `retry_target` on goal gates to specify where to jump on failure
|
||||
|
||||
### Step 5: Assign Models via Stylesheet
|
||||
|
||||
Use `model_stylesheet` for model assignment rather than per-node attributes:
|
||||
|
||||
```dot
|
||||
graph [model_stylesheet="
|
||||
* { model: claude-sonnet-4-6;}
|
||||
.coding { model: claude-opus-4-6;}
|
||||
.review { model: gemini-3.1-pro-preview;}
|
||||
"]
|
||||
```
|
||||
|
||||
- Use `*` for the default model (usually a fast/cheap model)
|
||||
- Use `.class` selectors for role-based assignment (`.coding`, `.review`, `.verify`)
|
||||
- Use `#nodeid` selectors for specific node overrides
|
||||
- Assign `class="coding"` etc. on nodes to match stylesheet rules
|
||||
- **Critical:** Use semicolons between properties in stylesheet rules
|
||||
|
||||
**Model selection heuristics:**
|
||||
- Fast/cheap models for simple analysis, summaries, routing: `claude-haiku-4-5`, `gemini-3-flash-preview`, `gpt-5-mini`
|
||||
- Strong models for coding, complex reasoning: `claude-opus-4-6`, `claude-sonnet-4-6`, `gpt-5.4`
|
||||
- Use `reasoning_effort="high"` for complex coding tasks
|
||||
- For ensembles, pick models from different providers for diversity
|
||||
|
||||
### Step 6: Write the TOML Run Configuration (if needed)
|
||||
|
||||
See `references/run-configuration.md` for the full reference.
|
||||
|
||||
A TOML file is optional for simple workflows (you can run `fabro run workflow.fabro` directly). Create one when you need:
|
||||
- Sandbox configuration (provider, environment variables)
|
||||
- Setup commands (install dependencies)
|
||||
- Variable definitions
|
||||
- LLM fallbacks
|
||||
- Hooks
|
||||
- Asset collection
|
||||
|
||||
Minimal TOML:
|
||||
|
||||
```toml
|
||||
version = 1
|
||||
graph = "workflow.fabro"
|
||||
```
|
||||
|
||||
Common additions:
|
||||
|
||||
```toml
|
||||
[sandbox]
|
||||
provider = "local"
|
||||
|
||||
[sandbox.local]
|
||||
worktree_mode = "always"
|
||||
|
||||
[sandbox.env]
|
||||
NODE_ENV = "test"
|
||||
```
|
||||
|
||||
### Step 7: Validate
|
||||
|
||||
Run `fabro preflight workflow.toml` (or `fabro preflight workflow.fabro`) to validate without executing.
|
||||
|
||||
If validation fails, fix the reported errors and re-validate.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Diamond nodes route only.** Never put a `prompt` on a `diamond` node -- it only evaluates edge conditions.
|
||||
- **All nodes reachable from start.** No orphan nodes.
|
||||
- **No edges into start or out of exit.**
|
||||
- **LLM nodes need prompts.** Every `box` and `tab` node must have a `prompt` attribute.
|
||||
- **Conditional nodes need multiple outgoing edges** with `condition` attributes.
|
||||
- **Prevent infinite loops.** Use `max_visits` on retry/fix nodes. Typical value: 3.
|
||||
- **Use `goal_gate=true`** on critical verification steps that must succeed.
|
||||
- **Use `outcome=success` conditions** on edges leaving command/conditional nodes.
|
||||
- **Model IDs must match the catalog.** Always run `fabro model list` first.
|
||||
- **Semicolons in stylesheets.** Properties must be separated by semicolons.
|
||||
|
||||
## File Organization
|
||||
|
||||
Place workflow files together in a directory:
|
||||
|
||||
```
|
||||
my-workflow/
|
||||
workflow.fabro # the graph
|
||||
workflow.toml # run configuration (optional)
|
||||
prompts/ # external prompt files (optional)
|
||||
implement.md
|
||||
review.md
|
||||
```
|
||||
|
||||
## Running Workflows
|
||||
|
||||
```bash
|
||||
fabro run workflow.fabro # run graph directly
|
||||
fabro run workflow.toml # run with TOML config
|
||||
fabro run workflow.toml --dry-run # simulated LLM backend
|
||||
fabro run workflow.toml --no-retro # skip retro (faster for testing)
|
||||
fabro run workflow.toml --auto-approve # auto-approve human gates
|
||||
fabro run workflow.toml --model claude-opus-4-6 # override model
|
||||
fabro run workflow.toml --sandbox local # override sandbox
|
||||
fabro validate workflow.fabro # validate only
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- `references/dot-language.md` -- Complete DOT language reference (node types, attributes, edges, conditions, stylesheets, fidelity, variables)
|
||||
- `references/run-configuration.md` -- Complete TOML run configuration reference (sandbox, setup, hooks, LLM, vars, assets)
|
||||
- `references/example-workflows.md` -- 9 complete example workflows from simple to production-grade
|
||||
|
||||
{{user_input}}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
# DOT Language Reference for Arc Workflows
|
||||
|
||||
## Graph Structure
|
||||
|
||||
Every workflow is a `digraph` with a name and body. Three required elements:
|
||||
1. A `goal` attribute on the graph
|
||||
2. Exactly one `start` node with `shape=Mdiamond`
|
||||
3. Exactly one `exit` node with `shape=Msquare`
|
||||
|
||||
Only `digraph` is supported (not `graph` or `strict`).
|
||||
|
||||
```dot
|
||||
digraph MyWorkflow {
|
||||
graph [goal="Describe the objective"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
// nodes and edges here
|
||||
|
||||
start -> node1 -> node2 -> exit
|
||||
}
|
||||
```
|
||||
|
||||
Workflows **can include loops** (unlike DAGs).
|
||||
|
||||
## Value Types
|
||||
|
||||
| Type | Syntax | Examples |
|
||||
|---|---|---|
|
||||
| String | Double-quoted | `"Run tests"` |
|
||||
| Integer | Bare digits | `42`, `-1` |
|
||||
| Float | Digits with decimal | `3.14` |
|
||||
| Boolean | Keywords | `true`, `false` |
|
||||
| Duration | Integer + unit | `250ms`, `30s`, `15m`, `2h`, `1d` |
|
||||
| Bare string | Identifier | `claude-sonnet-4-6` |
|
||||
|
||||
Comments: `//` line and `/* */` block.
|
||||
|
||||
## Graph-Level Attributes
|
||||
|
||||
| Attribute | Type | Description |
|
||||
|---|---|---|
|
||||
| `goal` | String | Workflow objective (required) |
|
||||
| `rankdir` | Identifier | Layout: `LR` or `TB` |
|
||||
| `model_stylesheet` | String | CSS-like model assignment rules |
|
||||
| `default_max_retries` | Integer | Default retry count for all nodes (default: 3) |
|
||||
| `retry_target` | String | Default node to jump to on retry |
|
||||
| `fallback_retry_target` | String | Fallback retry target |
|
||||
| `default_fidelity` | String | Default fidelity for all nodes |
|
||||
| `default_thread` | String | Default thread ID for all nodes |
|
||||
| `max_node_visits` | Integer | Max visits per node (0 = unlimited) |
|
||||
| `stall_timeout` | Duration | Timeout for stalled workflows (default: 1800s) |
|
||||
|
||||
## Node Types (by shape)
|
||||
|
||||
| Shape | Handler | Purpose |
|
||||
|---|---|---|
|
||||
| `Mdiamond` | start | Entry point (exactly one) |
|
||||
| `Msquare` | exit | Terminal (exactly one) |
|
||||
| `box` (default) | agent | Multi-turn LLM with tool access |
|
||||
| `tab` | prompt | Single LLM call, no tools |
|
||||
| `parallelogram` | command | Execute a shell script |
|
||||
| `hexagon` | human | Human-in-the-loop decision gate |
|
||||
| `diamond` | conditional | Route based on conditions |
|
||||
| `component` | parallel | Fan-out to concurrent branches |
|
||||
| `tripleoctagon` | parallel.fan_in | Merge parallel branch results |
|
||||
| `insulator` | wait | Pause for a duration |
|
||||
| `house` | stack.manager_loop | Sub-workflow orchestration |
|
||||
|
||||
### Agent Nodes (box, default)
|
||||
|
||||
Multi-turn LLM with tools (shell, read_file, write_file, grep, glob, web_search, web_fetch, edit_file).
|
||||
|
||||
Key attributes: `prompt`, `reasoning_effort` (low/medium/high), `max_tokens`, `fidelity`, `thread_id`, `timeout`, `backend` (api or cli), `model`, `provider`, `project_memory`.
|
||||
|
||||
### Prompt Nodes (tab)
|
||||
|
||||
Single LLM call, no tool use. Same attributes as agent but never invokes tools.
|
||||
|
||||
### Command Nodes (parallelogram)
|
||||
|
||||
Run a shell script. Attributes: `script` (shell command), `language` ("shell" or "python").
|
||||
|
||||
### Human Nodes (hexagon)
|
||||
|
||||
Pause for human choice. Edge labels define options. Supports:
|
||||
- Keyboard accelerators: `[A] Approve`, `A) Approve`, `A - Approve`
|
||||
- Freeform input: `freeform=true` on an edge
|
||||
- Default on timeout: `human.default_choice`
|
||||
|
||||
### Conditional Nodes (diamond)
|
||||
|
||||
Route execution based on conditions. Must have multiple outgoing edges with `condition` attributes. No prompt attribute.
|
||||
|
||||
### Parallel Fan-Out (component)
|
||||
|
||||
Attributes: `join_policy` (wait_all, first_success), `max_parallel` (default: 4).
|
||||
|
||||
### Fan-In / Merge (tripleoctagon)
|
||||
|
||||
Collects results from parallel branches. Results available as `parallel_results.json`.
|
||||
|
||||
### Wait Nodes (insulator)
|
||||
|
||||
Attribute: `duration` (e.g. `"30s"`, `"2m"`).
|
||||
|
||||
### Sub-workflow (house)
|
||||
|
||||
Attributes: `stack.child_dotfile`, `stack.child_dot_source`, `manager.max_cycles` (default: 1000), `manager.poll_interval` (default: 45s), `manager.stop_condition`.
|
||||
|
||||
## Common Node Attributes
|
||||
|
||||
| Attribute | Description |
|
||||
|---|---|
|
||||
| `label` | Display name |
|
||||
| `class` | Space-separated classes for stylesheet targeting |
|
||||
| `max_visits` | Max times this node can execute |
|
||||
| `goal_gate` | When `true`, workflow fails if this node doesn't succeed |
|
||||
| `max_retries` | Override default retry count |
|
||||
| `retry_policy` | Preset: `none`, `standard`, `aggressive`, `linear`, `patient` |
|
||||
| `retry_target` | Node ID to jump to on retry |
|
||||
| `auto_status` | Auto-generate status updates |
|
||||
|
||||
## Edges and Transitions
|
||||
|
||||
After each node, Arc evaluates outgoing edges in priority order:
|
||||
1. **Condition match** -- edges with `condition`, highest `weight` wins
|
||||
2. **Preferred label** -- from human gate or LLM routing directive
|
||||
3. **Suggested next** -- node suggests a next node ID
|
||||
4. **Unconditional fallback** -- edges without conditions, `weight` tiebreak
|
||||
|
||||
Edge attributes: `label`, `condition`, `weight` (higher wins, default: 0), `fidelity`, `thread_id`, `loop_restart`.
|
||||
|
||||
### Condition Grammar
|
||||
|
||||
```
|
||||
Expr ::= OrExpr
|
||||
OrExpr ::= AndExpr ('||' AndExpr)*
|
||||
AndExpr ::= UnaryExpr ('&&' UnaryExpr)*
|
||||
UnaryExpr ::= '!' UnaryExpr | Clause
|
||||
Clause ::= Key Op Value | Key
|
||||
```
|
||||
|
||||
Operators: `=`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `matches`.
|
||||
|
||||
Common patterns:
|
||||
- `condition="outcome=success"` -- stage succeeded
|
||||
- `condition="outcome=fail"` -- stage failed
|
||||
- `condition="context.tests_passed=true"` -- check context variable
|
||||
|
||||
## Model Stylesheets
|
||||
|
||||
CSS-like syntax for assigning models to nodes:
|
||||
|
||||
```dot
|
||||
graph [model_stylesheet="
|
||||
* { model: claude-haiku-4-5;}
|
||||
.coding { model: claude-sonnet-4-6;}
|
||||
#review { model: gemini-3.1-pro-preview;}
|
||||
"]
|
||||
```
|
||||
|
||||
Selectors by specificity (low to high): `*` (universal, 0), shape name (1), `.class` (2), `#nodeid` (3). Higher specificity wins. Same specificity: last rule wins. Explicit node attributes override stylesheets.
|
||||
|
||||
Properties: `model`, `provider` (optional — auto-inferred from the model catalog), `reasoning_effort`, `backend`.
|
||||
|
||||
**Critical:** Use semicolons between properties (e.g. `model: foo; provider: bar;`).
|
||||
|
||||
## Variables
|
||||
|
||||
Define in `[vars]` section of TOML. Expanded into DOT source before parsing with `$variable` syntax. Undefined variables raise an error. Escape literal `$` with `$$`. Built-in: `$goal`.
|
||||
|
||||
## External Prompt Files
|
||||
|
||||
Reference external files with `prompt="@path/to/file.md"` (path relative to DOT file).
|
||||
|
||||
## Subgraphs
|
||||
|
||||
Group nodes visually and apply scoped defaults:
|
||||
|
||||
```dot
|
||||
subgraph cluster_impl {
|
||||
label = "Implementation"
|
||||
node [thread_id="impl", fidelity="full"]
|
||||
plan [label="Plan"]
|
||||
implement [label="Implement"]
|
||||
}
|
||||
```
|
||||
|
||||
When a subgraph has a `label`, it's converted to a CSS class applied to all nodes within.
|
||||
|
||||
## Validation Rules
|
||||
|
||||
Enforced at parse time:
|
||||
- Exactly one start node and one exit node
|
||||
- All nodes reachable from start
|
||||
- No incoming edges to start, no outgoing edges from exit
|
||||
- Edge targets reference existing nodes
|
||||
- Condition expressions parse correctly
|
||||
- Stylesheet syntax is valid
|
||||
- LLM nodes have a `prompt` attribute
|
||||
- `@file` references point to existing files
|
||||
- Conditional (diamond) nodes have multiple outgoing edges with conditions
|
||||
|
||||
## Retry Policies
|
||||
|
||||
| Preset | Attempts | Backoff |
|
||||
|---|---|---|
|
||||
| `none` | 1 | No retries |
|
||||
| `standard` | 5 | 5s initial, 2x exponential |
|
||||
| `aggressive` | 5 | 500ms initial, 2x exponential |
|
||||
| `linear` | 3 | 500ms fixed |
|
||||
| `patient` | 3 | 2s initial, 3x exponential |
|
||||
|
||||
## Fidelity Levels
|
||||
|
||||
Controls how much prior context is passed to a node:
|
||||
|
||||
| Value | Behavior |
|
||||
|---|---|
|
||||
| `compact` | Structured summary (default) |
|
||||
| `full` | Complete context |
|
||||
| `summary:high` | Detailed summary |
|
||||
| `summary:medium` | Moderate summary |
|
||||
| `summary:low` | Brief summary |
|
||||
| `truncate` | Minimal -- only goal and run ID |
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
# Example Workflows
|
||||
|
||||
Use these as starting points. Choose the simplest topology that fits the requirements.
|
||||
|
||||
## 1. Linear Pipeline (simplest)
|
||||
|
||||
One-shot prompt, no tools:
|
||||
|
||||
```dot
|
||||
digraph Hello {
|
||||
graph [goal="Write a haiku about software workflows"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
compose [label="Compose", prompt="Write a haiku (5-7-5 syllable) about software workflows. Output only the haiku, nothing else.", shape=tab, reasoning_effort="low"]
|
||||
|
||||
start -> compose -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Command-Then-Analyze Pipeline
|
||||
|
||||
Shell command feeds into LLM analysis:
|
||||
|
||||
```dot
|
||||
digraph Pipeline {
|
||||
graph [goal="Analyze the current directory and suggest improvements"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
scan [label="Scan Files", shape=parallelogram, script="find . -maxdepth 2 -type f | head -30"]
|
||||
analyze [label="Analyze", prompt="Review the file listing from the previous step. Identify what kind of project this is and summarize its structure in 3-4 bullet points.", shape=tab, reasoning_effort="low"]
|
||||
suggest [label="Suggest", prompt="Based on the analysis, suggest 3 concrete improvements to the project structure. Be specific and actionable.", shape=tab, reasoning_effort="low"]
|
||||
|
||||
start -> scan -> analyze -> suggest -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Implement-Test-Fix Loop
|
||||
|
||||
Agent writes code, command validates, conditional routes back on failure:
|
||||
|
||||
```dot
|
||||
digraph BranchLoop {
|
||||
graph [goal="Create a Python script that passes its test suite"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
plan [label="Plan", prompt="Plan a small Python script (fizzbuzz.py) and a test file (test_fizzbuzz.py) using pytest. Describe what you will create.", shape=tab, reasoning_effort="low"]
|
||||
implement [label="Implement", prompt="Create fizzbuzz.py and test_fizzbuzz.py as planned. Write the files to disk."]
|
||||
validate [label="Validate", shape=parallelogram, script="python3 -m pytest test_fizzbuzz.py -v 2>&1 || true"]
|
||||
gate [shape=diamond, label="Tests passing?"]
|
||||
|
||||
start -> plan -> implement -> validate -> gate
|
||||
gate -> exit [label="Pass", condition="outcome=success"]
|
||||
gate -> implement [label="Fix"]
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Human Approval Gate
|
||||
|
||||
Draft, get human approval, then apply:
|
||||
|
||||
```dot
|
||||
digraph HumanGate {
|
||||
graph [goal="Propose and implement a README improvement"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
draft [label="Draft Proposal", prompt="Read the README.md (or note its absence). Propose a specific improvement. Describe your proposed changes clearly but do NOT make any changes yet.", shape=tab]
|
||||
approve [label="Approve Changes?", shape=hexagon]
|
||||
apply [label="Apply Changes", prompt="Apply the proposed README changes that were approved."]
|
||||
skip [label="Skip", prompt="Acknowledged. No changes made.", shape=tab, reasoning_effort="low"]
|
||||
|
||||
start -> draft -> approve
|
||||
approve -> apply [label="[A] Approve"]
|
||||
approve -> skip [label="[S] Skip"]
|
||||
apply -> exit
|
||||
skip -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Plan-Approve-Implement with Revision Loop
|
||||
|
||||
```dot
|
||||
digraph PlanImplement {
|
||||
graph [goal="Plan, approve, implement, and simplify a change"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
plan [label="Plan", prompt="Analyze the goal and codebase. Write a clear, step-by-step implementation plan to plan.md. Include what files will change and why.", reasoning_effort="high"]
|
||||
approve [shape=hexagon, label="Approve Plan"]
|
||||
implement [label="Implement", prompt="Read plan.md and implement every step. Make all the code changes described in the plan."]
|
||||
simplify [label="Simplify", prompt="Review the changes just made. Simplify and clean up the code without changing behavior."]
|
||||
|
||||
start -> plan -> approve
|
||||
approve -> implement [label="[A] Approve"]
|
||||
approve -> plan [label="[R] Revise"]
|
||||
implement -> simplify -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Parallel Fan-Out Review
|
||||
|
||||
Multiple independent analyses merged into a synthesis:
|
||||
|
||||
```dot
|
||||
digraph Parallel {
|
||||
graph [goal="Perform a multi-perspective code review"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
fork [label="Fork Analysis", shape=component, join_policy="wait_all"]
|
||||
|
||||
security [label="Security Audit", prompt="Examine the codebase for security concerns: hardcoded secrets, injection risks, unsafe dependencies. List findings as bullet points.", shape=tab, reasoning_effort="low"]
|
||||
architecture [label="Architecture Review", prompt="Assess the codebase architecture: separation of concerns, dependency structure, modularity. List findings as bullet points.", shape=tab, reasoning_effort="low"]
|
||||
quality [label="Code Quality", prompt="Check code quality: naming conventions, dead code, test coverage gaps, error handling. List findings as bullet points.", shape=tab, reasoning_effort="low"]
|
||||
|
||||
merge [label="Merge Findings", shape=tripleoctagon]
|
||||
report [label="Final Report", prompt="Synthesize the security, architecture, and code quality findings into a prioritized summary report with top 5 action items.", shape=tab]
|
||||
|
||||
start -> fork
|
||||
fork -> security
|
||||
fork -> architecture
|
||||
fork -> quality
|
||||
security -> merge
|
||||
architecture -> merge
|
||||
quality -> merge
|
||||
merge -> report -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Multi-Model with Stylesheet
|
||||
|
||||
Different models for different roles:
|
||||
|
||||
```dot
|
||||
digraph MultiModel {
|
||||
graph [
|
||||
goal="Build and review a utility function using multiple models",
|
||||
model_stylesheet="
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-6;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-6;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
spec [label="Write Spec", prompt="Write a brief spec for a TypeScript string utility module with 3 functions: slugify, truncate, and capitalize. Output the spec only.", shape=tab]
|
||||
implement [label="Implement", prompt="Implement the TypeScript string utility module from the spec. Write it to string-utils.ts.", class="coding"]
|
||||
test [label="Write Tests", prompt="Write tests for the string utility module using Bun's test runner. Write to string-utils.test.ts.", class="coding"]
|
||||
review [label="Code Review", prompt="Review the implementation and tests. Check for edge cases, type safety, and correctness. Provide a brief verdict.", shape=tab]
|
||||
|
||||
start -> spec -> implement -> test -> review -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Multi-Provider Ensemble
|
||||
|
||||
Independent opinions from multiple providers, then synthesize:
|
||||
|
||||
```dot
|
||||
digraph Ensemble {
|
||||
graph [
|
||||
goal="Get independent opinions from multiple providers, then synthesize",
|
||||
model_stylesheet="
|
||||
#opus { model: claude-opus-4-6; }
|
||||
#gemini { model: gemini-3.1-pro-preview;}
|
||||
#codex { model: gpt-5.3-codex; }
|
||||
#synth { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
fork [label="Fan Out", shape=component, join_policy="wait_all"]
|
||||
|
||||
opus [label="Opus", prompt="Analyze the goal. Provide your independent assessment and recommendations. Be thorough.", shape=tab]
|
||||
gemini [label="Gemini", prompt="Analyze the goal. Provide your independent assessment and recommendations. Be thorough.", shape=tab]
|
||||
codex [label="Codex", prompt="Analyze the goal. Provide your independent assessment and recommendations. Be thorough.", shape=tab]
|
||||
|
||||
merge [label="Merge", shape=tripleoctagon]
|
||||
synth [label="Synthesize", prompt="You have received independent analyses from three different models. Compare their perspectives: identify consensus, highlight disagreements, and synthesize the strongest ideas into a single coherent recommendation.", shape=tab]
|
||||
|
||||
start -> fork
|
||||
fork -> opus
|
||||
fork -> gemini
|
||||
fork -> codex
|
||||
opus -> merge
|
||||
gemini -> merge
|
||||
codex -> merge
|
||||
merge -> synth -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Production Implement-and-Simplify with Verification
|
||||
|
||||
Full pipeline with toolchain checks, lint loops, and verification gates:
|
||||
|
||||
```dot
|
||||
digraph ImplementAndSimplify {
|
||||
graph [
|
||||
goal="Implement and simplify",
|
||||
model_stylesheet="
|
||||
* { backend: api; model: claude-opus-4-6;}
|
||||
"
|
||||
]
|
||||
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 2>&1", max_retries=0]
|
||||
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -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."]
|
||||
simplify [label="Simplify", prompt="Review the changes just made. Simplify and clean up the code without changing behavior."]
|
||||
verify [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 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 and test failures.", max_visits=3]
|
||||
|
||||
start -> toolchain
|
||||
toolchain -> preflight_compile [condition="outcome=success"]
|
||||
toolchain -> exit
|
||||
preflight_compile -> preflight_lint [condition="outcome=success"]
|
||||
preflight_compile -> exit
|
||||
preflight_lint -> implement [condition="outcome=success"]
|
||||
preflight_lint -> fix_lints
|
||||
fix_lints -> preflight_lint
|
||||
implement -> simplify -> verify
|
||||
verify -> exit [condition="outcome=success"]
|
||||
verify -> fixup
|
||||
fixup -> verify
|
||||
}
|
||||
```
|
||||
|
||||
Paired TOML:
|
||||
|
||||
```toml
|
||||
version = 1
|
||||
graph = "workflow.fabro"
|
||||
|
||||
[sandbox]
|
||||
provider = "local"
|
||||
|
||||
[sandbox.local]
|
||||
worktree_mode = "always"
|
||||
```
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
# Run Configuration (TOML) Reference
|
||||
|
||||
The TOML file configures how a workflow is executed. It is separate from the DOT graph which defines the workflow structure.
|
||||
|
||||
## Minimal Config
|
||||
|
||||
```toml
|
||||
version = 1
|
||||
graph = "workflow.fabro"
|
||||
goal = "Implement the login feature"
|
||||
```
|
||||
|
||||
**Required:** `version` (must be 1), `graph` (path relative to TOML file's directory).
|
||||
|
||||
## All Sections
|
||||
|
||||
### Top-level
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `version` | Integer | Must be `1` |
|
||||
| `graph` | String | Path to DOT file (relative to TOML) |
|
||||
| `goal` | String | Override workflow goal |
|
||||
| `directory` | String | Working directory for the run |
|
||||
|
||||
### `[llm]`
|
||||
|
||||
```toml
|
||||
[llm]
|
||||
model = "claude-opus-4-6"
|
||||
provider = "anthropic"
|
||||
|
||||
[llm.fallbacks]
|
||||
anthropic = ["gemini-3.1-pro-preview", "gpt-5.2"]
|
||||
openai = ["claude-sonnet-4-6"]
|
||||
```
|
||||
|
||||
### `[setup]`
|
||||
|
||||
Sequential shell commands run before the workflow starts:
|
||||
|
||||
```toml
|
||||
[setup]
|
||||
commands = ["npm install", "npm run build"]
|
||||
timeout_ms = 300000
|
||||
```
|
||||
|
||||
### `[sandbox]`
|
||||
|
||||
```toml
|
||||
[sandbox]
|
||||
provider = "local" # local, docker, daytona, exe
|
||||
preserve = false
|
||||
devcontainer = false
|
||||
|
||||
[sandbox.local]
|
||||
worktree_mode = "always" # always, auto, never
|
||||
|
||||
[sandbox.daytona]
|
||||
auto_stop_interval = "30m"
|
||||
|
||||
[sandbox.daytona.snapshot]
|
||||
name = "my-snapshot"
|
||||
cpu = 4
|
||||
memory = 8
|
||||
disk = 20
|
||||
dockerfile = { path = "Dockerfile" }
|
||||
|
||||
[sandbox.env]
|
||||
NODE_ENV = "test"
|
||||
API_KEY = "${env.API_KEY}" # passthrough from host
|
||||
```
|
||||
|
||||
### `[vars]`
|
||||
|
||||
Variables expanded into DOT source before parsing:
|
||||
|
||||
```toml
|
||||
[vars]
|
||||
language = "typescript"
|
||||
framework = "react"
|
||||
```
|
||||
|
||||
Used in DOT as `$language`, `$framework`.
|
||||
|
||||
### `[checkpoint]`
|
||||
|
||||
```toml
|
||||
[checkpoint]
|
||||
exclude_globs = ["node_modules/**", ".git/**"]
|
||||
```
|
||||
|
||||
### `[assets]`
|
||||
|
||||
```toml
|
||||
[assets]
|
||||
include = ["output/**", "*.png"]
|
||||
```
|
||||
|
||||
### `[pull_request]`
|
||||
|
||||
```toml
|
||||
[pull_request]
|
||||
enabled = true
|
||||
draft = true
|
||||
```
|
||||
|
||||
### `[[hooks]]`
|
||||
|
||||
Lifecycle event hooks:
|
||||
|
||||
```toml
|
||||
[[hooks]]
|
||||
event = "stage_complete"
|
||||
type = "command"
|
||||
command = "echo 'Stage done'"
|
||||
|
||||
[[hooks]]
|
||||
event = "run_complete"
|
||||
type = "http"
|
||||
url = "https://example.com/webhook"
|
||||
```
|
||||
|
||||
Events: run_start, run_complete, run_failed, stage_start, stage_complete, stage_failed, stage_retrying, edge_selected, parallel_start, parallel_complete, sandbox_ready, sandbox_cleanup, checkpoint_saved, pre_tool_use, post_tool_use, post_tool_use_failure.
|
||||
|
||||
### `[mcp_servers]`
|
||||
|
||||
```toml
|
||||
[mcp_servers.my-server]
|
||||
transport = "stdio"
|
||||
command = "node"
|
||||
args = ["server.js"]
|
||||
```
|
||||
|
||||
## Precedence (first match wins)
|
||||
|
||||
Node-level attribute > Stylesheet > TOML config > CLI flags > Server defaults > DOT graph attributes > Built-in defaults
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
fabro preflight workflow.toml # validate without executing
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue