move docs -> docs-internal

This commit is contained in:
Bryan Helmkamp 2026-03-02 09:59:00 -05:00
parent 4849dd8488
commit a456672959
9 changed files with 240 additions and 6908 deletions

View file

@ -215,39 +215,178 @@ pub async fn get_run_graph(
pub async fn get_run_retro(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
Path(id): Path<String>,
) -> Response {
// Return a demo retro as JSON
(StatusCode::OK, Json(serde_json::json!({
"run_id": "run-1",
"pipeline_name": "implement",
"goal": "Add rate limiting to auth endpoints",
"smoothness": "smooth",
"intent": "Implement token-bucket rate limiting on /auth/login and /auth/register to prevent brute-force attacks.",
"outcome": "Rate limiter deployed with configurable per-IP limits. Integration tests added. Redis-backed counter with sliding window.",
"stages": [
{"stage_id": "detect-drift", "stage_label": "Detect Drift", "status": "completed", "duration_ms": 72000, "retries": 0, "cost": 0.48, "files_touched": ["src/middleware/rate-limit.ts"]},
{"stage_id": "propose-changes", "stage_label": "Propose Changes", "status": "completed", "duration_ms": 154000, "retries": 0, "cost": 1.12, "files_touched": ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts"]},
{"stage_id": "review-changes", "stage_label": "Review Changes", "status": "completed", "duration_ms": 45000, "retries": 0, "cost": 0.31, "files_touched": []},
{"stage_id": "apply-changes", "stage_label": "Apply Changes", "status": "completed", "duration_ms": 118000, "retries": 0, "cost": 0.87, "files_touched": ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts", "tests/rate-limit.test.ts"]}
],
"stats": {
"total_duration_ms": 389000,
"total_cost": 2.78,
"total_retries": 0,
"files_touched": ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts", "tests/rate-limit.test.ts"],
"stages_completed": 4,
"stages_failed": 0
},
"learnings": [
{"category": "repo", "text": "Redis client is initialized lazily in src/infra/redis.ts -- reuse existing connection pool."},
{"category": "code", "text": "Auth middleware chain order matters: rate-limit must run before JWT validation."}
],
"friction_points": [],
"open_items": [
{"kind": "follow_up", "description": "Add rate-limit headers (X-RateLimit-Remaining) to response."}
]
}))).into_response()
let retro = match id.as_str() {
"run-1" => serde_json::json!({
"run_id": "run-1",
"workflow_name": "implement",
"goal": "Add rate limiting to auth endpoints",
"timestamp": "2026-02-28T14:32:00Z",
"smoothness": "smooth",
"intent": "Implement token-bucket rate limiting on /auth/login and /auth/register to prevent brute-force attacks.",
"outcome": "Rate limiter deployed with configurable per-IP limits. Integration tests added. Redis-backed counter with sliding window.",
"stages": [
{"stage_id": "detect-drift", "stage_label": "Detect Drift", "status": "completed", "duration_ms": 72000, "retries": 0, "cost": 0.48, "files_touched": ["src/middleware/rate-limit.ts"]},
{"stage_id": "propose-changes", "stage_label": "Propose Changes", "status": "completed", "duration_ms": 154000, "retries": 0, "cost": 1.12, "files_touched": ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts"]},
{"stage_id": "review-changes", "stage_label": "Review Changes", "status": "completed", "duration_ms": 45000, "retries": 0, "cost": 0.31, "files_touched": []},
{"stage_id": "apply-changes", "stage_label": "Apply Changes", "status": "completed", "duration_ms": 118000, "retries": 0, "cost": 0.87, "files_touched": ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts", "tests/rate-limit.test.ts"]}
],
"stats": {
"total_duration_ms": 389000,
"total_cost": 2.78,
"total_retries": 0,
"files_touched": ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts", "tests/rate-limit.test.ts"],
"stages_completed": 4,
"stages_failed": 0
},
"learnings": [
{"category": "repo", "text": "Redis client is initialized lazily in src/infra/redis.ts -- reuse existing connection pool."},
{"category": "code", "text": "Auth middleware chain order matters: rate-limit must run before JWT validation."}
],
"friction_points": [],
"open_items": [
{"kind": "follow_up", "description": "Add rate-limit headers (X-RateLimit-Remaining) to response."}
]
}),
"run-2" => serde_json::json!({
"run_id": "run-2",
"workflow_name": "implement",
"goal": "Migrate to React Router v7",
"timestamp": "2026-02-28T10:15:00Z",
"smoothness": "bumpy",
"intent": "Upgrade react-router from v6 to v7, updating all route definitions and loader/action patterns to the new API.",
"outcome": "Migration completed but required 3 retries in the apply stage due to breaking changes in nested route handling. All routes now use the v7 data API.",
"stages": [
{"stage_id": "detect-drift", "stage_label": "Detect Drift", "status": "completed", "duration_ms": 95000, "retries": 0, "cost": 0.62, "files_touched": ["package.json"]},
{"stage_id": "propose-changes", "stage_label": "Propose Changes", "status": "completed", "duration_ms": 312000, "retries": 1, "cost": 2.45, "notes": "First proposal missed nested outlet patterns. Retry produced correct migration.", "files_touched": ["src/routes.ts", "src/app.tsx", "src/routes/dashboard.tsx", "src/routes/settings.tsx"]},
{"stage_id": "review-changes", "stage_label": "Review Changes", "status": "completed", "duration_ms": 88000, "retries": 0, "cost": 0.54, "files_touched": []},
{"stage_id": "apply-changes", "stage_label": "Apply Changes", "status": "completed", "duration_ms": 480000, "retries": 3, "cost": 3.21, "notes": "Type errors in nested layouts required multiple correction passes.", "files_touched": ["src/routes.ts", "src/app.tsx", "src/routes/dashboard.tsx", "src/routes/settings.tsx", "src/routes/profile.tsx", "tests/routes.test.tsx"]}
],
"stats": {
"total_duration_ms": 975000,
"total_cost": 6.82,
"total_retries": 4,
"files_touched": ["package.json", "src/routes.ts", "src/app.tsx", "src/routes/dashboard.tsx", "src/routes/settings.tsx", "src/routes/profile.tsx", "tests/routes.test.tsx"],
"stages_completed": 4,
"stages_failed": 0
},
"learnings": [
{"category": "workflow", "text": "Framework migration tasks benefit from running type-check after each stage, not just at the end."},
{"category": "code", "text": "React Router v7 outlets require explicit type annotations for loader data in nested routes."},
{"category": "tool", "text": "The codemod tool missed JSX spread patterns -- manual fixup was needed."}
],
"friction_points": [
{"kind": "retry", "description": "Nested route outlet types were incorrect on first 3 attempts.", "stage_id": "apply-changes"},
{"kind": "wrong_approach", "description": "Initially tried to keep v6 compat layer, which created more issues than a clean migration.", "stage_id": "propose-changes"}
],
"open_items": [
{"kind": "tech_debt", "description": "Leftover v6 compat shims in src/utils/router-compat.ts should be deleted."},
{"kind": "test_gap", "description": "No E2E coverage for the new nested layout error boundaries."}
]
}),
"run-6" => serde_json::json!({
"run_id": "run-6",
"workflow_name": "implement",
"goal": "Add dark mode toggle",
"timestamp": "2026-02-27T16:45:00Z",
"smoothness": "effortless",
"intent": "Add a theme toggle component to the dashboard header with system/light/dark options, persisting preference to localStorage.",
"outcome": "Dark mode toggle shipped with smooth CSS transitions. All existing components already used CSS variables, so no style refactoring was needed.",
"stages": [
{"stage_id": "detect-drift", "stage_label": "Detect Drift", "status": "completed", "duration_ms": 42000, "retries": 0, "cost": 0.28, "files_touched": []},
{"stage_id": "propose-changes", "stage_label": "Propose Changes", "status": "completed", "duration_ms": 98000, "retries": 0, "cost": 0.71, "files_touched": ["src/components/ThemeToggle.tsx", "src/hooks/useTheme.ts"]},
{"stage_id": "apply-changes", "stage_label": "Apply Changes", "status": "completed", "duration_ms": 76000, "retries": 0, "cost": 0.52, "files_touched": ["src/components/ThemeToggle.tsx", "src/hooks/useTheme.ts", "src/layouts/Header.tsx"]}
],
"stats": {
"total_duration_ms": 216000,
"total_cost": 1.51,
"total_retries": 0,
"files_touched": ["src/components/ThemeToggle.tsx", "src/hooks/useTheme.ts", "src/layouts/Header.tsx"],
"stages_completed": 3,
"stages_failed": 0
},
"learnings": [
{"category": "repo", "text": "CSS variables are defined in src/styles/tokens.css and already support dark values."}
],
"friction_points": [],
"open_items": []
}),
"run-3" => serde_json::json!({
"run_id": "run-3",
"workflow_name": "fix_build",
"goal": "Fix config parsing for nested values",
"timestamp": "2026-02-27T09:20:00Z",
"smoothness": "struggled",
"intent": "Fix TOML config parser to handle deeply nested table arrays, which was causing silent data loss on certain pipeline configs.",
"outcome": "Root cause identified as incorrect recursion depth limit in the TOML walker. Fix applied but exposed a second bug in default value merging that required additional changes.",
"stages": [
{"stage_id": "investigate", "stage_label": "Investigate", "status": "completed", "duration_ms": 340000, "retries": 2, "cost": 1.85, "notes": "First investigation looked at wrong parser path. Second attempt found the actual recursion limit.", "files_touched": ["src/config/parser.ts", "src/config/defaults.ts"]},
{"stage_id": "propose-fix", "stage_label": "Propose Fix", "status": "completed", "duration_ms": 210000, "retries": 1, "cost": 1.42, "files_touched": ["src/config/parser.ts", "src/config/defaults.ts", "src/config/merge.ts"]},
{"stage_id": "apply-fix", "stage_label": "Apply Fix", "status": "completed", "duration_ms": 185000, "retries": 1, "cost": 1.15, "failure_reason": "Initial fix broke the default value merging path. Required a second pass.", "files_touched": ["src/config/parser.ts", "src/config/defaults.ts", "src/config/merge.ts", "tests/config-parser.test.ts"]},
{"stage_id": "verify", "stage_label": "Verify", "status": "completed", "duration_ms": 95000, "retries": 0, "cost": 0.55, "files_touched": []}
],
"stats": {
"total_duration_ms": 830000,
"total_cost": 4.97,
"total_retries": 4,
"files_touched": ["src/config/parser.ts", "src/config/defaults.ts", "src/config/merge.ts", "tests/config-parser.test.ts"],
"stages_completed": 4,
"stages_failed": 0
},
"learnings": [
{"category": "code", "text": "TOML walker in parser.ts has a hardcoded depth limit of 8 -- needs to be configurable."},
{"category": "code", "text": "Default merging in merge.ts uses shallow spread, which silently drops nested keys."},
{"category": "workflow", "text": "Bug fix pipelines should include a regression test stage before verification."}
],
"friction_points": [
{"kind": "wrong_approach", "description": "Initial investigation focused on the YAML compatibility layer instead of the TOML parser.", "stage_id": "investigate"},
{"kind": "retry", "description": "Fix introduced a regression in default value merging that required rework.", "stage_id": "apply-fix"},
{"kind": "ambiguity", "description": "Config schema docs were outdated, making it unclear which nesting depth was intended."}
],
"open_items": [
{"kind": "tech_debt", "description": "Remove the hardcoded depth limit in src/config/parser.ts and make it configurable."},
{"kind": "investigation", "description": "Audit other parsers for similar shallow-spread bugs in merging logic."},
{"kind": "test_gap", "description": "No tests for configs nested deeper than 4 levels."}
]
}),
"run-8" => serde_json::json!({
"run_id": "run-8",
"workflow_name": "implement",
"goal": "Implement webhook retry logic",
"timestamp": "2026-02-26T11:00:00Z",
"smoothness": "smooth",
"intent": "Add exponential backoff retry logic for failed webhook deliveries with configurable max attempts and dead-letter queue.",
"outcome": "Webhook retry system implemented with exponential backoff (base 2s, max 5 retries). Failed deliveries route to SQS dead-letter queue. Dashboard shows retry status.",
"stages": [
{"stage_id": "detect-drift", "stage_label": "Detect Drift", "status": "completed", "duration_ms": 55000, "retries": 0, "cost": 0.35, "files_touched": []},
{"stage_id": "propose-changes", "stage_label": "Propose Changes", "status": "completed", "duration_ms": 178000, "retries": 0, "cost": 1.28, "files_touched": ["src/webhooks/retry.ts", "src/webhooks/dlq.ts", "src/webhooks/dispatcher.ts"]},
{"stage_id": "review-changes", "stage_label": "Review Changes", "status": "completed", "duration_ms": 62000, "retries": 0, "cost": 0.41, "files_touched": []},
{"stage_id": "apply-changes", "stage_label": "Apply Changes", "status": "completed", "duration_ms": 145000, "retries": 1, "cost": 1.05, "notes": "Minor type fix needed on retry delay calculation.", "files_touched": ["src/webhooks/retry.ts", "src/webhooks/dlq.ts", "src/webhooks/dispatcher.ts", "tests/webhook-retry.test.ts"]}
],
"stats": {
"total_duration_ms": 440000,
"total_cost": 3.09,
"total_retries": 1,
"files_touched": ["src/webhooks/retry.ts", "src/webhooks/dlq.ts", "src/webhooks/dispatcher.ts", "tests/webhook-retry.test.ts"],
"stages_completed": 4,
"stages_failed": 0
},
"learnings": [
{"category": "repo", "text": "SQS client wrapper is in src/infra/sqs.ts with pre-configured DLQ ARNs per environment."},
{"category": "code", "text": "Webhook dispatcher already had a hook point for retry logic via the onFailure callback."}
],
"friction_points": [
{"kind": "retry", "description": "Retry delay formula had an off-by-one in the exponent calculation.", "stage_id": "apply-changes"}
],
"open_items": [
{"kind": "follow_up", "description": "Add webhook retry metrics to the Grafana dashboard."},
{"kind": "follow_up", "description": "Document the DLQ reprocessing procedure in the runbook."}
]
}),
_ => serde_json::json!(null),
};
(StatusCode::OK, Json(retro)).into_response()
}
// ── Workflows ──────────────────────────────────────────────────────────
@ -562,7 +701,14 @@ mod runs {
CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(68.0) },
CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(192.0) },
CheckRun { name: "integration-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(334.0) },
CheckRun { name: "e2e / chrome".into(), status: CheckRunStatus::Success, duration_secs: Some(262.0) },
CheckRun { name: "e2e / firefox".into(), status: CheckRunStatus::Success, duration_secs: Some(285.0) },
CheckRun { name: "build".into(), status: CheckRunStatus::Success, duration_secs: Some(121.0) },
CheckRun { name: "deploy-preview".into(), status: CheckRunStatus::Success, duration_secs: Some(93.0) },
CheckRun { name: "security-scan".into(), status: CheckRunStatus::Skipped, duration_secs: None },
CheckRun { name: "performance".into(), status: CheckRunStatus::Success, duration_secs: Some(138.0) },
CheckRun { name: "bundle-size".into(), status: CheckRunStatus::Success, duration_secs: Some(34.0) },
CheckRun { name: "accessibility".into(), status: CheckRunStatus::Success, duration_secs: Some(72.0) },
],
elapsed_secs: Some(259200.0), elapsed_warning: Some(true),
resources: None, comments: Some(7),
@ -578,6 +724,8 @@ mod runs {
CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(48.0) },
CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(116.0) },
CheckRun { name: "build".into(), status: CheckRunStatus::Success, duration_secs: Some(82.0) },
CheckRun { name: "coverage".into(), status: CheckRunStatus::Success, duration_secs: Some(124.0) },
CheckRun { name: "bundle-size".into(), status: CheckRunStatus::Skipped, duration_secs: None },
],
elapsed_secs: Some(3900.0), elapsed_warning: Some(false),
resources: None, comments: Some(2),
@ -1050,7 +1198,7 @@ mod retros {
goal: "Add rate limiting to auth endpoints".into(),
timestamp: "2026-02-28T14:32:00Z".into(),
smoothness: Some(SmoothnessRating::Smooth),
stats: RetroStats { total_duration_ms: 389000, total_cost: Some(2.78), total_retries: 0, files_touched: vec!["src/middleware/rate-limit.ts".into()], stages_completed: 4, stages_failed: 0 },
stats: RetroStats { total_duration_ms: 389000, total_cost: Some(2.78), total_retries: 0, files_touched: vec!["src/middleware/rate-limit.ts".into(), "src/routes/auth.ts".into(), "src/config.ts".into(), "tests/rate-limit.test.ts".into()], stages_completed: 4, stages_failed: 0 },
friction_point_count: 0,
},
RetroListItem {
@ -1058,7 +1206,7 @@ mod retros {
goal: "Migrate to React Router v7".into(),
timestamp: "2026-02-28T10:15:00Z".into(),
smoothness: Some(SmoothnessRating::Bumpy),
stats: RetroStats { total_duration_ms: 975000, total_cost: Some(6.82), total_retries: 4, files_touched: vec!["src/routes.ts".into()], stages_completed: 4, stages_failed: 0 },
stats: RetroStats { total_duration_ms: 975000, total_cost: Some(6.82), total_retries: 4, files_touched: vec!["package.json".into(), "src/routes.ts".into(), "src/app.tsx".into(), "src/routes/dashboard.tsx".into(), "src/routes/settings.tsx".into(), "src/routes/profile.tsx".into(), "tests/routes.test.tsx".into()], stages_completed: 4, stages_failed: 0 },
friction_point_count: 2,
},
RetroListItem {
@ -1066,7 +1214,7 @@ mod retros {
goal: "Add dark mode toggle".into(),
timestamp: "2026-02-27T16:45:00Z".into(),
smoothness: Some(SmoothnessRating::Effortless),
stats: RetroStats { total_duration_ms: 216000, total_cost: Some(1.51), total_retries: 0, files_touched: vec!["src/components/ThemeToggle.tsx".into()], stages_completed: 3, stages_failed: 0 },
stats: RetroStats { total_duration_ms: 216000, total_cost: Some(1.51), total_retries: 0, files_touched: vec!["src/components/ThemeToggle.tsx".into(), "src/hooks/useTheme.ts".into(), "src/layouts/Header.tsx".into()], stages_completed: 3, stages_failed: 0 },
friction_point_count: 0,
},
RetroListItem {
@ -1074,7 +1222,7 @@ mod retros {
goal: "Fix config parsing for nested values".into(),
timestamp: "2026-02-27T09:20:00Z".into(),
smoothness: Some(SmoothnessRating::Struggled),
stats: RetroStats { total_duration_ms: 830000, total_cost: Some(4.97), total_retries: 4, files_touched: vec!["src/config/parser.ts".into()], stages_completed: 4, stages_failed: 0 },
stats: RetroStats { total_duration_ms: 830000, total_cost: Some(4.97), total_retries: 4, files_touched: vec!["src/config/parser.ts".into(), "src/config/defaults.ts".into(), "src/config/merge.ts".into(), "tests/config-parser.test.ts".into()], stages_completed: 4, stages_failed: 0 },
friction_point_count: 3,
},
RetroListItem {
@ -1082,7 +1230,7 @@ mod retros {
goal: "Implement webhook retry logic".into(),
timestamp: "2026-02-26T11:00:00Z".into(),
smoothness: Some(SmoothnessRating::Smooth),
stats: RetroStats { total_duration_ms: 440000, total_cost: Some(3.09), total_retries: 1, files_touched: vec!["src/webhooks/retry.ts".into()], stages_completed: 4, stages_failed: 0 },
stats: RetroStats { total_duration_ms: 440000, total_cost: Some(3.09), total_retries: 1, files_touched: vec!["src/webhooks/retry.ts".into(), "src/webhooks/dlq.ts".into(), "src/webhooks/dispatcher.ts".into(), "tests/webhook-retry.test.ts".into()], stages_completed: 4, stages_failed: 0 },
friction_point_count: 1,
},
]
@ -1106,13 +1254,15 @@ mod sessions {
sessions: vec![
SessionListItem { id: "s3".into(), title: "Migrate to React Router v7".into(), repo: "web-dashboard".into(), time: "1d ago".into() },
SessionListItem { id: "s4".into(), title: "Add dark mode toggle".into(), repo: "web-dashboard".into(), time: "1d ago".into() },
SessionListItem { id: "s5".into(), title: "Update OpenAPI spec for v3".into(), repo: "api-server".into(), time: "1d ago".into() },
],
},
SessionGroup {
label: "Previous 7 days".into(),
sessions: vec![
SessionListItem { id: "s6".into(), title: "Terraform module for Redis cluster".into(), repo: "infrastructure".into(), time: "3d ago".into() },
SessionListItem { id: "s7".into(), title: "Add workflow run event types".into(), repo: "shared-types".into(), time: "5d ago".into() },
SessionListItem { id: "s7".into(), title: "Add pipeline event types".into(), repo: "shared-types".into(), time: "5d ago".into() },
SessionListItem { id: "s8".into(), title: "Implement webhook retry logic".into(), repo: "api-server".into(), time: "6d ago".into() },
],
},
]
@ -1123,12 +1273,48 @@ mod sessions {
"s1" => Some(SessionDetail {
id: "s1".into(), title: "Add rate limiting to auth endpoints".into(), repo: "api-server".into(), model: "Opus 4.6".into(),
turns: vec![
SessionTurn { kind: SessionTurnKind::User, content: Some("Add rate limiting to the auth endpoints. Use a sliding window approach with Redis, 10 requests per minute per IP.".into()), date: Some("Feb 28".into()), tools: vec![] },
SessionTurn { kind: SessionTurnKind::User, content: Some("Add rate limiting to the auth endpoints. We're getting hit with brute force attempts on /api/auth/login and /api/auth/register. Use a sliding window approach with Redis, 10 requests per minute per IP.".into()), date: Some("Feb 28".into()), tools: vec![] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("I'll implement sliding window rate limiting using Redis. Let me first look at the existing auth routes and middleware setup.".into()), date: None, tools: vec![] },
SessionTurn { kind: SessionTurnKind::Tool, content: None, date: None, tools: vec![
ToolUse { tool_name: "read_file".into(), args: r#"{ "path": "src/routes/auth.ts" }"#.into(), result: "import { Router } from \"express\";\n// ...".into() },
ToolUse { tool_name: "read_file".into(), args: r#"{ "path": "src/routes/auth.ts" }"#.into(), result: "import { Router } from \"express\";\nimport { loginHandler } from \"../handlers/auth\";\nimport { registerHandler } from \"../handlers/auth\";\n\nconst router = Router();\nrouter.post(\"/login\", loginHandler);\nrouter.post(\"/register\", registerHandler);\n\nexport default router;".into() },
ToolUse { tool_name: "read_file".into(), args: r#"{ "path": "src/middleware/index.ts" }"#.into(), result: "export { authMiddleware } from \"./auth\";\nexport { corsMiddleware } from \"./cors\";\nexport { loggingMiddleware } from \"./logging\";".into() },
ToolUse { tool_name: "search_files".into(), args: r#"{ "pattern": "redis", "path": "src/" }"#.into(), result: "src/lib/redis.ts:1: import Redis from \"ioredis\";\nsrc/lib/redis.ts:3: export const redis = new Redis(process.env.REDIS_URL);".into() },
] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("Good — there's already a Redis client at `src/lib/redis.ts`. I'll create a rate limiting middleware and apply it to the auth routes.".into()), date: None, tools: vec![] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("Good \u{2014} there's already a Redis client at `src/lib/redis.ts`. I'll create a rate limiting middleware using the sliding window algorithm and apply it to the auth routes.\n\nHere's the plan:\n1. Create `src/middleware/rate-limit.ts` with sliding window logic\n2. Apply it to `/login` and `/register` routes\n3. Return `429 Too Many Requests` when the limit is exceeded".into()), date: None, tools: vec![] },
SessionTurn { kind: SessionTurnKind::Tool, content: None, date: None, tools: vec![
ToolUse { tool_name: "write_file".into(), args: r#"{ "path": "src/middleware/rate-limit.ts" }"#.into(), result: "File written: src/middleware/rate-limit.ts (47 lines)".into() },
ToolUse { tool_name: "edit_file".into(), args: r#"{ "path": "src/routes/auth.ts", "operations": [{ "type": "insert", "line": 3 }] }"#.into(), result: "Applied 1 edit to src/routes/auth.ts".into() },
ToolUse { tool_name: "edit_file".into(), args: r#"{ "path": "src/middleware/index.ts", "operations": [{ "type": "append" }] }"#.into(), result: "Applied 1 edit to src/middleware/index.ts".into() },
] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("Done. I've created the rate limiter and wired it up:\n\n- **`src/middleware/rate-limit.ts`** \u{2014} Sliding window rate limiter using Redis sorted sets. Each request adds a timestamped entry, expired entries are pruned, and the count is checked against the limit.\n- **`src/routes/auth.ts`** \u{2014} Applied `rateLimit({ windowMs: 60_000, max: 10 })` to both `/login` and `/register`.\n- **`src/middleware/index.ts`** \u{2014} Re-exported the new middleware.\n\nWhen the limit is exceeded, clients get a `429` response with a `Retry-After` header indicating seconds until the window resets.".into()), date: None, tools: vec![] },
],
}),
"s2" => Some(SessionDetail {
id: "s2".into(), title: "Fix config parsing for nested values".into(), repo: "cli-tools".into(), model: "Sonnet 4.6".into(),
turns: vec![
SessionTurn { kind: SessionTurnKind::User, content: Some("The CLI crashes when parsing nested TOML config values like [database.connection]. Can you debug and fix this?".into()), date: Some("Feb 28".into()), tools: vec![] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("Let me look at the config parser to understand how nested keys are handled.".into()), date: None, tools: vec![] },
SessionTurn { kind: SessionTurnKind::Tool, content: None, date: None, tools: vec![
ToolUse { tool_name: "read_file".into(), args: r#"{ "path": "src/config/parser.ts" }"#.into(), result: "export function parseConfig(raw: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const line of raw.split(\"\\n\")) {\n const match = line.match(/^(\\w+)\\s*=\\s*(.+)$/);\n if (match) result[match[1]] = match[2].trim();\n }\n return result;\n}".into() },
] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("Found the issue. The parser uses a flat `Record<string, string>` and the regex `^(\\w+)` only matches simple keys \u{2014} it can't handle TOML section headers like `[database.connection]` or dotted keys like `database.host`.\n\nI'll rewrite the parser to support nested sections and dotted keys, producing a properly nested object.".into()), date: None, tools: vec![] },
SessionTurn { kind: SessionTurnKind::Tool, content: None, date: None, tools: vec![
ToolUse { tool_name: "write_file".into(), args: r#"{ "path": "src/config/parser.ts" }"#.into(), result: "File written: src/config/parser.ts (62 lines)".into() },
ToolUse { tool_name: "run_command".into(), args: r#"{ "command": "npm test -- --grep 'config parser'" }"#.into(), result: "PASS src/config/__tests__/parser.test.ts\n config parser\n \u{2713} parses flat keys (2ms)\n \u{2713} parses section headers (1ms)\n \u{2713} parses dotted keys (1ms)\n \u{2713} handles nested sections (1ms)\n\nTest Suites: 1 passed\nTests: 4 passed".into() },
] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("Fixed. The parser now:\n\n1. Tracks the current section header (e.g. `[database.connection]`)\n2. Constructs nested objects from dotted paths\n3. Handles both `key = value` within sections and bare `dotted.key = value` syntax\n\nAll 4 tests pass, including the new nested section case.".into()), date: None, tools: vec![] },
],
}),
"s3" => Some(SessionDetail {
id: "s3".into(), title: "Migrate to React Router v7".into(), repo: "web-dashboard".into(), model: "Opus 4.6".into(),
turns: vec![
SessionTurn { kind: SessionTurnKind::User, content: Some("Help me migrate our app from React Router v6 to v7. We're using createBrowserRouter with data loaders.".into()), date: Some("Feb 26".into()), tools: vec![] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("I'll audit your current router setup and identify what needs to change for v7. Let me scan the codebase.".into()), date: None, tools: vec![] },
SessionTurn { kind: SessionTurnKind::Tool, content: None, date: None, tools: vec![
ToolUse { tool_name: "search_files".into(), args: r#"{ "pattern": "createBrowserRouter" }"#.into(), result: "src/main.tsx:5: import { createBrowserRouter } from \"react-router-dom\";\nsrc/main.tsx:8: const router = createBrowserRouter([".into() },
ToolUse { tool_name: "read_file".into(), args: r#"{ "path": "package.json" }"#.into(), result: "\"react-router-dom\": \"^6.22.0\"".into() },
] },
SessionTurn { kind: SessionTurnKind::Assistant, content: Some("You're on React Router 6.22. The migration to v7 involves:\n\n1. Replace `react-router-dom` with `react-router` (single package)\n2. Update `createBrowserRouter` import path\n3. Migrate data loaders to the new `Route.LoaderArgs` type\n4. Update any `useLoaderData` calls to be type-safe\n\nLet me make these changes.".into()), date: None, tools: vec![] },
],
}),
_ => None,
@ -1166,7 +1352,7 @@ mod settings {
fields: vec![
SettingField { key: "org_name".into(), label: "Organization name".into(), value: "Acme Corp".into(), type_: SettingFieldType::Text, options: vec![], description: None },
SettingField { key: "default_branch".into(), label: "Default branch".into(), value: "main".into(), type_: SettingFieldType::Text, options: vec![], description: None },
SettingField { key: "timezone".into(), label: "Timezone".into(), value: "America/New_York".into(), type_: SettingFieldType::Select, options: vec!["America/New_York".into(), "UTC".into(), "Europe/London".into()], description: None },
SettingField { key: "timezone".into(), label: "Timezone".into(), value: "America/New_York".into(), type_: SettingFieldType::Select, options: vec!["America/New_York".into(), "America/Chicago".into(), "America/Denver".into(), "America/Los_Angeles".into(), "UTC".into(), "Europe/London".into(), "Europe/Berlin".into(), "Asia/Tokyo".into()], description: None },
SettingField { key: "auto_cancel".into(), label: "Auto-cancel superseded runs".into(), value: "true".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
],
},
@ -1176,28 +1362,38 @@ mod settings {
SettingField { key: "github_org".into(), label: "GitHub organization".into(), value: "acme-corp".into(), type_: SettingFieldType::Text, options: vec![], description: None },
SettingField { key: "clone_protocol".into(), label: "Clone protocol".into(), value: "SSH".into(), type_: SettingFieldType::Select, options: vec!["SSH".into(), "HTTPS".into()], description: None },
SettingField { key: "auto_merge".into(), label: "Auto-merge when checks pass".into(), value: "false".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "delete_branch".into(), label: "Delete branch after merge".into(), value: "true".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "commit_signing".into(), label: "Require commit signing".into(), value: "false".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
],
},
SettingGroup {
id: "compute".into(), name: "Compute".into(), description: "Resource allocation and execution environment.".into(),
fields: vec![
SettingField { key: "default_cpu".into(), label: "Default CPU".into(), value: "4".into(), type_: SettingFieldType::Select, options: vec!["2".into(), "4".into(), "8".into(), "16".into()], description: None },
SettingField { key: "default_memory".into(), label: "Default memory".into(), value: "8 GB".into(), type_: SettingFieldType::Select, options: vec!["4 GB".into(), "8 GB".into(), "16 GB".into()], description: None },
SettingField { key: "default_memory".into(), label: "Default memory".into(), value: "8 GB".into(), type_: SettingFieldType::Select, options: vec!["4 GB".into(), "8 GB".into(), "16 GB".into(), "32 GB".into()], description: None },
SettingField { key: "max_parallel".into(), label: "Max parallel runs".into(), value: "10".into(), type_: SettingFieldType::Text, options: vec![], description: None },
SettingField { key: "timeout_minutes".into(), label: "Run timeout (minutes)".into(), value: "120".into(), type_: SettingFieldType::Text, options: vec![], description: None },
SettingField { key: "gpu_enabled".into(), label: "GPU acceleration".into(), value: "false".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
],
},
SettingGroup {
id: "notifications".into(), name: "Notifications".into(), description: "Alerts and notification delivery preferences.".into(),
fields: vec![
SettingField { key: "slack_webhook".into(), label: "Slack webhook URL".into(), value: "https://hooks.slack.com/services/T00/B00/xxxx".into(), type_: SettingFieldType::Text, options: vec![], description: None },
SettingField { key: "notify_on_failure".into(), label: "Notify on failure".into(), value: "true".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "notify_on_success".into(), label: "Notify on success".into(), value: "false".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "notify_on_approval".into(), label: "Notify on approval needed".into(), value: "true".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "email_digest".into(), label: "Daily email digest".into(), value: "false".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
],
},
SettingGroup {
id: "security".into(), name: "Security".into(), description: "Access control and security policies.".into(),
fields: vec![
SettingField { key: "sso_provider".into(), label: "SSO provider".into(), value: "Okta".into(), type_: SettingFieldType::Select, options: vec!["None".into(), "Okta".into(), "Azure AD".into()], description: None },
SettingField { key: "sso_provider".into(), label: "SSO provider".into(), value: "Okta".into(), type_: SettingFieldType::Select, options: vec!["None".into(), "Okta".into(), "Azure AD".into(), "Google Workspace".into(), "OneLogin".into()], description: None },
SettingField { key: "mfa_required".into(), label: "Require MFA".into(), value: "true".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "session_timeout".into(), label: "Session timeout".into(), value: "8 hours".into(), type_: SettingFieldType::Select, options: vec!["1 hour".into(), "4 hours".into(), "8 hours".into(), "24 hours".into(), "7 days".into()], description: None },
SettingField { key: "audit_log".into(), label: "Audit logging".into(), value: "true".into(), type_: SettingFieldType::Toggle, options: vec![], description: None },
SettingField { key: "ip_allowlist".into(), label: "IP allowlist".into(), value: "".into(), type_: SettingFieldType::Text, options: vec![], description: Some("Comma-separated CIDRs. Leave empty to allow all.".into()) },
],
},
]

View file

@ -1,473 +0,0 @@
# Arc API Design — Draft
Comprehensive API to support all data needs of `arc-web`.
## Current state
**Existing Rust API** (`crates/arc-api/src/server.rs`) exposes 10 endpoints under `/pipelines`.
The OpenAPI spec (`openapi/arc-api.yaml`) uses `/runs`. The router needs to be renamed to match.
**arc-web** uses zero real API calls today. Every page renders hardcoded mock data from:
- `app/data/runs.ts` — 10 runs in 4 kanban columns
- `app/data/retros.ts` — 5 retros with full stage/learning/friction data
- `app/data/verifications.ts` — 8 categories, 30 controls, performance metrics, recent results
- `routes/workflow-detail.tsx` — 4 workflow definitions (config TOML + graph DOT)
- `routes/run-stages.tsx` — hardcoded conversation turns (system/assistant/tool)
- `routes/run-files-changed.tsx` — hardcoded file diffs + checkpoints
- `routes/run-usage.tsx` — hardcoded token/cost usage per stage
- `routes/start.tsx` — hardcoded projects, branches, session history
- `routes/session-detail.tsx` — 3 sessions with chat turns
- `routes/insights.tsx` — saved SQL queries + history
- `routes/settings.tsx` — 5 setting groups with fields
---
## Endpoint inventory
### 1. Runs
Already partially exists. Needs enrichment to carry the data the UI actually renders.
| Method | Path | Description | Status |
|--------|------|-------------|--------|
| `GET` | `/runs` | List runs (board view data) | **extend** |
| `POST` | `/runs` | Start a new run | exists |
| `GET` | `/runs/{id}` | Full run detail | **extend** |
| `POST` | `/runs/{id}/cancel` | Cancel a running run | exists |
| `GET` | `/runs/{id}/events` | SSE event stream | exists |
| `GET` | `/runs/{id}/questions` | Pending questions | exists |
| `POST` | `/runs/{id}/questions/{qid}/answer` | Submit answer | exists |
| `GET` | `/runs/{id}/checkpoint` | Checkpoint data | exists |
| `GET` | `/runs/{id}/context` | Context key-value map | exists |
| `GET` | `/runs/{id}/graph` | Workflow graph SVG | exists |
| `GET` | `/runs/{id}/retro` | Retrospective | exists |
| `GET` | `/runs/{id}/stages` | List stages with status/duration | **new** |
| `GET` | `/runs/{id}/stages/{stageId}/turns` | Conversation transcript for a stage | **new** |
| `GET` | `/runs/{id}/files` | File diffs grouped by checkpoint | **new** |
| `GET` | `/runs/{id}/usage` | Token/cost breakdown by stage + model | **new** |
| `GET` | `/runs/{id}/verifications` | Verification results for this run | **new** |
| `GET` | `/runs/{id}/configuration` | Run configuration (TOML) | **new** |
| `POST` | `/runs/{id}/steer` | Submit steering guidance on a file line | **new** |
#### `GET /runs` response shape
```jsonc
[
{
"id": "run-1",
"repo": "api-server",
"title": "Add rate limiting to auth endpoints",
"workflow": "implement",
"status": "working", // working | pending | review | merge
"number": null, // PR number, if opened
"additions": null,
"deletions": null,
"checks": [ // CI check runs
{ "name": "lint", "status": "success", "duration_secs": 23 }
],
"elapsed_secs": 420,
"elapsed_warning": false,
"resources": "4 CPU / 8 GB",
"comments": 0,
"question": null, // pending human-in-the-loop question
"sandbox_id": "sb-a1b2c3d4"
}
]
```
#### `GET /runs/{id}/stages` response shape
```jsonc
[
{
"id": "detect-drift",
"name": "Detect Drift",
"status": "completed", // completed | running | pending | failed
"duration_secs": 72,
"dot_id": "detect" // node ID in workflow graph (for annotations)
}
]
```
#### `GET /runs/{id}/stages/{stageId}/turns` response shape
```jsonc
[
{ "kind": "system", "content": "You are a drift detection agent..." },
{ "kind": "assistant", "content": "I'll start by loading..." },
{
"kind": "tool",
"tools": [
{ "tool_name": "read_file", "args": "{ \"path\": \"...\" }", "result": "..." }
]
}
]
```
#### `GET /runs/{id}/files?checkpoint=all` response shape
```jsonc
{
"checkpoints": [
{ "id": "all", "label": "All changes" },
{ "id": "cp-4", "label": "Checkpoint 4 — Apply Changes" }
],
"files": [
{
"old_file": { "name": "src/commands/run.ts", "contents": "..." },
"new_file": { "name": "src/commands/run.ts", "contents": "..." }
}
],
"stats": { "additions": 567, "deletions": 234 }
}
```
#### `GET /runs/{id}/usage` response shape
```jsonc
{
"stages": [
{
"stage": "Detect Drift",
"model": "Opus 4.6",
"input_tokens": 12480,
"output_tokens": 3210,
"runtime_secs": 72,
"cost": 0.48
}
],
"totals": {
"runtime_secs": 389,
"input_tokens": 71540,
"output_tokens": 21080,
"cost": 2.26
},
"by_model": [
{ "model": "Opus 4.6", "stages": 2, "input_tokens": 33780, "output_tokens": 9690, "cost": 1.35 }
]
}
```
#### `GET /runs/{id}/verifications` response shape
```jsonc
[
{
"name": "Traceability",
"question": "Do we understand what this change is and why we're making it?",
"status": "pass",
"controls": [
{
"name": "Motivation",
"description": "Origin of proposal identified",
"type": "ai", // ai | automated | analysis | ai-analysis | null
"status": "pass" // pass | fail | na
}
]
}
]
```
---
### 2. Workflows
Entirely new. Supports the `/workflows` list page, detail page with definition/diagram/runs tabs.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/workflows` | List all workflows |
| `GET` | `/workflows/{name}` | Workflow detail (config + graph DOT) |
| `POST` | `/workflows/{name}/runs` | Trigger a run for this workflow |
| `GET` | `/workflows/{name}/runs` | List runs filtered to this workflow |
#### `GET /workflows` response shape
```jsonc
[
{
"name": "Fix Build",
"slug": "fix_build",
"filename": "fix_build.dot",
"last_run": "2 hours ago",
"schedule": null, // e.g. "Daily at 09:00"
"next_run": null // e.g. "Starts in 3 hours"
}
]
```
#### `GET /workflows/{name}` response shape
```jsonc
{
"title": "Fix Build",
"slug": "fix_build",
"filename": "fix_build.dot",
"description": "Automatically diagnoses and fixes CI build failures...",
"config": "version = 1\ntask = ...", // raw TOML
"graph": "digraph fix_build { ... }" // raw DOT source
}
```
---
### 3. Verifications
Entirely new. Supports the `/verifications` list and `/verifications/:slug` detail pages.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/verifications` | List all verification categories + controls |
| `GET` | `/verifications/{slug}` | Control detail (performance, evaluations, control detail, recent results) |
#### `GET /verifications` response shape
```jsonc
[
{
"name": "Traceability",
"question": "Do we understand what this change is and why we're making it?",
"controls": [
{
"name": "Motivation",
"slug": "motivation",
"description": "Origin of proposal identified",
"type": "ai",
"mode": "active", // active | evaluate | disabled
"f1": 0.87,
"pass_at_1": 0.82,
"evaluations": ["pass", "pass", "fail", "pass", ...]
}
]
}
]
```
#### `GET /verifications/{slug}` response shape
```jsonc
{
"control": {
"name": "Motivation",
"slug": "motivation",
"description": "Origin of proposal identified",
"type": "ai",
"category": "Traceability"
},
"performance": {
"mode": "active",
"f1": 0.87,
"pass_at_1": 0.82,
"evaluations": ["pass", "pass", "fail", ...]
},
"control_detail": {
"description": "Verifies that every change traces back to a clear origin...",
"checks": ["PR body or linked issue explains why...", ...],
"pass_example": "PR links to JIRA-1234...",
"fail_example": "PR description is empty..."
},
"recent_results": [
{
"run_id": "run-047",
"run_title": "PR #312 — Add OAuth2 PKCE flow",
"workflow": "code_review",
"result": "pass",
"timestamp": "2h ago"
}
],
"siblings": [
{ "name": "Specifications", "slug": "specifications", "type": "ai", "mode": "active" }
]
}
```
---
### 4. Retros
Already partial (per-run). Needs a top-level list endpoint.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/retros` | List all retros across runs |
| `GET` | `/runs/{id}/retro` | Retro for a specific run (exists) |
#### `GET /retros` response shape
```jsonc
[
{
"run_id": "run-1",
"pipeline_name": "implement",
"goal": "Add rate limiting to auth endpoints",
"timestamp": "2026-02-28T14:32:00Z",
"smoothness": "smooth",
"stats": {
"total_duration_ms": 389000,
"total_cost": 2.78,
"total_retries": 0,
"files_touched": [...],
"stages_completed": 4,
"stages_failed": 0
},
"friction_point_count": 0
}
]
```
---
### 5. Sessions
Entirely new. Supports the `/start` and `/sessions/:id` pages (chat-like interaction).
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/sessions` | List sessions grouped by recency |
| `POST` | `/sessions` | Create a new session |
| `GET` | `/sessions/{id}` | Session detail with full turn history |
| `POST` | `/sessions/{id}/messages` | Send a user message |
| `GET` | `/sessions/{id}/events` | SSE stream for live assistant responses |
#### `GET /sessions` response shape
```jsonc
[
{
"label": "Today",
"sessions": [
{ "id": "s1", "title": "Add rate limiting to auth endpoints", "repo": "api-server", "time": "2h ago" }
]
}
]
```
#### `GET /sessions/{id}` response shape
```jsonc
{
"id": "s1",
"title": "Add rate limiting to auth endpoints",
"repo": "api-server",
"model": "Opus 4.6",
"turns": [
{ "kind": "user", "content": "Add rate limiting...", "date": "Feb 28" },
{ "kind": "assistant", "content": "I'll implement..." },
{ "kind": "tool", "tools": [{ "tool_name": "read_file", "args": "...", "result": "..." }] }
]
}
```
---
### 6. Insights
Entirely new. Supports the `/insights` SQL query editor.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/insights/queries` | List saved queries |
| `POST` | `/insights/queries` | Save a query |
| `PUT` | `/insights/queries/{id}` | Update a saved query |
| `DELETE` | `/insights/queries/{id}` | Delete a saved query |
| `POST` | `/insights/execute` | Execute a SQL query, return results |
| `GET` | `/insights/history` | Query execution history |
#### `POST /insights/execute` request/response
```jsonc
// Request
{ "sql": "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1" }
// Response
{
"columns": ["workflow_name", "count"],
"rows": [["implement", 42], ["fix_build", 18]],
"elapsed": 0.342,
"row_count": 6
}
```
---
### 7. Settings
Entirely new. Supports the `/settings` page.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/settings` | Get all setting groups with current values |
#### `GET /settings` response shape
```jsonc
[
{
"id": "general",
"name": "General",
"description": "Core platform settings and defaults.",
"fields": [
{
"key": "org_name",
"label": "Organization name",
"value": "Acme Corp",
"type": "text"
},
{
"key": "timezone",
"label": "Timezone",
"value": "America/New_York",
"type": "select",
"options": ["America/New_York", "UTC", ...]
}
]
}
]
```
---
### 8. Projects (for Start page)
Supports the project/branch picker on `/start`.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/projects` | List available projects |
| `GET` | `/projects/{id}/branches` | List branches for a project |
---
## Summary: total endpoints
| Domain | Existing | New | Total |
|--------|----------|-----|-------|
| Runs | 10 | 7 | 17 |
| Workflows | 0 | 4 | 4 |
| Verifications | 0 | 2 | 2 |
| Retros | 0 | 1 | 1 |
| Sessions | 0 | 5 | 5 |
| Insights | 0 | 6 | 6 |
| Settings | 0 | 1 | 1 |
| Projects | 0 | 2 | 2 |
| **Total** | **10** | **28** | **38** |
## Priority order
1. **Runs** — extend existing endpoints to carry full board data (status columns, checks, diffs, usage, stages)
2. **Workflows** — needed for the core workflow management UI
3. **Sessions** — the primary interaction model (chat UX)
4. **Verifications** — central to the quality assurance story
5. **Retros** — lightweight list endpoint on top of existing per-run retro
6. **Insights** — SQL query interface (requires query engine backend)
7. **Settings** — configuration CRUD
8. **Projects** — start page pickers
## Open questions
- Should `GET /runs` support filtering by status column, repo, workflow? The UI has search + repo filter + view toggle.
- Should file diffs be fetched per-checkpoint or all at once with checkpoint metadata?
- Is the insights SQL query engine in-process (SQLite) or a separate service?
- Should sessions use SSE for streaming assistant responses, or WebSocket?
- How should verification criteria definitions be managed — API-editable or config-file driven?
- Should the stage turn transcript be paginated for very long conversations?

View file

@ -1,180 +0,0 @@
# Arc Logging Strategy
Arc uses the `tracing` crate for structured, file-based logging. Logs write to `~/.arc/logs/YYYY-MM-DD.log`, controlled by the `ARC_LOG` env var (default: `info`). Logs are for **developers debugging issues after the fact** — they are not user-facing output.
Production runs at INFO level. INFO should be low-volume and high-signal — the summary of what happened. When something goes wrong, developers enable `ARC_LOG=debug` to get the full picture. DEBUG can be as verbose as needed since it's only turned on temporarily.
## When to Log
**Log at INFO (always on in production):**
- Lifecycle boundaries of top-level operations — session started/completed, pipeline started/completed, server ready
- Failures and warnings — every error/warn path, with enough context to diagnose the cause
- Keep it sparse: a typical agent session should produce ~5-10 INFO lines
**Log at DEBUG (enabled on-demand for investigation):**
- Individual steps within an operation — each LLM request, each tool call, each pipeline node
- External interactions with detail — request parameters, response metadata, token counts
- Decision points — why a code path was taken (retry triggered, fallback used, config value resolved)
- State changes and intermediate results — config resolution, parsing outcomes
**Do not log:**
- Hot loops or per-token streaming events (use DEBUG only if truly needed for diagnosis)
- Data that belongs in user-facing output (`eprintln!` for CLI feedback, not tracing)
- Redundant information already captured by a parent event (if you logged "starting X", you don't need to log every sub-step at the same level)
- Events that are already traced via `EventEnum::trace()` — the event enums (`AgentEvent`, `PipelineEvent`, `ExecutionEnvEvent`) each have a `trace()` method called automatically at their emit site; do not add manual `info!`/`debug!` calls that duplicate what `trace()` already emits
- Wrapper/forwarding variants that re-emit an inner event — `PipelineEvent::Agent`, `PipelineEvent::ExecutionEnv`, and `AgentEvent::SubAgentEvent` are no-ops in `trace()` because the inner event is already traced at its origin
- Secrets, API keys, or auth tokens — even at DEBUG level
## Log Levels
### ERROR — Something failed and the operation cannot continue
The current operation is aborting. A human reviewing logs should investigate every ERROR.
```rust
error!(server = %name, error = %err, "MCP server failed to start");
error!(provider = %provider, status = %status, "LLM request failed after all retries");
```
### WARN — Something unexpected happened but execution continues
Degraded behavior, fallback paths, or conditions that might indicate a problem.
```rust
warn!(server = %name, "MCP server disconnected, removing tools");
warn!(attempt = attempt, max = max_retries, error = %err, "LLM request failed, retrying");
```
### INFO — The production log level
INFO is always on. It should tell you **what** happened at a high level: which operations started, which completed, and key outcomes. Think of INFO as the audit trail — enough to answer "what did the system do?" but not so much that it creates noise. A typical agent session should produce a handful of INFO lines, not hundreds.
```rust
info!(model = %model, "Starting agent session");
info!(server = %name, tools = tool_count, "MCP server ready");
info!(pipeline = %name, "Pipeline complete");
info!(turns = turn_count, tool_calls = tool_call_count, "Agent session complete");
```
### DEBUG — Turn this on when something goes wrong
DEBUG is off in production by default. Enable it with `ARC_LOG=debug` to investigate a specific issue. DEBUG events provide the **how** and **why**: request/response details, intermediate state, config resolution, individual steps within a larger operation. DEBUG can be verbose — that's fine, since it's only enabled temporarily.
```rust
debug!(model = %model, messages = msg_count, tools = tool_count, "Sending LLM request");
debug!(provider = %provider, input_tokens = input, output_tokens = output, "LLM response received");
debug!(tool = %name, duration_ms = elapsed, "Tool call complete");
debug!(path = %path.display(), "Loading workflow file");
debug!(env_var = "ANTHROPIC_API_KEY", "API key resolved from environment");
```
## How to Write a Log Event
### Message: describe what happened
The message string is a short, human-readable description. Use sentence fragments starting with a verb or noun. No variable interpolation in the message — put variable data in structured fields.
```rust
// Good — message is a fixed string, data is in fields
info!(server = %name, tools = tool_count, "MCP server ready");
// Bad — variable data interpolated into message string
info!("MCP server '{}' ready with {} tools", name, tool_count);
```
Fixed message strings make logs grepable and let tooling aggregate events by message.
### Fields: attach structured context
Fields are key-value pairs that make events queryable. Include enough context that the event is useful on its own without reading surrounding log lines.
**Field naming:**
- Use `snake_case` for field names
- Use consistent names across the codebase (see table below)
- Keep names short but unambiguous
**Common field names:**
| Field | Used for |
|-------|----------|
| `model` | LLM model identifier |
| `provider` | LLM provider name (anthropic, openai, gemini) |
| `server` | MCP server name |
| `tool` | Tool name being called |
| `turn` | Agent turn number |
| `attempt` | Retry attempt number |
| `error` | Error value on failure |
| `path` | File system path |
| `duration_ms` | Elapsed time in milliseconds |
| `input_tokens` | Token count for LLM input |
| `output_tokens` | Token count for LLM output |
**Field format specifiers:**
- `%` (Display) for user-readable values: `server = %name`, `error = %err`, `path = %path.display()`
- `?` (Debug) for internal/enum values: `level = ?params.level`, `status = ?response.status`
- No specifier for primitives: `tools = tool_count`, `attempt = 3`
### Examples by crate
**arc-agent:**
```rust
info!(model = %model, "Starting agent session");
info!(turns = turn_count, tool_calls = total_calls, "Agent session complete");
debug!(turn = turn_number, "Starting agent turn");
debug!(tool = %name, "Executing tool call");
debug!(tool = %name, duration_ms = elapsed, "Tool call complete");
warn!(tool = %name, error = %err, "Tool execution failed");
```
**arc-llm:**
```rust
debug!(provider = %provider, model = %model, messages = count, "Sending LLM request");
debug!(provider = %provider, model = %model, input_tokens = input, output_tokens = output, "LLM response received");
warn!(provider = %provider, attempt = n, error = %err, "Request failed, retrying");
error!(provider = %provider, error = %err, "Request failed after all retries");
```
**arc-workflows:**
```rust
info!(pipeline = %name, "Starting pipeline execution");
info!(pipeline = %name, nodes = count, "Pipeline complete");
debug!(node = %id, handler = %handler_type, "Executing pipeline node");
debug!(node = %id, duration_ms = elapsed, "Pipeline node complete");
```
**arc-mcp:**
```rust
info!(server = %name, tools = tool_count, "MCP server ready");
debug!(server = %name, transport = %transport_type, "Connecting to MCP server");
error!(server = %name, error = %err, "MCP server failed to start");
```
## Cross-Package Guidelines
Every crate that does meaningful work should emit tracing events. The `tracing` dependency is workspace-level — add it to any crate's `Cargo.toml` with:
```toml
tracing.workspace = true
```
The subscriber is initialized once in `arc-cli`. Library crates (`arc-agent`, `arc-llm`, etc.) only emit events — they never configure the subscriber. This means:
- Library crates import `tracing::{info, debug, warn, error}` and call the macros
- The events go nowhere in unit tests (this is fine — tests verify behavior, not log output)
- The events are captured by whatever subscriber the binary sets up
When adding tracing to a new crate, start with the boundaries: INFO for the start/end of top-level operations, DEBUG for the individual steps within them. When in doubt about the level, use DEBUG — it's easy to promote something to INFO later, but hard to demote a noisy INFO event without breaking someone's log monitoring.
## Event Enum Tracing
The domain event enums (`AgentEvent`, `PipelineEvent`, `ExecutionEnvEvent`) each implement a `pub fn trace(&self)` method (or `trace(&self, session_id: &str)` for `AgentEvent`) that emits a structured tracing log line per variant. This method is called automatically from each enum's emit site, so every emitted event produces a log line without any additional code at the call site.
**Rules for event tracing:**
- **Add tracing for new variants** by adding a match arm in the enum's `trace()` method. Choose the level based on the guidelines above (INFO for lifecycle boundaries, DEBUG for individual steps, WARN/ERROR for failures).
- **Do not add manual log calls at emit sites.** The `trace()` call in the emitter handles it. Adding `info!` or `debug!` next to an `emit()` call will double-log.
- **Wrapper variants are no-ops.** When one event enum wraps another (`PipelineEvent::Agent` wraps `AgentEvent`, `AgentEvent::SubAgentEvent` wraps a child `AgentEvent`), the wrapper's `trace()` arm is `{}` because the inner event was already traced at its origin. This prevents double-logging.
- **Streaming noise variants are no-ops.** `TextDelta` and `ToolCallOutputDelta` produce no log output — per-token events would flood the logs even at DEBUG level.

View file

@ -1,121 +0,0 @@
# Spec Format Reference
Common format shared by `specs/unified-llm-spec.md`, `specs/coding-agent-loop-spec.md` (agent crate), and `specs/arc-spec.md`.
---
## 1. Title and Opening Paragraph
Each spec starts with a level-1 heading (`# <Name> Specification`) followed immediately by a one-sentence summary paragraph that describes what the spec is, states it is **language-agnostic**, and says it is **designed to be implementable from scratch by any developer or coding agent in any programming language**. This framing signals the intended audience: an AI coding agent or a human developer doing a greenfield implementation.
## 2. Horizontal Rule + Table of Contents
A `---` separator follows the summary, then a `## Table of Contents` section with a numbered list of all top-level sections, each as a markdown anchor link (e.g., `[Overview and Goals](#1-overview-and-goals)`).
## 3. Numbered Top-Level Sections
All sections use the pattern `## N. Section Name` where N is a sequential integer. Subsections use `### N.M` (e.g., `### 2.3 Session Lifecycle`). This gives every concept a unique coordinate (e.g., "Section 5.4") for cross-referencing.
## 4. Section 1: Overview and Goals
Always the first section. Contains these standard subsections:
### 4.1 Problem Statement (1.1)
A prose description of the problem being solved. Written in concrete, opinionated terms. Explains *why* this thing needs to exist by describing the pain of not having it.
### 4.2 Design Principles (1.2)
A bulleted list of named principles, each formatted as **`Bold keyword.`** followed by an explanation. Examples: "Provider-agnostic.", "Streaming-first.", "Declarative pipelines.", "Hackable." These are prescriptive statements about how the system should behave, not aspirational goals.
### 4.3 Reference Open-Source Projects (1.3 or 1.4)
A list of existing projects that solve related problems. Each entry includes the project name, URL, language, and a description of what patterns to study from it. Explicitly stated as "not dependencies" -- inspiration sources only.
### 4.4 Architecture Diagram
An ASCII art box diagram showing the layers/components and how they connect. All three specs include at least one.
### 4.5 Relationship to Companion Specs
When the spec depends on another (agent depends on unified-llm; arc depends on agent), it states this explicitly with the types it imports and how the layering works.
## 5. Core Technical Sections
The middle sections define the system's data model, algorithms, and contracts using a consistent notation.
### 5.1 Pseudocode and Type Definitions
All code is written in a language-neutral pseudocode style:
- **Records** use `RECORD Name:` with indented fields as `field_name : Type -- comment`
- **Enums** use `ENUM Name:` with indented values
- **Interfaces** use `INTERFACE Name:` with method signatures
- **Functions** use `FUNCTION name(params) -> ReturnType:` with indented body
- **Control flow** uses `IF`, `ELSE`, `FOR EACH`, `WHILE`, `LOOP`, `BREAK`, `CONTINUE`, `RETURN`, `TRY/CATCH`
- Keywords are UPPERCASE: `APPEND`, `AWAIT_ALL`, `YIELD`, `NONE`
### 5.2 Type Convention
A standard set of type primitives is used across all three specs:
- `String`, `Integer`, `Float`, `Boolean`, `Bytes`, `Dict`
- `List<T>` for ordered collections
- `T | None` for optional values
- `T | U` for union types
- `Map<K, V>` for key-value stores
### 5.3 Tables
Heavy use of markdown tables for:
- **Attribute reference tables** with columns: Key, Type, Default, Description
- **Provider mapping tables** showing how one concept translates per-provider (OpenAI / Anthropic / Gemini)
- **Enum value tables** with Value and Meaning columns
### 5.4 Design Decision Rationale
When a non-obvious choice is made, it's explained inline with **bold "Why..." questions**. For example: "**Why two methods, not one.**" or "**Why provider-aligned toolsets instead of a universal tool set?**" These appear immediately after the design they justify, or collected in an appendix.
## 6. Out of Scope / Nice-to-Haves (optional)
A section explicitly listing features that are *intentionally excluded*, with an explanation of why each is out of scope and where in the architecture it could be added later. Prevents scope creep and signals extensibility points. (Present in the agent spec; not all specs include this.)
## 7. Definition of Done
Always the **last numbered section**. Opens with the standard sentence: "This section defines how to validate that an implementation of this spec is complete and correct. An implementation is done when every item is checked off."
### 7.1 Subsections by Feature Area
Each subsection (e.g., "Core Infrastructure", "DOT Parsing", "Provider Adapters") contains a markdown checklist (`- [ ]`) of specific, verifiable assertions.
### 7.2 Cross-Provider/Feature Parity Matrix
A markdown table where rows are test cases and columns are providers (or a single "Pass" column). Every cell is `[ ]`. Serves as a validation matrix ensuring nothing is missed.
### 7.3 Integration Smoke Test
The final subsection. Contains a pseudocode end-to-end test that exercises the major codepaths with real APIs/backends. Written as executable assertions (`ASSERT`), not prose. This is the "if this passes, you're done" test.
## 8. Appendices
After the Definition of Done, labeled as `## Appendix A/B/C/D: Title`. Used for:
- Reference material too detailed for the main spec (e.g., the `apply_patch` v4a format grammar)
- Complete attribute reference tables
- Error category taxonomies
- Design decision rationale (when there's a lot of it)
## 9. Cross-cutting Patterns
Several patterns repeat across all three specs:
**Escape hatches over false abstractions.** Each spec defines a clean unified model, then provides explicit escape hatches (`provider_options`, `type` attribute overrides, `CodergenBackend` interface) for cases the unified model doesn't cover. The escape hatches are documented, not hidden.
**Concrete defaults with override points.** Every configurable value has a stated default (e.g., `max_tool_rounds_per_input = 200`, `default_command_timeout_ms = 10000`). Nothing is left as "implementation-defined."
**Provider-specific mapping tables.** When behavior differs per LLM provider, a table shows the exact field/header/API mapping for each.
**Event-driven observability.** All three systems emit typed events for external consumption. Event kinds are defined as enums with clear semantics.
**Separation of concerns via interfaces.** Key extension points are defined as interfaces (`ProviderAdapter`, `ExecutionEnvironment`, `Handler`, `CodergenBackend`, `Interviewer`) that decouple the core from implementations.

View file

@ -1,23 +0,0 @@
# Arc
This repository contains [NLSpecs](#terminology) to build your own version of Arc to create your own software factory.
Although bringing your own agentic loop and unified LLM SDK is not required to build your own Arc, we highly recommend controlling the stack so you have a strong foundation.
## Specs
- [Arc Specification](./arc-spec.md)
- [Agent Specification](./coding-agent-loop-spec.md)
- [Unified LLM Client Specification](./unified-llm-spec.md)
## Building Arc
Supply the following prompt to a modern coding agent (Claude Code, Codex, OpenCode, Amp, Cursor, etc):
```
codeagent> Implement Arc as described by https://factory.strongdm.ai/
```
## Terminology
- **NLSpec** (Natural Language Spec): a human-readable spec intended to be directly usable by coding agents to implement/validate behavior.

View file

@ -1,324 +0,0 @@
# Agent CLI Specification
This document specifies a command-line interface for the coding agent library defined in the [Coding Agent Loop Specification](./coding-agent-loop-spec.md). It is a thin wrapper that configures and runs a `Session`, designed to be implementable from scratch by any developer or coding agent in any programming language.
---
## Table of Contents
1. [Overview and Goals](#1-overview-and-goals)
2. [Invocation](#2-invocation)
3. [Permission Model](#3-permission-model)
4. [Output](#4-output)
5. [Environment and Configuration](#5-environment-and-configuration)
6. [Exit Behavior](#6-exit-behavior)
7. [Definition of Done](#7-definition-of-done)
---
## 1. Overview and Goals
### 1.1 Problem Statement
The coding agent library (`Session`) is programmable-first: it gives host applications full control over the agentic loop. But a library alone forces every user to write a host application before they can point an agent at a task. Developers need a zero-ceremony way to run a coding agent from the terminal, and CI systems need a headless way to invoke one from a script.
The CLI is the simplest possible host application for `Session`. It translates command-line arguments into a `SessionConfig`, subscribes to the event stream for rendering, and exits when the session completes. It does not add concepts of its own -- no workspace management, no persistent identity, no project-level config files. Every feature lives in the library; the CLI is the wiring.
### 1.2 Design Principles
**Thin wrapper.** The CLI configures a `Session` and renders its events. All intelligence -- tool execution, loop detection, truncation, subagent spawning -- lives in the library. The CLI never duplicates library logic.
**Dual-mode.** Interactive by default (streaming output, tool approval prompts). Fully scriptable with flags (`--auto-approve`). Same binary, same flags, different defaults based on whether a TTY is attached.
**Single command.** No subcommands. `agent <prompt>` is the only invocation. This keeps the mental model flat and the `--help` output short.
**Opinionated defaults.** The CLI picks reasonable defaults (provider, model, permissions) so that the common case requires zero flags. Power users override with explicit flags.
### 1.3 Relationship to Companion Specs
The CLI depends on the [Coding Agent Loop Specification](./coding-agent-loop-spec.md) for all agent behavior. It uses `Session`, `SessionConfig`, `EventEmitter`, `EventKind`, and `ExecutionEnvironment` directly. LLM communication flows through the agent library's use of the [Unified LLM Client Specification](./unified-llm-spec.md); the CLI does not interact with the LLM client directly.
```
┌─────────────────────────────┐
│ agent CLI │
│ (this spec) │
│ - arg parsing │
│ - event rendering │
│ - permission prompts │
└──────────┬──────────────────┘
│ configures + runs
┌─────────────────────────────┐
│ Session (agent library) │
│ - agentic loop │
│ - tools, subagents │
│ - loop detection │
└──────────┬──────────────────┘
│ uses
┌─────────────────────────────┐
│ LLM Client (llm library) │
│ - provider adapters │
│ - streaming, retry │
└─────────────────────────────┘
```
---
## 2. Invocation
### 2.1 Usage
```
agent [OPTIONS] <PROMPT>
```
`PROMPT` is a required positional argument: the task for the agent to perform. No subcommands exist. No stdin reading, no REPL. If the user has a long prompt, they can quote it or use shell heredoc syntax.
### 2.2 Flags
| Flag | Type | Default | Description |
|---|---|---|---|
| `--provider` | `String` | `"anthropic"` | LLM provider: `anthropic`, `openai`, or `gemini`. |
| `--model` | `String` | Provider default | Model identifier. When omitted, uses the provider profile's default model. |
| `--permissions` | `Enum` | `read-write` | Permission level: `read-only`, `read-write`, or `full`. See [Section 3](#3-permission-model). |
| `--auto-approve` | `Boolean` | `false` | Skip all interactive approval prompts. Denied tools are hard-blocked instead. |
| `--debug` | `Boolean` | `false` | Dump raw LLM request/response payloads to stderr. |
**No other flags.** The CLI does not expose: LLM parameters (temperature, max_tokens), subagent configuration, turn limits, session resumption, context injection, dry-run mode, structured output, working directory override, or verbose levels. These are deliberate omissions to keep the surface area minimal.
### 2.3 Provider and Model Resolution
The `--provider` flag selects the provider profile from the agent library:
| `--provider` value | Profile | Default model |
|---|---|---|
| `anthropic` | `AnthropicProfile` | Profile's default |
| `openai` | `OpenAiProfile` | Profile's default |
| `gemini` | `GeminiProfile` | Profile's default |
When `--model` is specified, it overrides the profile's default model but does not change the profile selection. The provider profile determines the system prompt, tool definitions, and tool-calling conventions.
**Why explicit --provider instead of auto-detection from model string.** Model naming conventions are not stable across providers and can collide. Explicit provider selection is unambiguous and avoids a mapping table that rots.
---
## 3. Permission Model
### 3.1 Permission Levels
The `--permissions` flag controls which tools the agent can use without approval:
| Level | Tools available without approval | Tools requiring approval |
|---|---|---|
| `read-only` | `read`, `grep`, `glob` | `write`, `edit`, `shell` |
| `read-write` | `read`, `grep`, `glob`, `write`, `edit` | `shell` |
| `full` | `read`, `grep`, `glob`, `write`, `edit`, `shell` | *(none)* |
The default is `read-write`: the agent can read and modify files freely but must ask before running shell commands.
### 3.2 Interactive Approval (TTY attached, no --auto-approve)
When the agent calls a tool that requires approval, the CLI prompts the user on stderr:
```
Agent wants to run shell: npm test
Allow? [y]es / [n]o / [a]lways
```
- **y**: Allow this single invocation. The agent proceeds. Future calls to the same tool still prompt.
- **n**: Deny this invocation. The agent receives a tool error: `"shell tool denied by user at current permission level"`. The agent must adapt.
- **a**: Escalate permissions for the remainder of the session. The tool (and all tools at or below its permission level) no longer prompt. Equivalent to upgrading `--permissions` mid-session.
### 3.3 Non-Interactive Mode (no TTY or --auto-approve)
When the CLI cannot prompt (piped stdin, `--auto-approve` set, or no TTY), tools that require approval are hard-blocked. The agent receives a tool error message and must find another way to accomplish the task.
`--auto-approve` does **not** implicitly upgrade to `--permissions full`. It means "don't prompt me, just enforce the stated permission level." A CI pipeline that wants full tool access must explicitly pass `--permissions full --auto-approve`.
**Why hard-block instead of auto-approve-all in CI.** Silent full access in CI is dangerous. Forcing `--permissions full` to be explicit makes the trust decision visible in the pipeline definition.
---
## 4. Output
### 4.1 Event Rendering
The CLI subscribes to the `Session`'s `EventEmitter` and renders events as follows:
| EventKind | Rendering |
|---|---|
| `AssistantMessage` | Stream text content to stdout as it arrives. |
| `ToolCall` | Print a one-line summary to stderr: tool name and key argument. |
| `ToolResult` | Suppressed (not shown to user). |
| `TurnComplete` | No output. |
| `Error` | Print error message to stderr. |
**Assistant text** streams to stdout character-by-character (or chunk-by-chunk as delivered by the LLM streaming response). This is the primary output.
**Tool call summaries** go to stderr so they don't interfere with piping stdout. Format:
```
[tool] read src/main.rs
[tool] edit src/lib.rs
[tool] shell npm test
```
### 4.2 Debug Mode
When `--debug` is set, the CLI additionally logs full LLM request and response payloads to stderr. This includes:
- The complete message array sent to the LLM
- System prompt
- Tool definitions
- Raw response body (streamed chunks or complete response)
Debug output is prefixed with `[debug]` to distinguish it from tool summaries.
### 4.3 Completion Summary
When the session ends, the CLI prints a one-line summary to stderr:
```
Done (4 turns, 7 tool calls, 3.2k tokens)
```
This always appears, regardless of whether the agent succeeded or failed. Token count is the total across all turns (input + output). The summary goes to stderr so stdout contains only the agent's text output.
---
## 5. Environment and Configuration
### 5.1 API Keys
API keys are read from standard environment variables. No config file, no `.env` loading, no key management.
| Provider | Environment variable |
|---|---|
| Anthropic | `ANTHROPIC_API_KEY` |
| OpenAI | `OPENAI_API_KEY` |
| Gemini | `GEMINI_API_KEY` |
If the required key is missing, the CLI exits immediately with a clear error message naming the expected variable.
### 5.2 Working Directory
The agent always operates in the process's current working directory. There is no `--dir` flag. Users who need a different directory use `cd` before invoking `agent`, following Unix convention.
### 5.3 Session Lifecycle
Every invocation is a fresh session. There is no session persistence, no resume flag, no checkpoint support at the CLI level. The agent starts, runs to completion, and exits. State between runs is carried only by the filesystem (files the agent created or modified).
---
## 6. Exit Behavior
### 6.1 Exit Codes
| Code | Meaning |
|---|---|
| `0` | Agent completed successfully. |
| `1` | Agent failed (LLM error, tool error, config error, agent gave up, or any other failure). |
Two codes only. The CLI does not differentiate between failure causes via exit code. Diagnostic information is in stderr output.
### 6.2 Interruption
Ctrl-C (SIGINT) triggers a graceful shutdown: the current LLM request is cancelled, any running tool is terminated, and the CLI exits with code 1. No cleanup prompt, no "are you sure" -- immediate stop.
---
## 7. Definition of Done
This section defines how to validate that an implementation of this spec is complete and correct. An implementation is done when every item is checked off.
### 7.1 Invocation
- [ ] `agent 'hello world'` sends "hello world" to the LLM and prints the response to stdout
- [ ] `agent` with no arguments prints usage and exits with code 1
- [ ] `--provider anthropic` uses the Anthropic profile
- [ ] `--provider openai` uses the OpenAI profile
- [ ] `--provider gemini` uses the Gemini profile
- [ ] `--model` overrides the default model within the selected profile
- [ ] Invalid `--provider` value exits with code 1 and a clear error
### 7.2 Permissions
- [ ] Default permission level is `read-write`
- [ ] `--permissions read-only` blocks write, edit, and shell
- [ ] `--permissions full` allows all tools without prompts
- [ ] In interactive mode, denied tools trigger an approval prompt on stderr
- [ ] Answering "y" allows a single invocation
- [ ] Answering "n" returns a tool error to the agent
- [ ] Answering "a" escalates permissions for the session
- [ ] In non-interactive mode, denied tools are hard-blocked (tool error returned to agent)
- [ ] `--auto-approve` does not implicitly upgrade permission level
### 7.3 Output
- [ ] Assistant text streams to stdout
- [ ] Tool call summaries print to stderr in `[tool] name args` format
- [ ] Tool results are not shown to the user
- [ ] Completion summary prints to stderr: `Done (N turns, N tool calls, Nk tokens)`
- [ ] `--debug` dumps full LLM request/response payloads to stderr
### 7.4 Configuration
- [ ] API keys read from `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`
- [ ] Missing API key exits with code 1 and names the expected variable
- [ ] Agent operates in the current working directory
### 7.5 Exit Behavior
- [ ] Successful completion exits with code 0
- [ ] Any failure exits with code 1
- [ ] Ctrl-C cancels the current operation and exits with code 1
### 7.6 Integration Smoke Test
```
FUNCTION smoke_test():
-- Setup
SET dir = create_temp_directory()
write_file(dir + "/hello.txt", "world")
SET env = {"ANTHROPIC_API_KEY": valid_key}
-- Test 1: Basic invocation
SET result = run_cli(
args: ["agent", "Read hello.txt and tell me what it says"],
cwd: dir,
env: env
)
ASSERT result.exit_code == 0
ASSERT result.stdout CONTAINS "world"
ASSERT result.stderr CONTAINS "Done ("
ASSERT result.stderr CONTAINS "[tool] read"
-- Test 2: Permission enforcement
SET result = run_cli(
args: ["agent", "--permissions", "read-only", "--auto-approve",
"Write 'test' to output.txt"],
cwd: dir,
env: env
)
-- Agent should complete (exit 0) but output.txt should not exist
-- because write was blocked and agent adapted
ASSERT NOT file_exists(dir + "/output.txt")
-- Test 3: Missing API key
SET result = run_cli(
args: ["agent", "hello"],
cwd: dir,
env: {}
)
ASSERT result.exit_code == 1
ASSERT result.stderr CONTAINS "ANTHROPIC_API_KEY"
-- Test 4: No arguments
SET result = run_cli(
args: ["agent"],
cwd: dir,
env: env
)
ASSERT result.exit_code == 1
```

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff