chore: switch mergemaster to opus and add cargo fmt guidance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dave
2026-04-14 12:25:12 +00:00
parent badfabcf5e
commit 28adef9739
4 changed files with 223 additions and 40 deletions
+8 -13
View File
@@ -253,35 +253,30 @@ When the auto-resolver fails, you have access to the merge worktree at `.story_k
6. Call the `run_tests` MCP tool to start tests, then poll `get_test_result` until complete 6. Call the `run_tests` MCP tool to start tests, then poll `get_test_result` until complete
7. If it compiles, commit and re-trigger merge_agent_work 7. If it compiles, commit and re-trigger merge_agent_work
### Common conflict patterns in this project: ### Common conflict patterns:
**Story file rename/rename conflicts:** Both branches moved the story .md file to different pipeline directories. Resolution: `git rm` both sides — story files in `work/2_current/`, `work/3_qa/`, `work/4_merge/` are gitignored and don't need to be committed. **Story file rename/rename conflicts:** Both branches moved the story .md file to different pipeline directories. Resolution: `git rm` both sides — story files in pipeline directories are gitignored and don't need to be committed.
**bot.rs tokio::select! conflicts:** Master has a `tokio::select!` loop in `handle_message()` that handles permission forwarding (story 275). Feature branches created before story 275 have a simpler direct `provider.chat_stream().await` call. Resolution: KEEP master's tokio::select! loop. Integrate only the feature's new logic (e.g. typing indicators, new callbacks) into the existing loop structure. Do NOT replace the loop with the old direct call.
**Duplicate functions/imports:** The auto-resolver keeps both sides, producing duplicates. Resolution: keep one copy (prefer master's version), delete the duplicate. **Duplicate functions/imports:** The auto-resolver keeps both sides, producing duplicates. Resolution: keep one copy (prefer master's version), delete the duplicate.
**Formatting-only conflicts:** Both sides reformatted the same code differently. Resolution: pick either side (prefer master). **Formatting-only conflicts:** Both sides reformatted the same code differently. Resolution: pick either side (prefer master).
**IMPORTANT: After resolving ANY conflict or fixing ANY gate failure in the merge workspace, run `script/lint` (if it exists) or the project's formatter before recommitting.** The auto-resolver frequently produces code that compiles but fails formatting checks. Running the formatter after every fix prevents repeated gate failures. **IMPORTANT: After resolving ANY conflict or fixing ANY gate failure in the merge workspace, use the `run_lint` MCP tool to check formatting, then `run_tests` to verify everything passes before recommitting.** The auto-resolver frequently produces code that compiles but fails formatting or linting checks.
## Fixing Gate Failures ## Fixing Gate Failures
If quality gates fail, attempt to fix issues yourself in the merge worktree. Use the run_tests MCP tool (then poll get_test_result) to verify — do not run script/test via Bash. If quality gates fail, attempt to fix issues yourself in the merge workspace. Use the run_tests MCP tool to verify before recommitting.
**Fix yourself (up to 3 attempts total):** **Fix yourself (up to 3 attempts total):**
- Syntax errors (missing semicolons, brackets, commas) - Syntax errors
- Duplicate definitions from merge artifacts - Duplicate definitions from merge artifacts
- Simple type annotation errors - Unused import warnings
- Unused import warnings flagged by clippy - Formatting issues that block linting
- Mismatched braces from bad conflict resolution
- Trivial formatting issues that block compilation or linting
**Report to human without attempting a fix:** **Report to human without attempting a fix:**
- Logic errors or incorrect business logic - Logic errors or incorrect business logic
- Missing function implementations - Missing function implementations
- Architectural changes required - Architectural changes required
- Non-trivial refactoring needed
**Max retry limit:** If gates still fail after 3 fix attempts, call report_merge_failure to record the failure, then stop immediately and report the full gate output to the human. **Max retry limit:** If gates still fail after 3 fix attempts, call report_merge_failure to record the failure, then stop immediately and report the full gate output to the human.
@@ -292,4 +287,4 @@ If quality gates fail, attempt to fix issues yourself in the merge worktree. Use
- Report conflict resolution outcomes clearly - Report conflict resolution outcomes clearly
- Report gate failures with full output so the human can act if needed - Report gate failures with full output so the human can act if needed
- The server automatically runs acceptance gates when your process exits""" - The server automatically runs acceptance gates when your process exits"""
system_prompt = "You are the mergemaster agent. Your primary job is to merge feature branches to master. First try the merge_agent_work MCP tool. If the auto-resolver fails on complex conflicts, resolve them yourself in the merge worktree — you are an opus-class agent capable of understanding both sides of a conflict and producing correct merged code. Common patterns: keep master's tokio::select! permission loop in bot.rs, discard story file rename conflicts (gitignored), remove duplicate definitions. After resolving, verify compilation before re-triggering merge. CRITICAL: Never manually move story files or call accept_story. After 3 failed fix attempts, call report_merge_failure and stop." system_prompt = "You are the mergemaster agent. Your primary job is to merge feature branches to master. First try the merge_agent_work MCP tool. If the auto-resolver fails on complex conflicts, resolve them yourself in the merge workspace. Common patterns: discard story file rename conflicts (gitignored), remove duplicate definitions/imports. After resolving, verify with run_tests MCP tool before re-triggering merge. CRITICAL: Never manually move story files or call accept_story. After 3 failed fix attempts, call report_merge_failure and stop."
+25
View File
@@ -8,6 +8,18 @@ export interface JoinedAgent {
label: string; label: string;
address: string; address: string;
registered_at: number; registered_at: number;
/// Project this agent is assigned to, if any.
assigned_project?: string;
}
export interface GatewayProject {
name: string;
url: string;
}
export interface GatewayInfo {
active: string;
projects: GatewayProject[];
} }
export interface GenerateTokenResponse { export interface GenerateTokenResponse {
@@ -61,4 +73,17 @@ export const gatewayApi = {
method: "DELETE", method: "DELETE",
}); });
}, },
/// Assign an agent to a project, or unassign it by passing null.
assignAgent(id: string, project: string | null): Promise<JoinedAgent> {
return gatewayRequest<JoinedAgent>(`/gateway/agents/${id}/assign`, {
method: "POST",
body: JSON.stringify({ project }),
});
},
/// Get the list of registered projects from the gateway.
getGatewayInfo(): Promise<GatewayInfo> {
return gatewayRequest<GatewayInfo>("/api/gateway");
},
}; };
+53 -3
View File
@@ -3,10 +3,10 @@
/// Provides: /// Provides:
/// - An "Add Agent" button that generates a one-time join token. /// - An "Add Agent" button that generates a one-time join token.
/// - Instructions for running a build agent with the token. /// - Instructions for running a build agent with the token.
/// - A list of connected agents with per-agent "Remove" buttons. /// - A list of connected agents with per-agent project assignment and "Remove" buttons.
import * as React from "react"; import * as React from "react";
import { gatewayApi, type JoinedAgent } from "../api/gateway"; import { gatewayApi, type JoinedAgent, type GatewayProject } from "../api/gateway";
const { useCallback, useEffect, useState } = React; const { useCallback, useEffect, useState } = React;
@@ -90,12 +90,17 @@ function TokenDisplay({ token }: { token: string }) {
function AgentRow({ function AgentRow({
agent, agent,
projects,
onRemove, onRemove,
onAssign,
}: { }: {
agent: JoinedAgent; agent: JoinedAgent;
projects: GatewayProject[];
onRemove: (id: string) => void; onRemove: (id: string) => void;
onAssign: (id: string, project: string | null) => void;
}) { }) {
const registeredAt = new Date(agent.registered_at * 1000).toLocaleString(); const registeredAt = new Date(agent.registered_at * 1000).toLocaleString();
const isAssigned = Boolean(agent.assigned_project);
return ( return (
<div <div
@@ -116,9 +121,10 @@ function AgentRow({
width: "8px", width: "8px",
height: "8px", height: "8px",
borderRadius: "50%", borderRadius: "50%",
background: "#3fb950", background: isAssigned ? "#3fb950" : "#6e7681",
flexShrink: 0, flexShrink: 0,
}} }}
title={isAssigned ? "Assigned" : "Idle (unassigned)"}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, color: "#e6edf3" }}>{agent.label}</div> <div style={{ fontWeight: 600, color: "#e6edf3" }}>{agent.label}</div>
@@ -129,6 +135,29 @@ function AgentRow({
Registered {registeredAt} Registered {registeredAt}
</div> </div>
</div> </div>
<select
data-testid={`assign-agent-${agent.id}`}
value={agent.assigned_project ?? ""}
onChange={(e) =>
onAssign(agent.id, e.target.value === "" ? null : e.target.value)
}
style={{
fontSize: "0.8em",
padding: "4px 8px",
borderRadius: "4px",
border: "1px solid #30363d",
background: "#0d1117",
color: "#e6edf3",
cursor: "pointer",
}}
>
<option value=""> unassigned </option>
{projects.map((p) => (
<option key={p.name} value={p.name}>
{p.name}
</option>
))}
</select>
<button <button
type="button" type="button"
data-testid={`remove-agent-${agent.id}`} data-testid={`remove-agent-${agent.id}`}
@@ -152,6 +181,7 @@ function AgentRow({
/// Gateway management panel — rendered when running in `--gateway` mode. /// Gateway management panel — rendered when running in `--gateway` mode.
export function GatewayPanel() { export function GatewayPanel() {
const [agents, setAgents] = useState<JoinedAgent[]>([]); const [agents, setAgents] = useState<JoinedAgent[]>([]);
const [projects, setProjects] = useState<GatewayProject[]>([]);
const [token, setToken] = useState<string | null>(null); const [token, setToken] = useState<string | null>(null);
const [generating, setGenerating] = useState(false); const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -161,6 +191,10 @@ export function GatewayPanel() {
.listAgents() .listAgents()
.then(setAgents) .then(setAgents)
.catch(() => setAgents([])); .catch(() => setAgents([]));
gatewayApi
.getGatewayInfo()
.then((info) => setProjects(info.projects))
.catch(() => setProjects([]));
}, []); }, []);
const handleAddAgent = useCallback(async () => { const handleAddAgent = useCallback(async () => {
@@ -186,6 +220,20 @@ export function GatewayPanel() {
} }
}, []); }, []);
const handleAssignAgent = useCallback(
async (id: string, project: string | null) => {
try {
const updated = await gatewayApi.assignAgent(id, project);
setAgents((prev) =>
prev.map((a) => (a.id === updated.id ? updated : a)),
);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
},
[],
);
return ( return (
<div <div
style={{ style={{
@@ -273,7 +321,9 @@ export function GatewayPanel() {
<AgentRow <AgentRow
key={agent.id} key={agent.id}
agent={agent} agent={agent}
projects={projects}
onRemove={handleRemoveAgent} onRemove={handleRemoveAgent}
onAssign={handleAssignAgent}
/> />
))} ))}
</div> </div>
+137 -24
View File
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
@@ -64,6 +64,9 @@ pub struct JoinedAgent {
pub address: String, pub address: String,
/// Unix timestamp when the agent registered. /// Unix timestamp when the agent registered.
pub registered_at: f64, pub registered_at: f64,
/// Project this agent is assigned to, if any.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assigned_project: Option<String>,
} }
/// A one-time join token that has been generated but not yet consumed. /// A one-time join token that has been generated but not yet consumed.
@@ -80,6 +83,14 @@ struct RegisterAgentRequest {
address: String, address: String,
} }
/// Request body for assigning or reassigning an agent to a project.
///
/// Send `{"project": "my-project"}` to assign, or `{"project": null}` to unassign.
#[derive(Deserialize)]
struct AssignAgentRequest {
project: Option<String>,
}
// ── Gateway state ──────────────────────────────────────────────────── // ── Gateway state ────────────────────────────────────────────────────
/// Shared gateway state threaded through HTTP handlers. /// Shared gateway state threaded through HTTP handlers.
@@ -95,22 +106,51 @@ pub struct GatewayState {
pub joined_agents: Arc<RwLock<Vec<JoinedAgent>>>, pub joined_agents: Arc<RwLock<Vec<JoinedAgent>>>,
/// One-time join tokens that have been issued but not yet consumed. /// One-time join tokens that have been issued but not yet consumed.
pending_tokens: Arc<RwLock<HashMap<String, PendingToken>>>, pending_tokens: Arc<RwLock<HashMap<String, PendingToken>>>,
/// Directory containing `projects.toml`, used for persisting agent data.
pub config_dir: PathBuf,
}
/// Load persisted agents from `<config_dir>/gateway_agents.json`.
/// Returns an empty list if the file does not exist or cannot be parsed.
fn load_agents(config_dir: &Path) -> Vec<JoinedAgent> {
let path = config_dir.join("gateway_agents.json");
match std::fs::read(&path) {
Ok(data) => serde_json::from_slice(&data).unwrap_or_default(),
Err(_) => Vec::new(),
}
}
/// Persist the current agent list to `<config_dir>/gateway_agents.json`.
/// Silently ignores write errors (e.g. read-only filesystem or empty path).
async fn save_agents(agents: &[JoinedAgent], config_dir: &Path) {
if config_dir == Path::new("") {
return;
}
let path = config_dir.join("gateway_agents.json");
if let Ok(data) = serde_json::to_vec_pretty(agents) {
let _ = tokio::fs::write(&path, data).await;
}
} }
impl GatewayState { impl GatewayState {
/// Create a new gateway state from a config. The first project in the config /// Create a new gateway state from a config and config directory.
/// becomes the active project by default. ///
pub fn new(config: GatewayConfig) -> Result<Self, String> { /// The first project in the config becomes the active project by default.
/// Previously registered agents are loaded from `gateway_agents.json` in
/// `config_dir` if the file exists.
pub fn new(config: GatewayConfig, config_dir: PathBuf) -> Result<Self, String> {
if config.projects.is_empty() { if config.projects.is_empty() {
return Err("projects.toml must define at least one project".to_string()); return Err("projects.toml must define at least one project".to_string());
} }
let first = config.projects.keys().next().unwrap().clone(); let first = config.projects.keys().next().unwrap().clone();
let agents = load_agents(&config_dir);
Ok(Self { Ok(Self {
config, config,
active_project: Arc::new(RwLock::new(first)), active_project: Arc::new(RwLock::new(first)),
client: Client::new(), client: Client::new(),
joined_agents: Arc::new(RwLock::new(Vec::new())), joined_agents: Arc::new(RwLock::new(agents)),
pending_tokens: Arc::new(RwLock::new(HashMap::new())), pending_tokens: Arc::new(RwLock::new(HashMap::new())),
config_dir,
}) })
} }
@@ -622,6 +662,7 @@ pub async fn gateway_register_agent_handler(
label: req.label, label: req.label,
address: req.address, address: req.address,
registered_at: chrono::Utc::now().timestamp() as f64, registered_at: chrono::Utc::now().timestamp() as f64,
assigned_project: None,
}; };
crate::slog!( crate::slog!(
@@ -630,7 +671,11 @@ pub async fn gateway_register_agent_handler(
agent.id agent.id
); );
state.joined_agents.write().await.push(agent.clone()); {
let mut agents = state.joined_agents.write().await;
agents.push(agent.clone());
save_agents(&agents, &state.config_dir).await;
}
let body = serde_json::to_vec(&agent).unwrap_or_default(); let body = serde_json::to_vec(&agent).unwrap_or_default();
Response::builder() Response::builder()
@@ -656,11 +701,16 @@ pub async fn gateway_remove_agent_handler(
PoemPath(id): PoemPath<String>, PoemPath(id): PoemPath<String>,
state: Data<&Arc<GatewayState>>, state: Data<&Arc<GatewayState>>,
) -> Response { ) -> Response {
let mut agents = state.joined_agents.write().await; let removed = {
let before = agents.len(); let mut agents = state.joined_agents.write().await;
agents.retain(|a| a.id != id); let before = agents.len();
let removed = agents.len() < before; agents.retain(|a| a.id != id);
drop(agents); let removed = agents.len() < before;
if removed {
save_agents(&agents, &state.config_dir).await;
}
removed
};
if removed { if removed {
crate::slog!("[gateway] Removed agent id={id}"); crate::slog!("[gateway] Removed agent id={id}");
@@ -674,6 +724,63 @@ pub async fn gateway_remove_agent_handler(
} }
} }
/// `POST /gateway/agents/:id/assign` — assign or unassign an agent to a project.
///
/// Body: `{ "project": "my-project" }` to assign, or `{ "project": null }` to unassign.
/// Returns the updated `JoinedAgent` on success. The assignment is persisted to disk
/// so it survives gateway restarts.
#[handler]
pub async fn gateway_assign_agent_handler(
PoemPath(id): PoemPath<String>,
body: Json<AssignAgentRequest>,
state: Data<&Arc<GatewayState>>,
) -> Response {
let project = body
.0
.project
.and_then(|p| if p.is_empty() { None } else { Some(p) });
if let Some(ref p) = project
&& !state.config.projects.contains_key(p.as_str())
{
return Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from(format!("unknown project '{p}'")));
}
let updated = {
let mut agents = state.joined_agents.write().await;
match agents.iter_mut().find(|a| a.id == id) {
None => None,
Some(a) => {
a.assigned_project = project;
Some(a.clone())
}
}
};
match updated {
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("agent not found")),
Some(agent) => {
crate::slog!(
"[gateway] Agent '{}' (id={}) assigned to {:?}",
agent.label,
agent.id,
agent.assigned_project
);
let agents = state.joined_agents.read().await.clone();
save_agents(&agents, &state.config_dir).await;
let body = serde_json::to_vec(&agent).unwrap_or_default();
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(body))
}
}
}
// ── Health aggregation endpoint ────────────────────────────────────── // ── Health aggregation endpoint ──────────────────────────────────────
/// HTTP GET `/health` handler for the gateway — aggregates health from all projects. /// HTTP GET `/health` handler for the gateway — aggregates health from all projects.
@@ -948,8 +1055,14 @@ pub async fn gateway_switch_handler(
/// Start the gateway HTTP server. This is the entry point when `--gateway` is used. /// Start the gateway HTTP server. This is the entry point when `--gateway` is used.
pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> { pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
// Locate the gateway config directory (parent of `projects.toml`).
let config_dir = config_path
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf();
let config = GatewayConfig::load(config_path).map_err(std::io::Error::other)?; let config = GatewayConfig::load(config_path).map_err(std::io::Error::other)?;
let state = GatewayState::new(config).map_err(std::io::Error::other)?; let state = GatewayState::new(config, config_dir.clone()).map_err(std::io::Error::other)?;
let state_arc = Arc::new(state); let state_arc = Arc::new(state);
let active = state_arc.active_project.read().await.clone(); let active = state_arc.active_project.read().await.clone();
@@ -965,12 +1078,6 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
.join(", ") .join(", ")
); );
// Locate the gateway config directory (parent of `projects.toml`).
let config_dir = config_path
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf();
// Write `.mcp.json` so that the gateway's Matrix bot's Claude Code CLI // Write `.mcp.json` so that the gateway's Matrix bot's Claude Code CLI
// connects to this gateway's MCP endpoint (which proxies to the active project). // connects to this gateway's MCP endpoint (which proxies to the active project).
if let Err(e) = write_gateway_mcp_json(&config_dir, port) { if let Err(e) = write_gateway_mcp_json(&config_dir, port) {
@@ -1010,6 +1117,10 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
"/gateway/agents/:id", "/gateway/agents/:id",
poem::delete(gateway_remove_agent_handler), poem::delete(gateway_remove_agent_handler),
) )
.at(
"/gateway/agents/:id/assign",
poem::post(gateway_assign_agent_handler),
)
// Serve the embedded React frontend so the gateway has a UI. // Serve the embedded React frontend so the gateway has a UI.
.at( .at(
"/assets/*path", "/assets/*path",
@@ -1127,7 +1238,7 @@ url = "http://localhost:3002"
let config = GatewayConfig { let config = GatewayConfig {
projects: BTreeMap::new(), projects: BTreeMap::new(),
}; };
assert!(GatewayState::new(config).is_err()); assert!(GatewayState::new(config, PathBuf::new()).is_err());
} }
#[test] #[test]
@@ -1146,7 +1257,7 @@ url = "http://localhost:3002"
}, },
); );
let config = GatewayConfig { projects }; let config = GatewayConfig { projects };
let state = GatewayState::new(config).unwrap(); let state = GatewayState::new(config, PathBuf::new()).unwrap();
let active = state.active_project.blocking_read().clone(); let active = state.active_project.blocking_read().clone();
assert_eq!(active, "alpha"); // BTreeMap sorts alphabetically. assert_eq!(active, "alpha"); // BTreeMap sorts alphabetically.
} }
@@ -1179,7 +1290,7 @@ url = "http://localhost:3002"
}, },
); );
let config = GatewayConfig { projects }; let config = GatewayConfig { projects };
let state = GatewayState::new(config).unwrap(); let state = GatewayState::new(config, PathBuf::new()).unwrap();
let params = json!({ "arguments": { "project": "beta" } }); let params = json!({ "arguments": { "project": "beta" } });
let resp = handle_switch_project(&params, &state).await; let resp = handle_switch_project(&params, &state).await;
@@ -1199,7 +1310,7 @@ url = "http://localhost:3002"
}, },
); );
let config = GatewayConfig { projects }; let config = GatewayConfig { projects };
let state = GatewayState::new(config).unwrap(); let state = GatewayState::new(config, PathBuf::new()).unwrap();
let params = json!({ "arguments": { "project": "nonexistent" } }); let params = json!({ "arguments": { "project": "nonexistent" } });
let resp = handle_switch_project(&params, &state).await; let resp = handle_switch_project(&params, &state).await;
@@ -1216,7 +1327,7 @@ url = "http://localhost:3002"
}, },
); );
let config = GatewayConfig { projects }; let config = GatewayConfig { projects };
let state = GatewayState::new(config).unwrap(); let state = GatewayState::new(config, PathBuf::new()).unwrap();
let url = state.active_url().await.unwrap(); let url = state.active_url().await.unwrap();
assert_eq!(url, "http://my:3001"); assert_eq!(url, "http://my:3001");
@@ -1352,7 +1463,7 @@ enabled = false
}, },
); );
let config = GatewayConfig { projects }; let config = GatewayConfig { projects };
Arc::new(GatewayState::new(config).unwrap()) Arc::new(GatewayState::new(config, PathBuf::new()).unwrap())
} }
#[tokio::test] #[tokio::test]
@@ -1452,6 +1563,7 @@ enabled = false
label: "agent-1".into(), label: "agent-1".into(),
address: "ws://a:3001/crdt-sync".into(), address: "ws://a:3001/crdt-sync".into(),
registered_at: 0.0, registered_at: 0.0,
assigned_project: None,
}); });
let app = poem::Route::new() let app = poem::Route::new()
.at("/gateway/agents", poem::get(gateway_list_agents_handler)) .at("/gateway/agents", poem::get(gateway_list_agents_handler))
@@ -1472,6 +1584,7 @@ enabled = false
label: "to-delete".into(), label: "to-delete".into(),
address: "ws://x:3001/crdt-sync".into(), address: "ws://x:3001/crdt-sync".into(),
registered_at: 0.0, registered_at: 0.0,
assigned_project: None,
}); });
let app = poem::Route::new() let app = poem::Route::new()
.at( .at(