From 3200e1a9d64435db77b31bf82da0940a2da83966 Mon Sep 17 00:00:00 2001 From: Fynn Datoo Date: Thu, 30 Apr 2026 10:23:02 +0000 Subject: [PATCH 1/3] Fix FBI integration gaps: branch ID, user branch, title pipeline - db/runs.gleam: insert_run now does a two-step INSERT+UPDATE so branch_name uses the actual auto-increment run ID (claude/run-37) instead of the millisecond timestamp (claude/run-1777542730698). Added branch/mock/mock_scenario params and branch_in_use() helper. mark_finished now updates branch_name and title via COALESCE so renames from the agent propagate back to the DB. - handlers/runs.gleam: create decoder now accepts branch, mock, mock_scenario, force; performs 409 branch-in-use check for explicit branches; fetches settings.global_prompt and passes it through LaunchInput. do_continue likewise passes global_prompt. - worker.gleam: LaunchInput gains global_prompt field; build_preamble now emits preamble.txt (FBI setup instructions: write title to /fbi-state/title, rename claude/run-* branch) and global.txt. - finalizeBranch.sh: reads /fbi-state/title (stripping newlines, capped at 200 chars) and includes it as "title" in result.json. - container_monitor.gleam, reattach.gleam: decode "title" from result.json and populate RunOutcome.title instead of always None. Co-Authored-By: Claude Sonnet 4.6 --- src/server/priv/static/finalizeBranch.sh | 9 +- src/server/src/fbi/db/runs.gleam | 39 +++- src/server/src/fbi/handlers/runs.gleam | 189 ++++++++++++------ .../src/fbi/run/container_monitor.gleam | 12 +- src/server/src/fbi/run/reattach.gleam | 19 +- src/server/src/fbi/run/worker.gleam | 13 ++ 6 files changed, 212 insertions(+), 69 deletions(-) diff --git a/src/server/priv/static/finalizeBranch.sh b/src/server/priv/static/finalizeBranch.sh index 914831a..6de5b7a 100755 --- a/src/server/priv/static/finalizeBranch.sh +++ b/src/server/priv/static/finalizeBranch.sh @@ -26,6 +26,11 @@ git fetch --quiet origin "$DEFAULT_BRANCH" 2>/dev/null || true CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')" HEAD_SHA="$(git rev-parse HEAD 2>/dev/null || echo '')" +TITLE="" +if [ -f /fbi-state/title ]; then + TITLE="$(tr -d '\n\r' < /fbi-state/title | head -c 200)" +fi + # Push exit is sourced from the last origin-push log (written by the post- # commit hook). No-remote projects write no log; treat absent log as success. PUSH_EXIT=0 @@ -35,5 +40,5 @@ if [ -f /tmp/last-origin-push.log ]; then fi fi -printf '{"exit_code":%d,"push_exit":%d,"head_sha":"%s","branch":"%s","session_id":"%s"}\n' \ - "$CLAUDE_EXIT" "$PUSH_EXIT" "$HEAD_SHA" "$CURRENT_BRANCH" "${SESSION_ID:-}" > "$RESULT_PATH" +printf '{"exit_code":%d,"push_exit":%d,"head_sha":"%s","branch":"%s","session_id":"%s","title":"%s"}\n' \ + "$CLAUDE_EXIT" "$PUSH_EXIT" "$HEAD_SHA" "$CURRENT_BRANCH" "${SESSION_ID:-}" "$TITLE" > "$RESULT_PATH" diff --git a/src/server/src/fbi/db/runs.gleam b/src/server/src/fbi/db/runs.gleam index 1895c17..a389385 100644 --- a/src/server/src/fbi/db/runs.gleam +++ b/src/server/src/fbi/db/runs.gleam @@ -362,6 +362,7 @@ pub fn insert_run( db: sqlight.Connection, project_id: Int, prompt: String, + branch: option.Option(String), model: Option(String), effort: Option(String), subagent_model: Option(String), @@ -369,17 +370,15 @@ pub fn insert_run( mock_scenario: Option(String), now: Int, ) -> Result(Run, DbError) { - let branch_name = "claude/run-" <> int.to_string(now) let log_path = "/var/log/fbi/runs/" <> int.to_string(now) <> ".log" - connection.query_one( + use run <- result.try(connection.query_one( "INSERT INTO runs (project_id, prompt, branch_name, state, log_path, created_at, state_entered_at, model, effort, subagent_model, mock, mock_scenario) - VALUES (?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?) RETURNING " + VALUES (?, ?, '', 'queued', ?, ?, ?, ?, ?, ?, ?, ?) RETURNING " <> columns(), db, [ sqlight.int(project_id), sqlight.text(prompt), - sqlight.text(branch_name), sqlight.text(log_path), sqlight.int(now), sqlight.int(now), @@ -393,7 +392,27 @@ pub fn insert_run( nullable_opt(mock_scenario), ], decoder(), + )) + let branch_name = case branch { + option.Some(b) -> b + option.None -> "claude/run-" <> int.to_string(run.id) + } + connection.query_one( + "UPDATE runs SET branch_name = ? WHERE id = ? RETURNING " <> columns(), + db, + [sqlight.text(branch_name), sqlight.int(run.id)], + decoder(), + ) +} + +pub fn branch_in_use(db: sqlight.Connection, branch: String) -> Result(Bool, DbError) { + connection.query_one( + "SELECT COUNT(*) FROM runs WHERE branch_name = ? AND state IN ('queued', 'running', 'waiting', 'awaiting_resume')", + db, + [sqlight.text(branch)], + decode.at([0], decode.int), ) + |> result.map(fn(n) { n > 0 }) } pub fn insert_continue_run( @@ -523,7 +542,9 @@ pub fn mark_finished( _ -> "failed" } connection.query_one( - "UPDATE runs SET state = ?, exit_code = ?, head_commit = ?, finished_at = ?, error = ?, + "UPDATE runs SET state = ?, exit_code = ?, branch_name = COALESCE(?, branch_name), + head_commit = ?, finished_at = ?, error = ?, + title = COALESCE(?, title), claude_session_id = COALESCE(?, claude_session_id) WHERE id = ? RETURNING " <> columns(), @@ -531,6 +552,10 @@ pub fn mark_finished( [ sqlight.text(state), sqlight.int(outcome.exit_code), + case outcome.branch_pushed { + option.None -> sqlight.null() + option.Some(b) -> sqlight.text(b) + }, case outcome.head_commit { option.None -> sqlight.null() option.Some(c) -> sqlight.text(c) @@ -540,6 +565,10 @@ pub fn mark_finished( option.None -> sqlight.null() option.Some(e) -> sqlight.text(e) }, + case outcome.title { + option.None -> sqlight.null() + option.Some(t) -> sqlight.text(t) + }, case outcome.claude_session_id { option.None -> sqlight.null() option.Some(s) -> sqlight.text(s) diff --git a/src/server/src/fbi/handlers/runs.gleam b/src/server/src/fbi/handlers/runs.gleam index 8625d7a..4bc81f5 100644 --- a/src/server/src/fbi/handlers/runs.gleam +++ b/src/server/src/fbi/handlers/runs.gleam @@ -2,6 +2,7 @@ import fbi/context.{type Context} import fbi/db/connection import fbi/db/projects import fbi/db/runs +import fbi/db/settings import fbi/json/run as run_json import fbi/run/reattach as run_reattach import fbi/run/registry as run_registry @@ -14,7 +15,7 @@ import gleam/http import gleam/int import gleam/json import gleam/list -import gleam/option.{None, Some} +import gleam/option.{type Option, None, Some} import wisp.{type Request, type Response} pub fn handle_list(req: Request, ctx: Context) -> Response { @@ -181,6 +182,11 @@ fn do_continue(req: Request, ctx: Context, run_id: Int) -> Response { wisp.internal_server_error() } Ok(#(actor_subject, bc)) -> { + let global_prompt = + case settings.get(ctx.db) { + Ok(s) -> s.global_prompt + Error(_) -> "" + } run_worker.launch( run_worker.LaunchInput( run: new_run, @@ -189,6 +195,7 @@ fn do_continue(req: Request, ctx: Context, run_id: Int) -> Response { cols: 80, rows: 24, broadcaster: bc, + global_prompt: global_prompt, ), actor_subject, ) @@ -247,6 +254,11 @@ fn create(req: Request, ctx: Context, project_id: Int) -> Response { use body <- wisp.require_json(req) let decoder = { use prompt <- decode.field("prompt", decode.string) + use branch <- decode.optional_field( + "branch", + None, + decode.optional(decode.string), + ) use model <- decode.optional_field( "model", None, @@ -268,70 +280,135 @@ fn create(req: Request, ctx: Context, project_id: Int) -> Response { None, decode.optional(decode.string), ) - decode.success(#(prompt, model, effort, subagent_model, mock, mock_scenario)) + use force <- decode.optional_field("force", False, decode.bool) + decode.success( + #(prompt, branch, model, effort, subagent_model, mock, mock_scenario, force), + ) } case decode.run(body, decoder) { Error(_) -> wisp.bad_request("Invalid request body") - Ok(#(prompt, model, effort, subagent_model, mock, mock_scenario)) -> + Ok(#(prompt, branch, model, effort, subagent_model, mock, mock_scenario, force)) -> case projects.get(ctx.db, project_id) { Error(_) -> wisp.not_found() - Ok(project) -> { - let now = now_ms() - case - runs.insert_run( - ctx.db, - project_id, - prompt, - model, - effort, - subagent_model, - mock, - mock_scenario, - now, - ) - { - Error(e) -> { - wisp.log_error( - "insert run for project " - <> int.to_string(project_id) - <> ": " - <> connection.describe_error(e), - ) - wisp.internal_server_error() - } - Ok(run) -> - case - run_supervisor.start_run( - ctx.run_registry, - ctx.db, - ctx.config, - run.id, - ) - { - Error(reason) -> { - wisp.log_error( - "start run " <> int.to_string(run.id) <> ": " <> reason, - ) - wisp.internal_server_error() - } - Ok(#(actor_subject, bc)) -> { - run_worker.launch( - run_worker.LaunchInput( - run: run, - project: project, - config: ctx.config, - cols: 80, - rows: 24, - broadcaster: bc, + Ok(project) -> + case branch, force { + Some(b), False -> + case runs.branch_in_use(ctx.db, b) { + Error(_) -> wisp.internal_server_error() + Ok(True) -> + json.object([ + #("error", json.string("branch_in_use")), + #( + "message", + json.string( + "Branch '" + <> b + <> "' is already in use by an active run.", + ), ), - actor_subject, - ) - run_json.encode(run) + ]) |> json.to_string() - |> wisp.json_response(201) - } + |> wisp.json_response(409) + Ok(False) -> + do_create( + ctx, + project, + prompt, + branch, + model, + effort, + subagent_model, + mock, + mock_scenario, + ) } + _, _ -> + do_create( + ctx, + project, + prompt, + branch, + model, + effort, + subagent_model, + mock, + mock_scenario, + ) } + } + } +} + +fn do_create( + ctx: Context, + project: projects.Project, + prompt: String, + branch: option.Option(String), + model: option.Option(String), + effort: option.Option(String), + subagent_model: option.Option(String), + mock: Bool, + mock_scenario: option.Option(String), +) -> Response { + let now = now_ms() + let global_prompt = case settings.get(ctx.db) { + Ok(s) -> s.global_prompt + Error(_) -> "" + } + case + runs.insert_run( + ctx.db, + project.id, + prompt, + branch, + model, + effort, + subagent_model, + mock, + mock_scenario, + now, + ) + { + Error(e) -> { + wisp.log_error( + "insert run for project " + <> int.to_string(project.id) + <> ": " + <> connection.describe_error(e), + ) + wisp.internal_server_error() + } + Ok(run) -> + case + run_supervisor.start_run( + ctx.run_registry, + ctx.db, + ctx.config, + run.id, + ) + { + Error(reason) -> { + wisp.log_error( + "start run " <> int.to_string(run.id) <> ": " <> reason, + ) + wisp.internal_server_error() + } + Ok(#(actor_subject, bc)) -> { + run_worker.launch( + run_worker.LaunchInput( + run: run, + project: project, + config: ctx.config, + cols: 80, + rows: 24, + broadcaster: bc, + global_prompt: global_prompt, + ), + actor_subject, + ) + run_json.encode(run) + |> json.to_string() + |> wisp.json_response(201) } } } diff --git a/src/server/src/fbi/run/container_monitor.gleam b/src/server/src/fbi/run/container_monitor.gleam index 67eb073..a421435 100644 --- a/src/server/src/fbi/run/container_monitor.gleam +++ b/src/server/src/fbi/run/container_monitor.gleam @@ -198,7 +198,10 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { use head_sha <- decode.optional_field("head_sha", "", decode.string) use branch <- decode.optional_field("branch", "", decode.string) use session_id <- decode.optional_field("session_id", "", decode.string) - decode.success(#(agent_exit, push_exit, head_sha, branch, session_id)) + use title <- decode.optional_field("title", "", decode.string) + decode.success( + #(agent_exit, push_exit, head_sha, branch, session_id, title), + ) } case json.parse(json_str, decoder) { Error(_) -> @@ -210,7 +213,7 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { error_message: Some("could not parse result.json"), claude_session_id: None, ) - Ok(#(agent_exit, push_exit, head_sha, branch, session_id)) -> + Ok(#(agent_exit, push_exit, head_sha, branch, session_id, title)) -> RunOutcome( exit_code: agent_exit, branch_pushed: case push_exit { @@ -225,7 +228,10 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { "" -> None sha -> Some(sha) }, - title: None, + title: case title { + "" -> None + t -> Some(t) + }, error_message: case agent_exit { 0 -> None code -> Some("agent exit " <> int.to_string(code)) diff --git a/src/server/src/fbi/run/reattach.gleam b/src/server/src/fbi/run/reattach.gleam index b36ca96..bd5157c 100644 --- a/src/server/src/fbi/run/reattach.gleam +++ b/src/server/src/fbi/run/reattach.gleam @@ -2,6 +2,7 @@ import fbi/config.{type Config} import fbi/db/connection import fbi/db/projects import fbi/db/runs.{type Run, type RunOutcome, RunOutcome} +import fbi/db/settings import fbi/docker import fbi/run/actor as run_actor import fbi/run/broadcaster @@ -290,6 +291,11 @@ pub fn resurrect( <> reason, ) Ok(#(actor_subject, bc)) -> { + let global_prompt = + case settings.get(db) { + Ok(s) -> s.global_prompt + Error(_) -> "" + } run_worker.launch( run_worker.LaunchInput( run: child, @@ -298,6 +304,7 @@ pub fn resurrect( cols: 80, rows: 24, broadcaster: bc, + global_prompt: global_prompt, ), actor_subject, ) @@ -371,7 +378,10 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { use head_sha <- decode.optional_field("head_sha", "", decode.string) use branch <- decode.optional_field("branch", "", decode.string) use session_id <- decode.optional_field("session_id", "", decode.string) - decode.success(#(agent_exit, push_exit, head_sha, branch, session_id)) + use title <- decode.optional_field("title", "", decode.string) + decode.success( + #(agent_exit, push_exit, head_sha, branch, session_id, title), + ) } case json.parse(json_str, decoder) { Error(_) -> @@ -383,7 +393,7 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { error_message: Some("could not parse result.json"), claude_session_id: None, ) - Ok(#(agent_exit, push_exit, head_sha, branch, session_id)) -> + Ok(#(agent_exit, push_exit, head_sha, branch, session_id, title)) -> RunOutcome( exit_code: agent_exit, branch_pushed: case push_exit { @@ -398,7 +408,10 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { "" -> None sha -> Some(sha) }, - title: None, + title: case title { + "" -> None + t -> Some(t) + }, error_message: case agent_exit { 0 -> None code -> Some("agent exit " <> int.to_string(code)) diff --git a/src/server/src/fbi/run/worker.gleam b/src/server/src/fbi/run/worker.gleam index 62b2f7d..6f8f302 100644 --- a/src/server/src/fbi/run/worker.gleam +++ b/src/server/src/fbi/run/worker.gleam @@ -29,6 +29,7 @@ pub type LaunchInput { cols: Int, rows: Int, broadcaster: Subject(BroadcastMsg), + global_prompt: String, ) } @@ -388,7 +389,19 @@ fn is_regular_directory(path: String) -> Bool { fn path_dirname(path: String) -> String fn build_preamble(input: LaunchInput) -> dict.Dict(String, BitArray) { + let preamble = + "# FBI Session Setup\n\n" + <> "Before starting the task, run these two setup steps:\n\n" + <> "1. Write a concise title (max 60 chars) describing this task:\n" + <> " echo 'Brief task title' > /fbi-state/title\n\n" + <> "2. If your current branch starts with \"claude/run-\", rename it to a\n" + <> " short descriptive kebab-case name reflecting the task:\n" + <> " git branch -m feat/your-task-description\n" + <> " (Skip if the branch already has a meaningful name.)\n\n" + <> "After the setup steps, proceed with the task." dict.from_list([ + #("preamble.txt", bit_array.from_string(preamble)), + #("global.txt", bit_array.from_string(input.global_prompt)), #("prompt.txt", bit_array.from_string(input.run.prompt)), #( "instructions.txt", From c17416cbc29bfdf1470905683ba2ec4663d1c1d3 Mon Sep 17 00:00:00 2001 From: Fynn Datoo Date: Thu, 30 Apr 2026 12:49:49 +0000 Subject: [PATCH 2/3] Apply gleam format Co-Authored-By: Claude Sonnet 4.6 --- src/server/src/fbi/db/runs.gleam | 5 +- src/server/src/fbi/handlers/runs.gleam | 51 ++++++++++--------- .../src/fbi/run/container_monitor.gleam | 11 ++-- src/server/src/fbi/run/reattach.gleam | 20 +++++--- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/server/src/fbi/db/runs.gleam b/src/server/src/fbi/db/runs.gleam index a389385..75818d3 100644 --- a/src/server/src/fbi/db/runs.gleam +++ b/src/server/src/fbi/db/runs.gleam @@ -405,7 +405,10 @@ pub fn insert_run( ) } -pub fn branch_in_use(db: sqlight.Connection, branch: String) -> Result(Bool, DbError) { +pub fn branch_in_use( + db: sqlight.Connection, + branch: String, +) -> Result(Bool, DbError) { connection.query_one( "SELECT COUNT(*) FROM runs WHERE branch_name = ? AND state IN ('queued', 'running', 'waiting', 'awaiting_resume')", db, diff --git a/src/server/src/fbi/handlers/runs.gleam b/src/server/src/fbi/handlers/runs.gleam index 4bc81f5..357413a 100644 --- a/src/server/src/fbi/handlers/runs.gleam +++ b/src/server/src/fbi/handlers/runs.gleam @@ -182,11 +182,10 @@ fn do_continue(req: Request, ctx: Context, run_id: Int) -> Response { wisp.internal_server_error() } Ok(#(actor_subject, bc)) -> { - let global_prompt = - case settings.get(ctx.db) { - Ok(s) -> s.global_prompt - Error(_) -> "" - } + let global_prompt = case settings.get(ctx.db) { + Ok(s) -> s.global_prompt + Error(_) -> "" + } run_worker.launch( run_worker.LaunchInput( run: new_run, @@ -212,11 +211,7 @@ fn do_continue(req: Request, ctx: Context, run_id: Int) -> Response { } } -pub fn handle_resume_now( - req: Request, - ctx: Context, - id_str: String, -) -> Response { +pub fn handle_resume_now(req: Request, ctx: Context, id_str: String) -> Response { case req.method { http.Post -> case int.parse(id_str) { @@ -281,13 +276,29 @@ fn create(req: Request, ctx: Context, project_id: Int) -> Response { decode.optional(decode.string), ) use force <- decode.optional_field("force", False, decode.bool) - decode.success( - #(prompt, branch, model, effort, subagent_model, mock, mock_scenario, force), - ) + decode.success(#( + prompt, + branch, + model, + effort, + subagent_model, + mock, + mock_scenario, + force, + )) } case decode.run(body, decoder) { Error(_) -> wisp.bad_request("Invalid request body") - Ok(#(prompt, branch, model, effort, subagent_model, mock, mock_scenario, force)) -> + Ok(#( + prompt, + branch, + model, + effort, + subagent_model, + mock, + mock_scenario, + force, + )) -> case projects.get(ctx.db, project_id) { Error(_) -> wisp.not_found() Ok(project) -> @@ -380,12 +391,7 @@ fn do_create( } Ok(run) -> case - run_supervisor.start_run( - ctx.run_registry, - ctx.db, - ctx.config, - run.id, - ) + run_supervisor.start_run(ctx.run_registry, ctx.db, ctx.config, run.id) { Error(reason) -> { wisp.log_error( @@ -459,10 +465,7 @@ fn index_paged(ctx: Context, filter: runs.ListFilter) -> Response { } } -fn get_param( - qs: List(#(String, String)), - key: String, -) -> option.Option(String) { +fn get_param(qs: List(#(String, String)), key: String) -> option.Option(String) { case list.key_find(qs, key) { Ok(v) if v != "" -> Some(v) _ -> None diff --git a/src/server/src/fbi/run/container_monitor.gleam b/src/server/src/fbi/run/container_monitor.gleam index a421435..5355d8d 100644 --- a/src/server/src/fbi/run/container_monitor.gleam +++ b/src/server/src/fbi/run/container_monitor.gleam @@ -199,9 +199,14 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { use branch <- decode.optional_field("branch", "", decode.string) use session_id <- decode.optional_field("session_id", "", decode.string) use title <- decode.optional_field("title", "", decode.string) - decode.success( - #(agent_exit, push_exit, head_sha, branch, session_id, title), - ) + decode.success(#( + agent_exit, + push_exit, + head_sha, + branch, + session_id, + title, + )) } case json.parse(json_str, decoder) { Error(_) -> diff --git a/src/server/src/fbi/run/reattach.gleam b/src/server/src/fbi/run/reattach.gleam index bd5157c..790b2a3 100644 --- a/src/server/src/fbi/run/reattach.gleam +++ b/src/server/src/fbi/run/reattach.gleam @@ -291,11 +291,10 @@ pub fn resurrect( <> reason, ) Ok(#(actor_subject, bc)) -> { - let global_prompt = - case settings.get(db) { - Ok(s) -> s.global_prompt - Error(_) -> "" - } + let global_prompt = case settings.get(db) { + Ok(s) -> s.global_prompt + Error(_) -> "" + } run_worker.launch( run_worker.LaunchInput( run: child, @@ -379,9 +378,14 @@ fn read_outcome(state_dir: String, exit_code: Int) -> RunOutcome { use branch <- decode.optional_field("branch", "", decode.string) use session_id <- decode.optional_field("session_id", "", decode.string) use title <- decode.optional_field("title", "", decode.string) - decode.success( - #(agent_exit, push_exit, head_sha, branch, session_id, title), - ) + decode.success(#( + agent_exit, + push_exit, + head_sha, + branch, + session_id, + title, + )) } case json.parse(json_str, decoder) { Error(_) -> From da9e75d1215f0e3b61c43cfc8861df25f396b10b Mon Sep 17 00:00:00 2001 From: Fynn Datoo Date: Thu, 30 Apr 2026 12:53:19 +0000 Subject: [PATCH 3/3] gleam format (1.16.0) Co-Authored-By: Claude Sonnet 4.6 --- src/server/src/fbi/handlers/runs.gleam | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/server/src/fbi/handlers/runs.gleam b/src/server/src/fbi/handlers/runs.gleam index 357413a..0d92e6c 100644 --- a/src/server/src/fbi/handlers/runs.gleam +++ b/src/server/src/fbi/handlers/runs.gleam @@ -211,7 +211,11 @@ fn do_continue(req: Request, ctx: Context, run_id: Int) -> Response { } } -pub fn handle_resume_now(req: Request, ctx: Context, id_str: String) -> Response { +pub fn handle_resume_now( + req: Request, + ctx: Context, + id_str: String, +) -> Response { case req.method { http.Post -> case int.parse(id_str) { @@ -465,7 +469,10 @@ fn index_paged(ctx: Context, filter: runs.ListFilter) -> Response { } } -fn get_param(qs: List(#(String, String)), key: String) -> option.Option(String) { +fn get_param( + qs: List(#(String, String)), + key: String, +) -> option.Option(String) { case list.key_find(qs, key) { Ok(v) if v != "" -> Some(v) _ -> None