Further WIP

This commit is contained in:
Louis Knight-Webb
2025-06-20 22:55:30 +01:00
parent 9c06c7fab3
commit 0fb477e738
12 changed files with 97 additions and 196 deletions

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "INSERT INTO task_attempts (id, task_id, worktree_path, merge_commit, executor, stdout, stderr) \n VALUES ($1, $2, $3, $4, $5, $6, $7) \n RETURNING id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, merge_commit, executor, stdout, stderr, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
"query": "INSERT INTO task_attempts (id, task_id, worktree_path, merge_commit, executor) \n VALUES ($1, $2, $3, $4, $5) \n RETURNING id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, merge_commit, executor, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
"describe": {
"columns": [
{
@@ -29,28 +29,18 @@
"type_info": "Text"
},
{
"name": "stdout",
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "stderr",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 7
"Right": 5
},
"nullable": [
true,
@@ -58,11 +48,9 @@
false,
true,
true,
true,
true,
false,
false
]
},
"hash": "32932b7fbc42de6c671d44d5e9a4ec7f73aadf48a482d4a8a9102cb503a9ffb9"
"hash": "0fc0dec5876cbe904d288a8e3bef15e0b7f75eb8e3c0fc1aeccd533ae73aab05"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE task_attempts SET stdout = COALESCE(stdout, '') || $1, updated_at = datetime('now') WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "3380f443d21ac408f96b014830322bd3411296b9ee8d29fe5f18974221bb0035"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, merge_commit, executor, stdout, stderr, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_attempts \n WHERE id = $1",
"query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, merge_commit, executor, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_attempts \n WHERE task_id = $1 \n ORDER BY created_at DESC",
"describe": {
"columns": [
{
@@ -29,23 +29,13 @@
"type_info": "Text"
},
{
"name": "stdout",
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "stderr",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"ordinal": 6,
"type_info": "Text"
}
],
@@ -58,11 +48,9 @@
false,
true,
true,
true,
true,
false,
false
]
},
"hash": "ab93108e1b1a983b7d7fe2bce4e37ea6cbd1bf7aa7b802d92e7bc33dab90b933"
"hash": "58bd05ee7354bb2aa7abffa7fa962f7143e071b9a1d39bca44e2e94512931209"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE task_attempts SET stderr = COALESCE(stderr, '') || $1, updated_at = datetime('now') WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "96957aa26773ee77bba123ebe27c675aa7698c235029ba09f2d503fea746baf6"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, merge_commit, executor, stdout, stderr, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_attempts \n WHERE task_id = $1 \n ORDER BY created_at DESC",
"query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, merge_commit, executor, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_attempts \n WHERE id = $1",
"describe": {
"columns": [
{
@@ -29,23 +29,13 @@
"type_info": "Text"
},
{
"name": "stdout",
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "stderr",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"ordinal": 6,
"type_info": "Text"
}
],
@@ -58,11 +48,9 @@
false,
true,
true,
true,
true,
false,
false
]
},
"hash": "3c109510d6e312b21f74508040a14c4f3c264cc8e53d864473b35e54662e99d5"
"hash": "a6058c6a30c6e3011e02b0ad81b2a98c5eca3bd88dec9632284ef7489f784fcd"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT ta.id as \"id!: Uuid\", ta.task_id as \"task_id!: Uuid\", ta.worktree_path, ta.merge_commit, ta.executor, ta.stdout, ta.stderr, ta.created_at as \"created_at!: DateTime<Utc>\", ta.updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_attempts ta \n JOIN tasks t ON ta.task_id = t.id \n WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3",
"query": "SELECT ta.id as \"id!: Uuid\", ta.task_id as \"task_id!: Uuid\", ta.worktree_path, ta.merge_commit, ta.executor, ta.created_at as \"created_at!: DateTime<Utc>\", ta.updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_attempts ta \n JOIN tasks t ON ta.task_id = t.id \n WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3",
"describe": {
"columns": [
{
@@ -29,23 +29,13 @@
"type_info": "Text"
},
{
"name": "stdout",
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "stderr",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"ordinal": 6,
"type_info": "Text"
}
],
@@ -58,11 +48,9 @@
false,
true,
true,
true,
true,
false,
false
]
},
"hash": "6c31a378ab5073e71dea882d4bdbe27c4091b5995f9c214f4e1b1043f6bcfeae"
"hash": "acb63e12f7fa91c1f1cd5513b6dae0d8bff94f8b675c0c5b81f429e44158d8a8"
}

View File

@@ -0,0 +1,28 @@
PRAGMA foreign_keys = ON;
-- Remove stdout and stderr columns from task_attempts table
-- These are now tracked in the execution_processes table for better granularity
-- SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
-- First, create a new table without stdout and stderr
CREATE TABLE task_attempts_new (
id BLOB PRIMARY KEY,
task_id BLOB NOT NULL,
worktree_path TEXT NOT NULL,
merge_commit TEXT,
executor TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
-- Copy data from old table to new table (excluding stdout and stderr)
INSERT INTO task_attempts_new (id, task_id, worktree_path, merge_commit, executor, created_at, updated_at)
SELECT id, task_id, worktree_path, merge_commit, executor, created_at, updated_at
FROM task_attempts;
-- Drop the old table
DROP TABLE task_attempts;
-- Rename the new table to the original name
ALTER TABLE task_attempts_new RENAME TO task_attempts;

View File

@@ -49,6 +49,7 @@ pub trait Executor: Send + Sync {
pool: &sqlx::SqlitePool,
task_id: Uuid,
attempt_id: Uuid,
execution_process_id: Uuid,
worktree_path: &str,
) -> Result<Child, ExecutorError> {
let mut child = self.spawn(pool, task_id, worktree_path).await?;
@@ -67,8 +68,20 @@ pub trait Executor: Send + Sync {
let pool_clone1 = pool.clone();
let pool_clone2 = pool.clone();
tokio::spawn(stream_output_to_db(stdout, pool_clone1, attempt_id, true));
tokio::spawn(stream_output_to_db(stderr, pool_clone2, attempt_id, false));
tokio::spawn(stream_output_to_db(
stdout,
pool_clone1,
attempt_id,
execution_process_id,
true,
));
tokio::spawn(stream_output_to_db(
stderr,
pool_clone2,
attempt_id,
execution_process_id,
false,
));
Ok(child)
}
@@ -102,9 +115,10 @@ async fn stream_output_to_db(
output: impl tokio::io::AsyncRead + Unpin,
pool: sqlx::SqlitePool,
attempt_id: Uuid,
execution_process_id: Uuid,
is_stdout: bool,
) {
use crate::models::task_attempt::TaskAttempt;
use crate::models::execution_process::ExecutionProcess;
let mut reader = BufReader::new(output);
let mut line = String::new();
@@ -121,9 +135,9 @@ async fn stream_output_to_db(
// Update database every 1 lines or when we have a significant amount of data
if update_counter >= 1 || accumulated_output.len() > 1024 {
if let Err(e) = TaskAttempt::append_output(
if let Err(e) = ExecutionProcess::append_output(
&pool,
attempt_id,
execution_process_id,
if is_stdout {
Some(&accumulated_output)
} else {
@@ -162,9 +176,9 @@ async fn stream_output_to_db(
// Flush any remaining output
if !accumulated_output.is_empty() {
if let Err(e) = TaskAttempt::append_output(
if let Err(e) = ExecutionProcess::append_output(
&pool,
attempt_id,
execution_process_id,
if is_stdout {
Some(&accumulated_output)
} else {

View File

@@ -69,8 +69,6 @@ pub struct TaskAttempt {
pub worktree_path: String,
pub merge_commit: Option<String>,
pub executor: Option<String>, // Name of the executor to use
pub stdout: Option<String>,
pub stderr: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -132,7 +130,7 @@ impl TaskAttempt {
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!(
TaskAttempt,
r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, merge_commit, executor, stdout, stderr, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, merge_commit, executor, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts
WHERE id = $1"#,
id
@@ -147,7 +145,7 @@ impl TaskAttempt {
) -> Result<Vec<Self>, sqlx::Error> {
sqlx::query_as!(
TaskAttempt,
r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, merge_commit, executor, stdout, stderr, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, merge_commit, executor, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts
WHERE task_id = $1
ORDER BY created_at DESC"#,
@@ -191,16 +189,14 @@ impl TaskAttempt {
// Insert the record into the database
Ok(sqlx::query_as!(
TaskAttempt,
r#"INSERT INTO task_attempts (id, task_id, worktree_path, merge_commit, executor, stdout, stderr)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, merge_commit, executor, stdout, stderr, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
r#"INSERT INTO task_attempts (id, task_id, worktree_path, merge_commit, executor)
VALUES ($1, $2, $3, $4, $5)
RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, merge_commit, executor, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
attempt_id,
data.task_id,
data.worktree_path,
data.merge_commit,
data.executor,
None::<String>, // stdout
None::<String> // stderr
data.executor
)
.fetch_one(pool)
.await?)
@@ -240,36 +236,6 @@ impl TaskAttempt {
}
}
/// Append to stdout and stderr for this task attempt (for streaming updates)
pub async fn append_output(
pool: &SqlitePool,
id: Uuid,
stdout_append: Option<&str>,
stderr_append: Option<&str>,
) -> Result<(), sqlx::Error> {
if let Some(stdout_data) = stdout_append {
sqlx::query!(
"UPDATE task_attempts SET stdout = COALESCE(stdout, '') || $1, updated_at = datetime('now') WHERE id = $2",
stdout_data,
id
)
.execute(pool)
.await?;
}
if let Some(stderr_data) = stderr_append {
sqlx::query!(
"UPDATE task_attempts SET stderr = COALESCE(stderr, '') || $1, updated_at = datetime('now') WHERE id = $2",
stderr_data,
id
)
.execute(pool)
.await?;
}
Ok(())
}
/// Perform the actual git merge operations (synchronous)
fn perform_merge_operation(
worktree_path: &str,
@@ -381,7 +347,7 @@ impl TaskAttempt {
// Get the task attempt with validation
let attempt = sqlx::query_as!(
TaskAttempt,
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.stdout, ta.stderr, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#,
@@ -585,7 +551,13 @@ impl TaskAttempt {
ExecutionProcess::create(pool, &create_agent_process, agent_process_id).await?;
let child = executor
.execute_streaming(pool, task_id, attempt_id, &task_attempt.worktree_path)
.execute_streaming(
pool,
task_id,
attempt_id,
agent_process_id,
&task_attempt.worktree_path,
)
.await
.map_err(|e| TaskAttemptError::Git(git2::Error::from_str(&e.to_string())))?;
@@ -620,7 +592,7 @@ impl TaskAttempt {
// Get the task attempt with validation
let attempt = sqlx::query_as!(
TaskAttempt,
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.stdout, ta.stderr, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#,
@@ -881,7 +853,7 @@ impl TaskAttempt {
// Get the task attempt with validation
let attempt = sqlx::query_as!(
TaskAttempt,
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.stdout, ta.stderr, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#,
@@ -1020,7 +992,7 @@ impl TaskAttempt {
// Get the task attempt with validation
let attempt = sqlx::query_as!(
TaskAttempt,
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.stdout, ta.stderr, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#,
@@ -1056,7 +1028,7 @@ impl TaskAttempt {
// Get the task attempt with validation
let attempt = sqlx::query_as!(
TaskAttempt,
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.stdout, ta.stderr, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.merge_commit, ta.executor, ta.created_at as "created_at!: DateTime<Utc>", ta.updated_at as "updated_at!: DateTime<Utc>"
FROM task_attempts ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#,

View File

@@ -388,51 +388,8 @@ export function TaskDetailsDialog({
</CardContent>
</Card>
{/* Task Attempt Output */}
{selectedAttempt &&
(selectedAttempt.stdout || selectedAttempt.stderr) && (
<Card className="bg-black">
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4 text-green-400">
Execution Output
</h3>
<div className="space-y-4">
{selectedAttempt.stdout && (
<div>
<Label className="text-sm font-medium mb-2 block text-console-success">
STDOUT
</Label>
<div
className="bg-console text-console-success border border-console-success rounded-md p-4 font-mono text-sm max-h-96 overflow-y-auto whitespace-pre-wrap shadow-inner"
style={{
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
}}
>
{selectedAttempt.stdout}
</div>
</div>
)}
{selectedAttempt.stderr && (
<div>
<Label className="text-sm font-medium mb-2 block text-console-error">
STDERR
</Label>
<div
className="bg-console text-console-error border border-console-error rounded-md p-4 font-mono text-sm max-h-96 overflow-y-auto whitespace-pre-wrap shadow-inner"
style={{
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
}}
>
{selectedAttempt.stderr}
</div>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* TODO: Task Attempt Output - migrate to use ExecutionProcess data */}
{/* ExecutionProcess stdout/stderr display will be implemented when execution processes are exposed via API */}
</div>
{/* Sidebar */}

View File

@@ -11,7 +11,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { ArrowLeft, FileText, Code, Monitor, Braces } from "lucide-react";
import { ArrowLeft, FileText, Code /* , Monitor, Braces */ } from "lucide-react";
import { makeRequest } from "@/lib/api";
import { TaskFormDialog } from "@/components/tasks/TaskFormDialog";
import { useKeyboardShortcuts } from "@/lib/keyboard-shortcuts";
@@ -91,7 +91,7 @@ export function TaskDetailsPage() {
const [stoppingAttempt, setStoppingAttempt] = useState(false);
const [openingEditor, setOpeningEditor] = useState(false);
const [error, setError] = useState<string | null>(null);
const [outputViewMode, setOutputViewMode] = useState<'console' | 'json'>('console');
// const [outputViewMode, setOutputViewMode] = useState<'console' | 'json'>('console');
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
@@ -416,7 +416,7 @@ export function TaskDetailsPage() {
navigate(`/projects/${projectId}/tasks`);
};
const parseJsonLines = (jsonlText: string) => {
/* const parseJsonLines = (jsonlText: string) => {
const lines = jsonlText.split('\n').filter(line => line.trim());
const parsedLines: { json: any; error?: string; raw: string }[] = [];
@@ -434,7 +434,7 @@ export function TaskDetailsPage() {
});
return parsedLines;
};
}; */
if (taskLoading) {
return (
@@ -526,9 +526,10 @@ export function TaskDetailsPage() {
</CardContent>
</Card>
{/* Task Attempt Output */}
{selectedAttempt &&
(selectedAttempt.stdout || selectedAttempt.stderr) && (
{/* TODO: Task Attempt Output - migrate to use ExecutionProcess data */}
{/* ExecutionProcess stdout/stderr display will be implemented when execution processes are exposed via API */}
{/*
{selectedAttempt && (
<Card className="bg-black">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
@@ -675,7 +676,8 @@ export function TaskDetailsPage() {
)}
</CardContent>
</Card>
)}
)
*/}
</div>
{/* Sidebar */}

View File

@@ -36,7 +36,7 @@ export type UpdateTask = { title: string | null, description: string | null, sta
export type TaskAttemptStatus = "init" | "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed" | "paused";
export type TaskAttempt = { id: string, task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, stdout: string | null, stderr: string | null, created_at: string, updated_at: string, };
export type TaskAttempt = { id: string, task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, created_at: string, updated_at: string, };
export type CreateTaskAttempt = { task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, };