@ours.network/fleet 1.0.4 → 1.1.0-nightly.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +41 -50
  2. package/dist/application/capabilities.d.ts +0 -1
  3. package/dist/application/capabilities.js +3 -13
  4. package/dist/application/errors.d.ts +1 -1
  5. package/dist/application/fleet-query-service.d.ts +0 -3
  6. package/dist/application/fleet-query-service.js +1 -24
  7. package/dist/application/role-creation-service.d.ts +5 -5
  8. package/dist/application/role-creation-service.js +5 -7
  9. package/dist/application/role-repository.d.ts +1 -2
  10. package/dist/application/role-repository.js +7 -7
  11. package/dist/application/session-control.d.ts +0 -12
  12. package/dist/application/session-control.js +0 -42
  13. package/dist/application/session-mutations.d.ts +5 -5
  14. package/dist/application/task-room-service.d.ts +37 -2
  15. package/dist/application/task-room-service.js +49 -7
  16. package/dist/application/types.d.ts +0 -7
  17. package/dist/atomic-file.d.ts +5 -3
  18. package/dist/atomic-file.js +32 -8
  19. package/dist/briefing.js +1 -4
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +9 -23
  22. package/dist/config.d.ts +7 -7
  23. package/dist/config.js +13 -34
  24. package/dist/docs.d.ts +1 -1
  25. package/dist/docs.js +46 -30
  26. package/dist/doctor.js +0 -7
  27. package/dist/fleet-proxy.d.ts +1 -1
  28. package/dist/harness/acp-session-transport.d.ts +8 -0
  29. package/dist/harness/acp-session-transport.js +3 -0
  30. package/dist/harness/agent-session.d.ts +33 -0
  31. package/dist/harness/agent-session.js +1 -0
  32. package/dist/harness/claude-code-session.d.ts +18 -0
  33. package/dist/harness/claude-code-session.js +28 -0
  34. package/dist/harness/claude-code.d.ts +2 -1
  35. package/dist/harness/claude-code.js +29 -81
  36. package/dist/harness/codex-session.d.ts +18 -0
  37. package/dist/harness/codex-session.js +28 -0
  38. package/dist/harness/codex.d.ts +2 -1
  39. package/dist/harness/codex.js +35 -45
  40. package/dist/harness/types.d.ts +6 -38
  41. package/dist/index.d.ts +3 -5
  42. package/dist/index.js +1 -3
  43. package/dist/isolation/resources.d.ts +3 -7
  44. package/dist/isolation/resources.js +3 -7
  45. package/dist/model-env.d.ts +2 -3
  46. package/dist/model-env.js +2 -3
  47. package/dist/monitor.d.ts +1 -24
  48. package/dist/monitor.js +5 -140
  49. package/dist/owner-channel/attachments.d.ts +2 -4
  50. package/dist/owner-channel/attachments.js +6 -39
  51. package/dist/owner-channel/channel.d.ts +2 -2
  52. package/dist/owner-channel/channel.js +16 -3
  53. package/dist/owner-channel/commands.d.ts +18 -1
  54. package/dist/owner-channel/commands.js +73 -4
  55. package/dist/rooms-tasks/cli.js +121 -2
  56. package/dist/rooms-tasks/task-lists.d.ts +19 -0
  57. package/dist/rooms-tasks/task-lists.js +96 -0
  58. package/dist/rooms-tasks/task-state.d.ts +3 -0
  59. package/dist/rooms-tasks/task-state.js +281 -175
  60. package/dist/rooms-tasks/types.d.ts +11 -1
  61. package/dist/runner.d.ts +5 -15
  62. package/dist/runner.js +26 -102
  63. package/dist/session/acp.d.ts +2 -2
  64. package/dist/session/acp.js +2 -3
  65. package/dist/session/activity.d.ts +2 -3
  66. package/dist/session/activity.js +2 -3
  67. package/dist/session/arbiter.d.ts +4 -4
  68. package/dist/session/control.d.ts +3 -3
  69. package/dist/session/types.d.ts +11 -5
  70. package/dist/spawn.js +8 -9
  71. package/dist/supervisor/none.d.ts +1 -1
  72. package/dist/supervisor/none.js +2 -2
  73. package/dist/tmux.d.ts +1 -14
  74. package/dist/tmux.js +1 -51
  75. package/dist/watchdog/config.js +6 -4
  76. package/dist/watchdog/run.js +8 -23
  77. package/dist/web/auth.d.ts +1 -1
  78. package/dist/web/runtime.js +6 -15
  79. package/dist/web/server.d.ts +3 -3
  80. package/dist/web/server.js +89 -15
  81. package/dist/web/topology-promote.js +2 -5
  82. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  83. package/dist/web-app/assets/{index-BCBK78hw.js → index-DhMDVRU1.js} +6 -6
  84. package/dist/web-app/index.html +2 -2
  85. package/package.json +3 -8
  86. package/dist/session/tmux.d.ts +0 -28
  87. package/dist/session/tmux.js +0 -80
  88. package/dist/web/terminal/bridge.d.ts +0 -27
  89. package/dist/web/terminal/bridge.js +0 -317
  90. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  91. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -1,7 +1,6 @@
1
1
  import { closeSync, existsSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
- import { spawn as spawnChild, execFile } from 'node:child_process';
2
+ import { spawn as spawnChild } from 'node:child_process';
3
3
  import { join } from 'node:path';
4
- import { promisify } from 'node:util';
5
4
  import { stringify } from 'yaml';
6
5
  import { errorReport, normalizeWatchdogReport, validateWatchdogReport } from './report.js';
7
6
  import { acquireRunLock, formatRunId, pruneReports, releaseRunLock, writeReport } from './store.js';
@@ -15,8 +14,6 @@ import { loadConfig, resolveMonitorConfig, resolveWorklogPolicy, ROLE_NAME_RE, }
15
14
  import { getAdapter } from '../harness/registry.js';
16
15
  import { redactLogLine } from '../application/log-service.js';
17
16
  import { controlRequest, controlSocketPath } from '../session/control.js';
18
- import { Tmux } from '../tmux.js';
19
- const execFileAsync = promisify(execFile);
20
17
  /** How often the deadline loop polls for a completed report or a dead child. */
21
18
  const POLL_MS = 1000;
22
19
  /** A `report.json` this old, with no further writes, is treated as finished. */
@@ -56,7 +53,6 @@ async function killIfNeeded(reason, child, roleName, sleep) {
56
53
  if (reason === 'stable')
57
54
  await sleep(HARVEST_GRACE_MS);
58
55
  child.kill();
59
- await killTmuxSession(roleName);
60
56
  }
61
57
  }
62
58
  function defaultLaunchChild(binPath, roleName, runDir) {
@@ -77,13 +73,6 @@ function defaultLaunchChild(binPath, roleName, runDir) {
77
73
  }),
78
74
  };
79
75
  }
80
- /** Best-effort: the tmux session outlives the supervisor child (`runOnce` created it). */
81
- async function killTmuxSession(roleName) {
82
- try {
83
- await execFileAsync('tmux', ['kill-session', '-t', roleName]);
84
- }
85
- catch { /* best effort */ }
86
- }
87
76
  /** Last 4096 chars of run.log, redacted — attached to error reports as diagnostic tail. */
88
77
  function readTail(runDir) {
89
78
  try {
@@ -121,19 +110,15 @@ async function discoverLiveTemporaryRoles() {
121
110
  catch {
122
111
  return [];
123
112
  }
124
- const tmux = new Tmux();
125
113
  const live = await Promise.all(names.map(async (name) => {
126
114
  const dir = agentDir(name, true);
127
- const [acpAlive, tmuxAlive] = await Promise.all([
128
- existsSync(controlSocketPath(dir))
129
- ? controlRequest(dir, { command: 'status' }, 2_000)
130
- .then(response => response.ok
131
- && response.result?.alive === true)
132
- .catch(() => false)
133
- : Promise.resolve(false),
134
- tmux.has(name).catch(() => false),
135
- ]);
136
- return acpAlive || tmuxAlive ? name : undefined;
115
+ const alive = existsSync(controlSocketPath(dir))
116
+ ? await controlRequest(dir, { command: 'status' }, 2_000)
117
+ .then(response => response.ok
118
+ && response.result?.alive === true)
119
+ .catch(() => false)
120
+ : false;
121
+ return alive ? name : undefined;
137
122
  }));
138
123
  return live.filter((name) => name !== undefined);
139
124
  }
@@ -16,7 +16,7 @@ export interface AuthResult {
16
16
  interface Ticket {
17
17
  value: string;
18
18
  sessionId: string;
19
- purpose: 'events' | 'terminal' | 'conversation';
19
+ purpose: 'events' | 'conversation';
20
20
  roleId?: string;
21
21
  expiresAt: number;
22
22
  }
@@ -4,14 +4,13 @@ import { resolve } from 'node:path';
4
4
  import { loadConfig } from '../config.js';
5
5
  import { RoleRepository } from '../application/role-repository.js';
6
6
  import { FleetQueryService } from '../application/fleet-query-service.js';
7
- import { AcpRoleSessionAdapter, TmuxRoleSessionAdapter } from '../application/session-control.js';
7
+ import { AcpRoleSessionAdapter } from '../application/session-control.js';
8
8
  import { StructuredLogService } from '../application/log-service.js';
9
9
  import { RoleCommandService } from '../application/role-command-service.js';
10
10
  import { RoleCreationService } from '../application/role-creation-service.js';
11
11
  import { RoleRemovalService } from '../application/role-removal-service.js';
12
12
  import { FleetError } from '../application/errors.js';
13
13
  import { controlRequest, controlSocketPath } from '../session/control.js';
14
- import { Tmux } from '../tmux.js';
15
14
  import { pickBackend } from '../supervisor/index.js';
16
15
  import { realExec } from '../exec.js';
17
16
  import { home, stateRoot } from '../paths.js';
@@ -23,7 +22,6 @@ import { mergeTopology } from './topology-model.js';
23
22
  import { TopologyDraftStore } from './topology-draft-store.js';
24
23
  import { TopologyPromoteService } from './topology-promote.js';
25
24
  import { doctor } from '../doctor.js';
26
- import { TerminalBridgeManager } from './terminal/bridge.js';
27
25
  import { acquireWebServerLock } from './lock.js';
28
26
  import { TrustedDeviceStore } from './device-store.js';
29
27
  import { WebAuth } from './auth.js';
@@ -31,6 +29,7 @@ import { startWebControlServer } from './control.js';
31
29
  import { WebAccessStore, validatePublicOrigin } from './access.js';
32
30
  import { buildWatchdogFindings, cachedWatchdogFindingsProvider, WatchdogQueryService } from '../watchdog/query.js';
33
31
  import { latestReport } from '../watchdog/store.js';
32
+ import { TaskRoomApplicationService } from '../application/task-room-service.js';
34
33
  const CONFIG_CACHE_TTL_MS = 5_000;
35
34
  /**
36
35
  * loadConfig re-parses YAML from disk on every call; the watchdog list/reports
@@ -61,7 +60,6 @@ export async function startWebConsole(options) {
61
60
  }
62
61
  const access = options.access ?? new WebAccessStore(webDir).read();
63
62
  const auth = new WebAuth(publicOrigin?.origin ?? `http://127.0.0.1:${requestedPort}`, publicOrigin?.host ?? `127.0.0.1:${requestedPort}`, Date.now, new TrustedDeviceStore(webDir), access);
64
- const tmux = new Tmux();
65
63
  const backend = pickBackend();
66
64
  const repository = new RoleRepository({
67
65
  configPath: options.configPath,
@@ -77,13 +75,11 @@ export async function startWebConsole(options) {
77
75
  }
78
76
  catch { /* stale socket is evidence, not reachability */ }
79
77
  }
80
- return { acp, tmux: await tmux.has(name).catch(() => false) };
78
+ return { acp };
81
79
  },
82
80
  });
83
81
  const events = new FleetEventBus();
84
82
  const audit = new AuditSink();
85
- const terminals = new TerminalBridgeManager({ repository, audit, tmux });
86
- const terminalAvailable = await terminals.available();
87
83
  const watchdogConfigProvider = cachedConfigProvider(options.configPath);
88
84
  const log = options.log ?? (() => { });
89
85
  let loggedWatchdogFindingsError = false;
@@ -108,8 +104,8 @@ export async function startWebConsole(options) {
108
104
  }
109
105
  }, CONFIG_CACHE_TTL_MS);
110
106
  const query = new FleetQueryService({
111
- repository, supervisor: backend, tmux,
112
- capabilityContext: { terminalPtyAvailable: terminalAvailable },
107
+ repository, supervisor: backend,
108
+ capabilityContext: {},
113
109
  watchdogFindings,
114
110
  });
115
111
  const ops = { backend, binPath: options.binPath, log };
@@ -159,10 +155,8 @@ export async function startWebConsole(options) {
159
155
  try {
160
156
  server = await buildWebServer({
161
157
  query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration,
158
+ taskRooms: new TaskRoomApplicationService(options.configPath),
162
159
  topology: readTopology, topologyDrafts, topologyPromote,
163
- terminalUpgrade: terminalAvailable
164
- ? async (socket, _request, roleId, _ticket, hello) => terminals.connect(socket, roleId, hello)
165
- : undefined,
166
160
  async session(roleId) {
167
161
  const role = await repository.get(roleId);
168
162
  if (!role)
@@ -173,8 +167,6 @@ export async function startWebConsole(options) {
173
167
  throw new FleetError('control_unavailable', 'role state directory is unavailable');
174
168
  return new AcpRoleSessionAdapter(dir);
175
169
  }
176
- if (role.configuredBackend === 'tmux' || role.detectedBackend === 'tmux')
177
- return new TmuxRoleSessionAdapter(roleId, tmux);
178
170
  throw new FleetError('capability_unavailable', 'role session backend is unavailable');
179
171
  },
180
172
  }, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth });
@@ -229,7 +221,6 @@ export async function startWebConsole(options) {
229
221
  async close() {
230
222
  try {
231
223
  await control.close();
232
- await terminals.close();
233
224
  await server.close();
234
225
  }
235
226
  finally {
@@ -1,5 +1,4 @@
1
- import { type FastifyInstance, type FastifyRequest } from 'fastify';
2
- import type { WebSocket } from 'ws';
1
+ import { type FastifyInstance } from 'fastify';
3
2
  import type { FleetQueryService } from '../application/fleet-query-service.js';
4
3
  import type { RoleRepository } from '../application/role-repository.js';
5
4
  import type { RoleSessionControl } from '../application/session-control.js';
@@ -15,6 +14,7 @@ import type { MergedTopology } from './topology-model.js';
15
14
  import type { TopologyDraftStore } from './topology-draft-store.js';
16
15
  import type { TopologyPromoteService } from './topology-promote.js';
17
16
  import type { RoleRemovalService } from '../application/role-removal-service.js';
17
+ import type { TaskRoomApplicationService } from '../application/task-room-service.js';
18
18
  export interface WebServices {
19
19
  query: FleetQueryService;
20
20
  repository: RoleRepository;
@@ -30,7 +30,7 @@ export interface WebServices {
30
30
  topologyDrafts?: TopologyDraftStore;
31
31
  topologyPromote?: TopologyPromoteService;
32
32
  removal?: RoleRemovalService;
33
- terminalUpgrade?: (socket: WebSocket, request: FastifyRequest, roleId: string, ticket: string, hello: Record<string, unknown>) => Promise<void>;
33
+ taskRooms?: TaskRoomApplicationService;
34
34
  }
35
35
  export interface WebServer {
36
36
  app: FastifyInstance;
@@ -11,8 +11,10 @@ import { AuditSink } from './audit.js';
11
11
  import { WebAuth } from './auth.js';
12
12
  import { FleetEventBus } from './events.js';
13
13
  import { ROLE_NAME_RE } from '../config.js';
14
+ import { TaskListError } from '../rooms-tasks/task-lists.js';
15
+ import { TaskStateError } from '../rooms-tasks/task-state.js';
14
16
  const statusFor = (code) => ({
15
- role_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
17
+ role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
16
18
  idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
17
19
  invalid_request: 400, capability_unavailable: 409, prerequisite_unavailable: 503,
18
20
  }[code] ?? 500);
@@ -99,7 +101,7 @@ export async function buildWebServer(services, boundary, options = {}) {
99
101
  return {
100
102
  version: VERSION, api: { major: 1, minor: 0 },
101
103
  websocketProtocols: [
102
- 'ours-fleet-events.v1', 'ours-fleet-terminal.v1', 'ours-fleet-conversation.v1',
104
+ 'ours-fleet-events.v1', 'ours-fleet-conversation.v1',
103
105
  ],
104
106
  auditDegraded: audit.degraded,
105
107
  };
@@ -128,6 +130,89 @@ export async function buildWebServer(services, boundary, options = {}) {
128
130
  auth.authenticate(request);
129
131
  return { roles: await services.query.list() };
130
132
  });
133
+ const taskApi = async (fn) => {
134
+ try {
135
+ return await fn();
136
+ }
137
+ catch (error) {
138
+ if (error instanceof TaskListError) {
139
+ const code = error.code === 'list_not_found' ? 'resource_not_found'
140
+ : ['duplicate_name', 'destination_required', 'same_destination', 'default_immutable'].includes(error.code)
141
+ ? 'conflict' : 'invalid_request';
142
+ throw new FleetError(code, error.message);
143
+ }
144
+ if (error instanceof TaskStateError) {
145
+ throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'conflict', error.message);
146
+ }
147
+ throw error;
148
+ }
149
+ };
150
+ const requireTaskRooms = () => {
151
+ if (!services.taskRooms)
152
+ throw new FleetError('capability_unavailable', 'task operations are unavailable');
153
+ return services.taskRooms;
154
+ };
155
+ app.get('/api/v1/task-lists', async (request) => {
156
+ auth.authenticate(request);
157
+ return { lists: requireTaskRooms().listTaskLists() };
158
+ });
159
+ app.post('/api/v1/task-lists', async (request, reply) => {
160
+ auth.authenticate(request, true);
161
+ const name = String(request.body?.name ?? '');
162
+ const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name }));
163
+ reply.code(201);
164
+ return { list };
165
+ });
166
+ app.patch('/api/v1/task-lists/:name', async (request) => {
167
+ auth.authenticate(request, true);
168
+ const name = request.params.name;
169
+ const newName = String(request.body?.name ?? '');
170
+ return { list: await taskApi(() => requireTaskRooms().renameTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, newName })) };
171
+ });
172
+ app.delete('/api/v1/task-lists/:name', async (request) => {
173
+ auth.authenticate(request, true);
174
+ const name = request.params.name;
175
+ const destination = request.query.destination;
176
+ return taskApi(() => requireTaskRooms().deleteTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, destination }));
177
+ });
178
+ app.get('/api/v1/tasks', async (request) => {
179
+ auth.authenticate(request);
180
+ const query = request.query;
181
+ const states = ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'];
182
+ if (query.state && query.state !== 'all' && !states.includes(query.state))
183
+ throw new FleetError('invalid_request', 'invalid task state filter');
184
+ if (query.groupByList !== undefined && !['true', 'false'].includes(query.groupByList))
185
+ throw new FleetError('invalid_request', 'groupByList must be true or false');
186
+ const state = query.state && query.state !== 'all' ? query.state : undefined;
187
+ const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}) };
188
+ return taskApi(() => query.groupByList === 'true'
189
+ ? { groups: requireTaskRooms().groupedTasks(filter) }
190
+ : { tasks: requireTaskRooms().listTasks(filter) });
191
+ });
192
+ app.post('/api/v1/tasks', async (request, reply) => {
193
+ auth.authenticate(request, true);
194
+ const body = request.body;
195
+ if (typeof body?.title !== 'string' || !body.title)
196
+ throw new FleetError('invalid_request', 'title is required');
197
+ const task = await taskApi(() => requireTaskRooms().createTask({
198
+ actor: { kind: 'local_control', surface: 'web' }, title: body.title,
199
+ brief: typeof body.brief === 'string' ? body.brief : undefined,
200
+ template: typeof body.template === 'string' ? body.template : undefined,
201
+ backlog: body.backlog === true, noRoom: body.noRoom === true,
202
+ list: typeof body.list === 'string' ? body.list : undefined,
203
+ idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
204
+ ? request.headers['idempotency-key'] : undefined,
205
+ origin: { type: 'web' },
206
+ }));
207
+ reply.code(201);
208
+ return { task };
209
+ });
210
+ app.patch('/api/v1/tasks/:id/list', async (request) => {
211
+ auth.authenticate(request, true);
212
+ const taskId = request.params.id;
213
+ const list = String(request.body?.list ?? '');
214
+ return { task: await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list })) };
215
+ });
131
216
  app.get('/api/v1/configuration', async (request) => {
132
217
  auth.authenticate(request);
133
218
  if (!services.configuration)
@@ -250,7 +335,7 @@ export async function buildWebServer(services, boundary, options = {}) {
250
335
  const control = await services.session(request.params.id);
251
336
  // The idempotent, durably admitted path — used whenever the caller sends a
252
337
  // command id and the role has a conversation ledger. The legacy path stays
253
- // for old clients and tmux roles.
338
+ // for old clients.
254
339
  if (control.submitPromptV2) {
255
340
  const admittedCommandId = commandId ?? randomBytes(16).toString('hex');
256
341
  const receipt = await control.submitPromptV2({
@@ -355,7 +440,7 @@ export async function buildWebServer(services, boundary, options = {}) {
355
440
  });
356
441
  app.post('/api/v1/ws-tickets', async (request) => {
357
442
  const body = request.body;
358
- if (!body?.purpose || !['events', 'terminal', 'conversation'].includes(body.purpose))
443
+ if (!body?.purpose || !['events', 'conversation'].includes(body.purpose))
359
444
  throw new FleetError('invalid_request', 'ticket purpose is required');
360
445
  if (body.purpose === 'conversation' && !body.roleId)
361
446
  throw new FleetError('invalid_request', 'a conversation ticket must be bound to a role');
@@ -449,17 +534,6 @@ export async function buildWebServer(services, boundary, options = {}) {
449
534
  socket.on('close', () => { clearInterval(heartbeat); follow.close(); });
450
535
  });
451
536
  });
452
- app.get('/api/v1/roles/:id/terminal', { websocket: true }, (socket, request) => {
453
- requireSubprotocol(request, 'ours-fleet-terminal.v1');
454
- authorizeSocket(socket, request, async (hello) => {
455
- const ticket = String(hello.ticket ?? '');
456
- const session = auth.consumeTicket(request, ticket, 'terminal', request.params.id);
457
- auth.bindSocket(session.id, socket);
458
- if (!services.terminalUpgrade)
459
- throw new FleetError('capability_unavailable', 'terminal PTY support is unavailable');
460
- await services.terminalUpgrade(socket, request, request.params.id, ticket, hello);
461
- });
462
- });
463
537
  const staticRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'web-app');
464
538
  if (existsSync(staticRoot)) {
465
539
  await app.register(fastifyStatic, { root: staticRoot, prefix: '/' });
@@ -126,10 +126,7 @@ function loopEntry(node, merged, model) {
126
126
  roles, ...pick(node, LOOP_FIELDS),
127
127
  };
128
128
  entry.interval ??= DEFAULT_LOOP_INTERVAL;
129
- // An enabled loop hard-requires `session: acp` on every target. Adding one to a
130
- // tmux agent would make the whole fleet unloadable, so it arrives switched off
131
- // and badged instead — the owner turns it on after switching the agent to ACP.
132
- if (node.enabled === false || !roles.every(role => sessionOf(model, role) === 'acp'))
129
+ if (node.enabled === false)
133
130
  entry.enabled = false;
134
131
  return entry;
135
132
  }
@@ -142,7 +139,7 @@ function linked(merged, id, kind) {
142
139
  function sessionOf(model, role) {
143
140
  const roles = model.roles;
144
141
  const defaults = model.defaults;
145
- return String(roles?.[role]?.session ?? defaults?.session ?? 'tmux');
142
+ return String(roles?.[role]?.session ?? defaults?.session ?? 'acp');
146
143
  }
147
144
  function section(model, key) {
148
145
  const existing = model[key];
@@ -0,0 +1 @@
1
+ :root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#e9eef0;background:#080b0c;font-synthesis:none;--panel: #111617;--line: #222a2c;--muted: #849093;--mint: #7ce7b0;--amber: #f0c66c;--red: #ff827c}*{box-sizing:border-box}body{margin:0;min-width:0;min-height:100vh;background:radial-gradient(circle at 70% -20%,#183027 0,transparent 35%),#080b0c}button,input,select,textarea{font:inherit}button{color:inherit}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--mint);outline-offset:2px}.auth-screen{display:grid;place-content:center;text-align:center;min-height:100vh;color:var(--muted)}.auth-screen h1{color:#fff;margin-bottom:0}.brand-wordmark{color:var(--mint);font-size:21px;font-weight:820;letter-spacing:-.02em}.auth-screen .brand-wordmark{justify-self:center;font-size:32px}.shell{display:grid;grid-template-columns:224px 1fr;min-height:100vh}aside{position:fixed;inset:0 auto 0 0;width:224px;display:flex;flex-direction:column;padding:24px 14px;border-right:1px solid #1d2425;background:#080b0ceb;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.brand{display:flex;gap:12px;align-items:center;padding:0 8px 30px}.brand div{display:flex;flex-direction:column}.brand small{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:1.4px}nav{display:grid;gap:5px}nav button{display:grid;grid-template-columns:24px 1fr auto;gap:8px;align-items:center;width:100%;border:0;border-radius:8px;padding:10px 12px;text-align:left;color:#98a3a5;background:transparent;text-transform:capitalize;cursor:pointer}nav button:hover,nav button.active{color:#fff;background:#151b1c}nav button.active{box-shadow:inset 2px 0 var(--mint)}nav b{background:#422f22;color:var(--amber);border-radius:10px;font-size:11px;padding:2px 7px}.sidebar-foot{margin-top:auto;display:grid;grid-template-columns:12px 1fr;align-items:center;padding:12px;color:var(--muted);font-size:12px;text-transform:capitalize}.sidebar-foot i{width:7px;height:7px;border-radius:50%;background:var(--amber)}.sidebar-foot i.live{background:var(--mint);box-shadow:0 0 9px var(--mint)}.sidebar-foot small{grid-column:2;opacity:.6;margin-top:3px}.main{grid-column:2;min-width:0}header{display:flex;justify-content:space-between;align-items:flex-end;height:124px;padding:25px 36px 23px;border-bottom:1px solid var(--line);background:#080b0c8c}h1,h2,h3,p{margin-top:0}header h1{margin:4px 0 0;font-size:29px;letter-spacing:-.8px}.eyebrow{color:var(--mint);text-transform:uppercase;font-size:10px;letter-spacing:1.8px;font-weight:700}.header-actions,.actions{display:flex;gap:9px}button.primary,button.secondary,.actions button{border-radius:7px;padding:9px 13px;cursor:pointer}button.primary{border:1px solid #83e9b5;color:#07130d;background:var(--mint);font-weight:700}button.primary:disabled{opacity:.4;cursor:default}button.secondary,.actions button{border:1px solid #30393b;background:#141a1b}button.secondary:hover,.actions button:hover{border-color:#526064}.content,.workspace{padding:28px 36px 60px}.toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;color:var(--muted);font-size:12px}.toolbar input{width:340px;padding:10px 13px;color:#fff;background:#101516;border:1px solid var(--line);border-radius:8px}.toolbar>div{display:flex;align-items:center;gap:10px}.toolbar button{padding:7px 10px}.role-row.inactive{opacity:.72}.role-row.active{background:#141a1b;border-left:2px solid var(--mint)}.watchdog-detail-layout{display:grid;gap:18px}.watchdog-report-head{display:flex;align-items:center;gap:12px}.watchdog-report-head h2{flex:1}.watchdog-roles{display:grid;gap:10px;margin-top:12px}.watchdog-role{border:1px solid var(--line);border-radius:8px;padding:12px 14px;background:#0d1213}.watchdog-role-head{display:flex;align-items:center;gap:10px}.watchdog-role-head strong{text-transform:capitalize}.watchdog-role-detail,.watchdog-role-note{margin:8px 0 0;font-size:12px;color:var(--muted)}.watchdog-role-note{color:var(--amber)}.watchdog-role details{margin-top:8px;font-size:12px}.watchdog-role summary{cursor:pointer;color:var(--muted)}.watchdog-role details ul{margin:8px 0 0;padding-left:18px;color:#c9d1d3}.watchdog-role details li{margin-bottom:4px}.status-guide{display:flex;flex-wrap:wrap;gap:7px;align-items:center;margin-bottom:15px;color:var(--muted);font-size:11px}.status-guide>span,.status-chip{display:inline-flex;align-items:center;gap:5px;border:1px solid #30393b;border-radius:12px;padding:3px 7px;white-space:nowrap}.status-guide b{color:#fff;font-size:11px}.status-guide small{flex-basis:100%;color:var(--muted)}.status-guide .ready,.status-chip.ready{border-color:#315e4a}.status-guide .busy,.status-chip.busy{border-color:#315478}.status-guide .attention,.status-chip.attention,.status-chip.unknown{border-color:#66552f}.status-guide .offline,.status-chip.offline{border-color:#693b38}.role-table{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:#0e1213cc}.role-row{display:grid;grid-template-columns:minmax(260px,1.55fr) repeat(4,minmax(125px,1fr));width:100%;align-items:center;gap:15px;padding:14px 18px;border:0;border-bottom:1px solid var(--line);background:transparent;text-align:left;cursor:pointer}button.role-row:hover{background:#141a1b}.role-row.heading{padding-block:10px;color:#6f7b7e;font-size:10px;letter-spacing:1px;text-transform:uppercase;cursor:default}.role-row:last-child{border-bottom:0}.role-row>span{min-width:0;display:flex;flex-direction:column;gap:3px}.role-row strong{font-size:13px;text-transform:capitalize;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-row small{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-name{position:relative;display:grid!important;grid-template-columns:auto 1fr auto;align-items:center}.role-name .mission-summary{display:-webkit-box;max-height:2.5em;line-height:1.25;overflow:hidden;overflow-wrap:anywhere;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.status-chip{color:#c9d1d3;font-size:10px;padding:3px 6px}.status-chip i{width:6px;height:6px;border-radius:50%;background:#667174}.status-chip.ready i{background:var(--mint)}.status-chip.busy i{background:#71b7ff}.status-chip.attention i,.status-chip.unknown i{background:var(--amber)}.status-chip.offline i{background:var(--red)}.state.ready,.hero-state.ready i{background:var(--mint);box-shadow:0 0 7px #69dba0}.state.busy,.hero-state.busy i{background:#71b7ff}.state.attention,.state.unknown,.hero-state.attention i,.hero-state.unknown i{background:var(--amber)}.state.offline,.hero-state.offline i{background:var(--red)}.role-name em{color:#7f8a8d;font-size:10px;font-style:normal;border:1px solid #2e3739;border-radius:10px;padding:3px 6px}.role-pills{display:flex;flex-direction:column;align-items:flex-end;gap:4px}.empty{padding:60px;text-align:center;color:var(--muted)}.banner{margin:16px 36px 0;border:1px solid;border-radius:8px;padding:10px 13px;font-size:13px}.banner.error,.error{border-color:#5c302f;background:#271616;color:#ffaaa5}.banner.warning,.warning{border-color:#5a4726;background:#241e12;color:#f1ce83}.banner.info{border-color:#28463b;background:#10231c;color:#9be4bf}.workspace>.banner{margin-inline:0}.back{border:0;background:none;color:var(--muted);padding:0 0 18px;cursor:pointer}.workspace-summary{display:flex;flex-wrap:wrap;align-items:center;gap:7px;color:var(--muted);font-size:12px}.workspace-summary>span{border:1px solid var(--line);border-radius:12px;padding:4px 8px}.workspace-summary>small{margin-left:auto}.hero-state{display:flex;gap:7px;align-items:center;color:#fff;text-transform:capitalize}.hero-state i{width:7px;height:7px;border-radius:50%;background:#788}.tabs{display:flex;gap:22px;margin:26px 0 22px;border-bottom:1px solid var(--line)}.tabs button{border:0;border-bottom:2px solid transparent;padding:10px 2px;background:none;color:var(--muted);text-transform:capitalize;cursor:pointer}.tabs button.active{color:#fff;border-bottom-color:var(--mint)}.tabs button:disabled{opacity:.35;cursor:not-allowed}.dashboard-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.panel{border:1px solid var(--line);border-radius:10px;background:#101516db;padding:18px}.panel h2{font-size:15px;letter-spacing:-.2px}.panel.wide{grid-column:1 / -1}.evidence>div:first-child{display:flex;justify-content:space-between}.evidence>div:first-child strong{color:var(--mint);text-transform:capitalize;font-size:12px}dl{margin:0}dl div{display:grid;grid-template-columns:120px 1fr;gap:15px;padding:7px 0;border-top:1px solid #1d2426;font-size:12px}dt{color:var(--muted)}dd{margin:0;word-break:break-word}.danger{color:#ff9e99!important;border-color:#613432!important}.activity-layout{display:grid;grid-template-columns:1fr 290px;gap:14px;align-items:start}.activity{display:grid;gap:9px}.activity-filter{display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--muted);font-size:11px}.activity-filter button{padding:6px 9px}.event{border-left:2px solid #334044;padding:10px 13px;background:#0d1213}.event.agent_text{border-color:var(--mint)}.event.error,.event.permission{border-color:var(--amber)}.event p{margin:5px 0 0;white-space:pre-wrap}.event small,.muted{color:var(--muted)}.composer{position:sticky;top:16px;display:grid;gap:9px}textarea{min-height:84px;resize:vertical}.composer textarea,fieldset input,fieldset select,fieldset textarea{width:100%;color:#fff;border:1px solid #303a3c;border-radius:6px;background:#0d1112;padding:9px 10px}.composer small{color:var(--muted);line-height:1.45}pre{max-width:100%;overflow:auto;white-space:pre-wrap;word-break:break-word;font:12px/1.55 SFMono-Regular,Consolas,monospace;color:#cbd5d7}.log-panel>pre{margin:0;padding:5px 8px;border-bottom:1px solid #1b2224}.audit-list div{display:grid;grid-template-columns:180px 1fr 120px;padding:10px 0;border-top:1px solid var(--line);font-size:12px}.audit-list time{color:var(--muted)}.modal-backdrop{position:fixed;z-index:10;inset:0;display:grid;place-items:center;padding:30px;background:#000000b8;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.modal{width:min(900px,95vw);max-height:92vh;overflow:auto;border:1px solid #354043;border-radius:13px;background:#0d1112;box-shadow:0 30px 100px #000}.modal-head{position:sticky;z-index:2;top:0;display:flex;justify-content:space-between;align-items:flex-start;padding:22px 26px 18px;border-bottom:1px solid var(--line);background:#0d1112f5}.modal-head h2{margin:4px 0 0}.icon{border:0;background:none;color:var(--muted);font-size:25px;cursor:pointer}.wizard,.progress{padding:22px 26px}fieldset{border:0;border-bottom:1px solid var(--line);padding:0 0 20px;margin:0 0 20px}legend{margin-bottom:13px;color:var(--mint);text-transform:uppercase;font-size:10px;letter-spacing:1.5px;font-weight:700}.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:13px}.form-grid.three{grid-template-columns:repeat(3,1fr)}.wake-sources{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px 14px}.wake-sources>small{grid-column:1 / -1;color:var(--muted)}.wake-sources label{display:flex;flex-direction:row;align-items:center;gap:7px;color:#c7d0d1;font-size:11px;text-transform:none;letter-spacing:0}.form-grid label{display:grid;gap:6px;color:#b7c0c2;font-size:12px}.form-grid label small{color:var(--muted)}.form-grid .wide{grid-column:1 / -1}.risk{display:flex;align-items:center;gap:8px;margin-top:14px;color:var(--amber);font-size:12px}.review{border:1px solid #2f493f;border-radius:8px;background:#101b17;padding:15px}.review dl{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}.modal-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:20px}.stage{display:flex;gap:12px;min-height:54px;position:relative}.stage:after{position:absolute;left:5px;top:14px;bottom:0;width:1px;background:#34413d;content:""}.stage:last-child:after{display:none}.stage i{z-index:1;width:11px;height:11px;margin-top:2px;border-radius:50%;background:var(--mint);box-shadow:0 0 0 4px #173026}.stage span{display:flex;flex-direction:column;text-transform:capitalize}.stage small{color:var(--muted);margin-top:3px}@media(max-width:1050px){.role-row{grid-template-columns:1.5fr repeat(3,1fr)}.role-row>span:nth-child(4){display:none}}.topology{overflow-x:auto;margin-bottom:16px;border:1px solid var(--line);border-radius:12px;background:radial-gradient(circle at 50% 50%,#13201b 0,#0b1011 58%)}.topology-canvas{position:relative;width:1000px;min-width:100%}.topology svg{position:absolute;inset:0;width:100%;height:100%}.edge line{stroke:#405052;stroke-width:1.5;vector-effect:non-scaling-stroke}.edge.spawned line{stroke:var(--mint);stroke-dasharray:5 4}.edge.watches line{stroke:#d7ad58;stroke-dasharray:2 3}.edge.targets line{stroke:#729ed6;stroke-dasharray:9 3 2 3}.topology-legend{display:grid;gap:8px;margin:0 0 12px;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#0c1112}.topology-legend ul{display:grid;grid-template-columns:repeat(4,minmax(140px,1fr));gap:8px;margin:0;padding:0;list-style:none}.topology-legend li{display:flex;gap:9px;align-items:start;padding:7px;border-radius:7px}.topology-legend li:focus{outline:2px solid var(--mint);outline-offset:1px}.topology-legend li>span:last-child{display:grid;gap:2px}.topology-legend small{color:var(--muted);line-height:1.3}.legend-line{width:34px;margin-top:7px;border-top:2px solid #405052;flex:0 0 auto}.legend-line.spawned{border-color:var(--mint);border-top-style:dashed}.legend-line.watches{border-color:#d7ad58;border-top-style:dotted}.legend-line.targets{height:5px;border-color:#729ed6;border-bottom:1px dotted #729ed6}.topology-node{position:absolute;width:150px;min-height:62px;display:grid;gap:3px;z-index:1;padding:0;text-align:left;border:1px solid #344044;border-radius:10px;background:#111718;box-shadow:0 8px 24px #00000047;transform:translateY(-50%)}.topology-open{display:grid;gap:3px;min-width:0;padding:10px 28px 10px 12px;color:inherit;text-align:left;border:0;background:transparent;cursor:pointer}.topology-remove{position:absolute;top:5px;right:5px;width:22px;height:22px;padding:0;color:var(--muted);border:0;background:transparent}.topology-remove:hover,.topology-remove:focus{color:#ff8e85}.topology-node:hover{border-color:var(--mint);transform:translateY(-50%) translateY(-2px)}.topology-open:disabled{cursor:default;opacity:1}.topology-node small{color:var(--muted);text-transform:uppercase;font-size:9px;letter-spacing:1px}.topology-node strong{overflow:hidden;text-overflow:ellipsis}.topology-node span{color:var(--muted);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10px}.topology-node.watchdog{border-color:#6a5830}.topology-node.loop{border-color:#385778}.topology-node.temporary{border-style:dashed}.topology-node.attention,.topology-node.unknown{box-shadow:inset 3px 0 var(--amber)}.topology-node.offline{opacity:.65}.lineage-note{color:var(--muted);font-size:11px}.topology-editor{display:grid;gap:12px}.topology-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.topology-toolbar .spacer{flex:1 1 auto}.draft-count{color:var(--amber);font-size:11px}.topology-empty{display:grid;gap:10px;justify-items:start;padding:26px;border:1px dashed var(--line);border-radius:12px;background:#0c1112}.topology-empty h3{margin:0}.topology-empty p{margin:0;color:var(--muted)}.topology-tutorial{padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#0c1112;font-size:12px}.topology-tutorial ol{display:grid;gap:6px;margin:0 0 8px;padding-left:18px;color:var(--muted)}.topology-tutorial li.current{color:#d5dddf}.banner{margin:0;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:#0c1112;color:var(--muted);font-size:12px}.banner.warning{border-color:#6a5830;color:var(--amber)}.node-badge{display:inline-block;padding:1px 6px;border-radius:999px;font-size:9px;letter-spacing:.3px}.node-badge.draft-incomplete{background:#2a2413;color:var(--amber)}.node-badge.draft-ready{background:#12261f;color:var(--mint)}.node-badge.configured{background:#16202a;color:#9fc0e0}.node-badge.running{background:#12261f;color:var(--mint)}.node-detail{display:block}.topology-node.draft-incomplete,.topology-node.draft-ready{border-style:dashed}.topology-node.selected{border-color:var(--mint);box-shadow:0 0 0 2px #58c7a059}.topology-node[data-node-id]{touch-action:none}.edge.implicit line{stroke-dasharray:1 5;opacity:.75}.edge.dangling line{stroke:var(--red);stroke-dasharray:3 3}.edge.drafted line{opacity:.7}.topology-list{width:100%;border-collapse:collapse;color:#d5dddf;text-align:left;font-size:12px}.topology-list th,.topology-list td{padding:8px;border-top:1px solid var(--line)}.topology-list tr.selected{background:#101718}.node-inspector{display:grid;gap:10px;padding:14px;border:1px solid var(--line);border-radius:12px;background:#0c1112}.node-inspector header{display:flex;align-items:center;justify-content:space-between}.node-inspector h3,.node-inspector h4{margin:0}.node-inspector label{display:grid;gap:4px;font-size:12px}.node-inspector small{color:var(--muted);font-size:10px}.missing-list{display:grid;gap:4px;margin:0;padding-left:18px;color:var(--amber);font-size:12px}.connection-list{display:grid;gap:4px;margin:0;padding-left:18px;color:var(--muted);font-size:11px}.inspector-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.review-panel{display:grid;gap:10px;padding:14px;border:1px solid var(--mint);border-radius:12px;background:#0c1112}.review-panel h3{margin:0}.review-panel .row{display:flex;gap:8px}.topology-fallback{margin:0 0 18px;color:var(--muted);font-size:12px}.topology-fallback summary{cursor:pointer}.topology-fallback table{width:100%;border-collapse:collapse;margin-top:10px;color:#d5dddf;text-align:left}.topology-fallback th,.topology-fallback td{padding:8px;border-top:1px solid var(--line)}.text-button{border:0;padding:0;color:var(--mint);background:transparent;cursor:pointer;text-align:left}.setup-entry{max-width:1180px;margin-inline:auto}.setup-hero{display:grid;grid-template-columns:1fr 300px;gap:30px;align-items:center;padding:12px 0 26px}.setup-hero h2{margin:6px 0 8px;font-size:28px}.setup-hero p{max-width:680px;color:var(--muted)}.setup-safety{display:grid;gap:5px;padding:15px;border:1px solid #315e4a;border-radius:10px;background:#102019}.setup-safety span{color:var(--muted);font-size:12px;line-height:1.45}.setup-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin:0 0 18px;padding:0;list-style:none}.setup-steps li{display:flex;gap:8px;align-items:center;color:#647174;border-bottom:2px solid #263033;padding:10px;font-size:12px}.setup-steps li.active{color:#fff;border-color:var(--mint)}.setup-steps li.done{color:var(--mint);border-color:#315e4a}.setup-panel{min-height:360px}.setup-panel h3{margin-bottom:6px}.mode-switch{display:flex;gap:8px;align-items:center;margin:0 0 14px;padding:8px;border:1px solid var(--line);border-radius:10px}.mode-switch button.active{color:#07110d;background:var(--mint);border-color:var(--mint)}.mode-switch span{margin-left:6px;color:var(--muted);font-size:12px}.decision-note{display:flex;gap:10px;margin:12px 0;padding:11px;border-left:3px solid var(--mint);background:#102019;color:var(--muted)}.decision-note strong{color:#dce6e2;white-space:nowrap}.setup-actions{display:grid;grid-template-columns:auto 1fr auto;gap:10px;margin-top:22px;padding-top:18px;border-top:1px solid var(--line)}.section-title{display:flex;justify-content:space-between;align-items:start;gap:20px}.entity-list{display:grid;gap:12px}.entity-card{position:relative;padding:15px;border:1px solid var(--line);border-radius:9px;background:#0c1112}.entity-card>.danger{position:absolute;right:14px;bottom:12px}.empty.compact{padding:34px}.preflight{display:grid;gap:4px;margin:12px 0 18px;padding:13px;border:1px solid #315e4a;border-radius:8px;background:#102019}.preflight.failed{border-color:#66552f;background:#211b10}.preflight span{color:var(--muted);font-size:12px}.setup-panel details{margin-top:14px}.setup-panel summary{cursor:pointer;color:#cbd4d6}.config-diff{max-height:360px;padding:14px;border-radius:8px;background:#080b0c}@media(max-width:760px){.topology-legend ul{grid-template-columns:1fr}.mode-switch{align-items:stretch;flex-wrap:wrap}.mode-switch span{flex-basis:100%;margin-left:0}}.check-list{display:grid;gap:7px;padding-left:20px;font-size:12px}.check-list .ok::marker{color:var(--mint)}.check-list .failed::marker{color:var(--red)}@media(max-width:760px){.shell{display:block;padding-bottom:68px}aside{z-index:9;inset:auto 0 0;width:auto;height:64px;padding:7px;border:1px solid var(--line);background:#080b0cf7}aside .brand,.sidebar-foot{display:none}aside nav{grid-template-columns:repeat(5,1fr);gap:2px}aside nav button{display:flex;min-width:0;flex-direction:column;gap:2px;justify-content:center;padding:6px 2px;text-align:center;font-size:9px}aside nav button span{font-size:15px}aside nav b{display:none}.main{width:100%}header{height:auto;min-height:104px;padding:18px;align-items:start;gap:12px}header h1{font-size:23px}.header-actions{flex-wrap:wrap;justify-content:flex-end}.header-actions button{padding:7px 9px;font-size:11px}.content,.workspace{padding:18px 14px 36px}.banner{margin-inline:14px}.toolbar{align-items:stretch;gap:10px;flex-direction:column}.toolbar input{width:100%}.role-table{overflow-x:auto}.role-row{min-width:720px}.setup-hero{grid-template-columns:1fr}.setup-steps li{flex-direction:column;font-size:10px}.form-grid,.form-grid.three{grid-template-columns:1fr}.form-grid .wide{grid-column:auto}.section-title{align-items:stretch;flex-direction:column}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}.conversation-layout{display:grid;grid-template-rows:1fr auto;gap:14px;min-height:60vh}.conversation{display:flex;flex-direction:column;min-height:0;position:relative}.conversation-header{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;padding-bottom:10px;border-bottom:1px solid var(--line);font-size:12px}.stream-state{text-transform:uppercase;font-size:10px;letter-spacing:1.4px;font-weight:700;color:var(--amber)}.stream-state.live{color:var(--mint)}.stream-state.offline{color:var(--red)}.conversation-header .warning{color:var(--amber)}.transcript{flex:1;overflow-y:auto;display:grid;gap:14px;padding:16px 4px;max-height:62vh}.turn{display:grid;gap:8px}.bubble{border:1px solid var(--line);border-radius:10px;padding:10px 14px;background:#0d1213}.bubble small{color:var(--muted);display:block;margin-bottom:6px}.bubble.user{border-left:2px solid var(--mint);margin-left:8%;background:#10181a}.bubble.assistant{margin-right:8%}.prompt-text,.assistant-text,.thought-text{margin:0;white-space:pre-wrap;word-break:break-word;font-family:inherit;font-size:14px;line-height:1.5}.assistant-text+.assistant-text{margin-top:10px;padding-top:10px;border-top:1px dashed var(--line)}.thought-text{color:var(--muted);font-size:13px;margin-top:8px}.thoughts{margin-bottom:8px}.streaming-hint{animation:pulse 1.4s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.streaming-hint{animation:none}}@keyframes pulse{50%{opacity:.45}}.tool-card{border:1px solid var(--line);border-radius:8px;padding:7px 11px;margin:6px 0;font-size:13px;background:#101516}.tool-card summary{display:flex;gap:10px;align-items:baseline;cursor:pointer}.tool-detail{display:grid;gap:9px;padding-top:10px}.tool-detail code{display:block;overflow-wrap:anywhere;color:var(--muted);margin-top:4px}.tool-content{border-top:1px dashed var(--line);padding-top:8px;min-width:0}.tool-content pre{max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:5px 0 0;color:#d9e4e2}.diff-old{color:#d99b96!important}.diff-new{color:#9ed8c4!important}.plan-card{border:1px solid var(--line);border-radius:8px;padding:9px 12px;margin:6px 0;font-size:13px}.plan-card ol{list-style:none;margin:7px 0 0;padding:0;display:grid;gap:5px}.plan-entry{display:grid;grid-template-columns:18px 1fr auto;gap:7px;color:var(--muted)}.plan-entry.in_progress{color:var(--amber)}.plan-entry.completed{color:var(--mint)}.usage-badge{margin-top:10px;color:var(--muted)}.tool-status{text-transform:uppercase;font-size:10px;letter-spacing:1px;font-weight:700;color:var(--muted)}.tool-status.in_progress{color:var(--amber)}.tool-status.completed{color:var(--mint)}.tool-status.failed{color:var(--red)}.permission-card{border:1px solid var(--amber);border-radius:8px;padding:10px 12px;margin:8px 0;font-size:13px;display:grid;gap:8px}.permission-card.resolved{border-color:var(--line);color:var(--muted)}.permission-actions{display:flex;gap:8px;flex-wrap:wrap}.jump-latest{position:absolute;right:20px;bottom:16px}.conversation-layout .composer{display:grid;gap:8px}.conversation-layout .composer textarea{min-height:68px;max-height:200px;resize:vertical;color:#fff;background:#101516;border:1px solid var(--line);border-radius:8px;padding:10px 13px}.composer-actions{display:flex;gap:9px;justify-content:space-between}.composer-actions .danger{margin-left:auto}button.danger{border:1px solid #5b322f;color:var(--red);background:#1a1213;border-radius:7px;padding:9px 13px;cursor:pointer}button.danger:disabled{opacity:.4;cursor:default}