Initial CodeCraft: web-based coding harness for AI models

Rust backend (axum/tokio): agent loop with streaming, tools (fs, bash,
web_fetch, todo, ask_user, subagents, skills, craft snapshots/compare),
permissions with modes and rules, Anthropic + OpenAI-compatible providers,
MCP client (stdio/HTTP, elicitation), SQLite sessions, usage statistics,
compaction, commented TOML config editable via toml_edit.

React PWA: chat with live streaming, permission/question cards, modes
(build/plan/craft), settings for providers/models/prices/MCP/permissions,
raw config editor, statistics dashboard, i18n (en/de). Docker setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 14:39:24 +01:00
commit f3a2fe3816
62 files changed
+21835

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
//! Tools that interact with the harness itself: todo list, questions to the
//! user and subagents.
use super::{Tool, ToolCtx, ToolOutput, arg_str};
use crate::types::Question;
use anyhow::{Result, bail};
use async_trait::async_trait;
use serde_json::{Value, json};
pub struct TodoWrite;
#[async_trait]
impl Tool for TodoWrite {
fn name(&self) -> &str {
"todo_write"
}
fn description(&self) -> String {
"Maintain a task list for the current work, shown live to the user. Send the complete \
list each time. Use it for multi-step tasks: exactly one item in_progress at a time, \
mark items completed as soon as they are done."
.into()
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}
},
"required": ["content", "status"]
}
}
},
"required": ["todos"]
})
}
fn read_only(&self) -> bool {
true
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
let todos = input.get("todos").cloned().unwrap_or(json!([]));
if !todos.is_array() {
bail!("todos must be an array");
}
ctx.app.set_todos(&ctx.session_id, todos.clone())?;
let open = todos.as_array().unwrap().iter().filter(|t| t["status"] != "completed").count();
Ok(ToolOutput::text(format!("Todo list updated ({open} open).")).with_meta(json!({"todos": todos})))
}
}
// ---------------------------------------------------------------------------
pub struct AskUser;
#[async_trait]
impl Tool for AskUser {
fn name(&self) -> &str {
"ask_user"
}
fn description(&self) -> String {
"Ask the user one or more questions and wait for the answers. Use this only when you are \
blocked on a decision that is genuinely the user's (requirements, preferences, choices \
between valid approaches) – not for things you can find out yourself. Offer 2-4 concrete \
options per question; the user can always answer with free text instead."
.into()
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"questions": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {
"type": "object",
"properties": {
"question": {"type": "string"},
"header": {"type": "string", "description": "Very short label (max 12 chars)"},
"multi_select": {"type": "boolean"},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {"type": "string"},
"description": {"type": "string"}
},
"required": ["label"]
}
}
},
"required": ["question"]
}
}
},
"required": ["questions"]
})
}
fn read_only(&self) -> bool {
true
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
let questions: Vec<Question> = serde_json::from_value(input["questions"].clone())?;
if questions.is_empty() {
bail!("at least one question is required");
}
let answers = ctx
.app
.ask_user(&ctx.session_id, "agent", questions.clone(), ctx.cancel.clone())
.await?;
let Some(answers) = answers else {
return Ok(ToolOutput::error("The user dismissed the questions without answering."));
};
let mut out = String::from("The user answered:\n");
for q in &questions {
let a = answers.get(&q.question).map(|v| match v {
Value::String(s) => s.clone(),
Value::Array(a) => a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", "),
other => other.to_string(),
});
out.push_str(&format!("- {}\n → {}\n", q.question, a.unwrap_or_else(|| "(no answer)".into())));
}
Ok(ToolOutput::text(out).with_meta(json!({"answers": answers})))
}
}
// ---------------------------------------------------------------------------
pub struct Task;
#[async_trait]
impl Tool for Task {
fn name(&self) -> &str {
"task"
}
fn description(&self) -> String {
"Launch a subagent to handle an independent, multi-step task in its own context (e.g. a \
broad codebase search, a focused review). The subagent only knows what you put in the \
prompt – write a complete, self-contained brief. Its final message is returned to you. \
Several task calls in one response run in parallel."
.into()
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"description": {"type": "string", "description": "Short (3-5 words) task description"},
"prompt": {"type": "string", "description": "The full brief for the subagent"},
"subagent_type": {"type": "string", "description": "Subagent to use (default: general)"}
},
"required": ["description", "prompt"]
})
}
fn read_only(&self) -> bool {
// Read-only agents (plan, explore) can spawn read-only subagents; the runner enforces it.
true
}
fn title(&self, input: &Value) -> String {
format!("Subagent: {}", input["description"].as_str().unwrap_or("task"))
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
let prompt = arg_str(&input, "prompt")?.to_string();
let description = input["description"].as_str().unwrap_or("subagent").to_string();
let agent = input["subagent_type"].as_str().unwrap_or("general").to_string();
let (child_id, result) =
crate::runner::run_subagent(ctx.app.clone(), ctx, &agent, &description, prompt).await?;
Ok(ToolOutput::text(result).with_meta(json!({"session_id": child_id, "agent": agent})))
}
}