fix(mise-init): unblock the bootstrap agent + surface live agent output

The mise-init agent wedged on launch and the UI reported it green. Three
distinct problems, fixed together:

1. Onboarding wedge (the proximate bug). A freshly-installed claude opens
   interactive setup — theme picker, then a folder-trust prompt — before
   the REPL. A detached tmux agent has no one to answer it, so it sat on
   the theme picker forever while agents.status said 'working'. New
   control.claude_config.ensure_onboarded() marks onboarding complete and
   trusts the working dir in ~/.claude.json (merge-only, so the login
   flow's oauthAccount survives); spawn() calls it before launching.

2. Config-name gate. control/mise.py only recognized `.mise.toml`, so a
   repo shipping `mise.toml` (no dot) — or config under `.config/mise/` —
   failed the [tasks.test] gate even when healthy. It now accepts the
   filenames mise itself reads and scans them all for the test task.

3. "Done" != done (the design gap). A spawned agent's real state lives in
   its tmux pane, but the socket is control-container-only, so the API
   couldn't see it. The worker now snapshots each working agent's pane
   tail (last ~40 lines) into two new agents columns (last_output,
   output_at, migration 0007) on its existing poll loop; the API
   serializes them and AgentsSection renders a live-output <pre> under
   each running agent. A wedged agent now shows the theme picker instead
   of a misleading green badge.

Tests: home-dir writes are isolated to tmp in conftest; added coverage for
claude_config seeding/merge, the mise filename set, the worker capture
(including dead-session skip), and the API serialization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
This commit is contained in:
Claude
2026-07-16 19:30:58 +00:00
parent 43e1fa3619
commit 24b8c44451
27 changed files with 474 additions and 54 deletions
+51 -24
View File
@@ -2,10 +2,10 @@
* Spawning enqueues a control command that the worker turns into a tmux + claude process. */
"use client";
import { useMemo, useState } from "react";
import { Fragment, useMemo, useState } from "react";
import { useDashboard } from "@/components/store";
import { Badge, Button, Card, Input, Select, StatusBadge, Textarea } from "@/components/ui";
import { fmtFull } from "@/lib/format";
import { fmtFull, timeAgo } from "@/lib/format";
const ROLE_OPTS = [
{ value: "", label: "Role — none" },
@@ -121,28 +121,55 @@ export function AgentsSection() {
</thead>
<tbody>
{agents.map((a) => (
<tr key={a.id}>
<td className="mono">{a.name}</td>
<td>{a.role ? <Badge tone="info">{a.role}</Badge> : "—"}</td>
<td>
<StatusBadge status={a.status} />
</td>
<td className="mono faint">{a.working_dir}</td>
<td className="faint nowrap">{fmtFull(a.created_at)}</td>
<td className="nowrap">
<div className="hstack">
<Button size="sm" variant="ghost" onClick={() => s.selectRun(a.project_id, a.name)}>
Open
</Button>
<Button size="sm" variant="secondary" onClick={() => s.killAgent(a.project_id, a.name)}>
Kill
</Button>
<Button size="sm" variant="danger" onClick={() => s.deleteAgent(a.project_id, a.name)}>
Delete
</Button>
</div>
</td>
</tr>
<Fragment key={a.id}>
<tr>
<td className="mono">{a.name}</td>
<td>{a.role ? <Badge tone="info">{a.role}</Badge> : "—"}</td>
<td>
<StatusBadge status={a.status} />
</td>
<td className="mono faint">{a.working_dir}</td>
<td className="faint nowrap">{fmtFull(a.created_at)}</td>
<td className="nowrap">
<div className="hstack">
<Button size="sm" variant="ghost" onClick={() => s.selectRun(a.project_id, a.name)}>
Open
</Button>
<Button size="sm" variant="secondary" onClick={() => s.killAgent(a.project_id, a.name)}>
Kill
</Button>
<Button size="sm" variant="danger" onClick={() => s.deleteAgent(a.project_id, a.name)}>
Delete
</Button>
</div>
</td>
</tr>
{a.status === "working" && a.last_output?.trim() && (
<tr>
<td colSpan={6} style={{ paddingTop: 0 }}>
<div className="faint" style={{ fontSize: "var(--text-xs)", marginBottom: 4 }}>
live output{a.output_at ? ` · ${timeAgo(a.output_at)}` : ""}
</div>
<pre
className="mono"
style={{
margin: 0,
padding: "8px 10px",
background: "var(--surface-2, rgba(0,0,0,0.25))",
borderRadius: 6,
fontSize: "var(--text-xs)",
lineHeight: 1.4,
maxHeight: 220,
overflow: "auto",
whiteSpace: "pre",
}}
>
{a.last_output}
</pre>
</td>
</tr>
)}
</Fragment>
))}
</tbody>
</table>
+4
View File
@@ -30,6 +30,10 @@ export interface Agent {
working_dir: string;
status: string;
role?: string | null;
/* Latest tmux pane-tail snapshot from the worker, so the UI can show what a running
* agent is actually doing (and expose one wedged on an interactive prompt). */
last_output?: string | null;
output_at?: string | null;
created_at: string;
}