@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,10 +1,11 @@
1
1
  import { type FleetConfig } from '../config.js';
2
2
  import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
3
3
  import { provisionMembers } from '../rooms-tasks/provision.js';
4
- import type { RoomOrchestrationRecord, TaskOrigin, TaskOutcome, TaskRecord, TaskState } from '../rooms-tasks/types.js';
4
+ import { moveTaskToList } from '../rooms-tasks/task-state.js';
5
+ import type { RoomOrchestrationRecord, TaskListRecord, TaskOrigin, TaskOutcome, TaskRecord, TaskState } from '../rooms-tasks/types.js';
5
6
  export type TaskRoomActor = {
6
7
  kind: 'local_control';
7
- surface: 'cli';
8
+ surface: 'cli' | 'web';
8
9
  } | {
9
10
  kind: 'authenticated_owner';
10
11
  surface: 'messenger';
@@ -55,6 +56,7 @@ export interface CreateTaskRequest {
55
56
  noRoom?: boolean;
56
57
  idempotencyKey?: string;
57
58
  origin: TaskOrigin;
59
+ list?: string;
58
60
  }
59
61
  export interface CreateRoomRequest {
60
62
  actor: TaskRoomActor;
@@ -69,6 +71,7 @@ export interface TaskRoomServiceDeps {
69
71
  cowork?(config: FleetConfig): CoworkAdapter;
70
72
  binPath?(): string;
71
73
  provisionMembers?: typeof provisionMembers;
74
+ moveTaskToList?: typeof moveTaskToList;
72
75
  }
73
76
  /** Exact extraction of the previously CLI-owned task create/start behavior. */
74
77
  export declare class TaskRoomApplicationService {
@@ -84,7 +87,39 @@ export declare class TaskRoomApplicationService {
84
87
  }): Promise<TaskRecord>;
85
88
  listTasks(filter?: {
86
89
  state?: TaskState | TaskState[];
90
+ list?: string;
87
91
  }): TaskRecord[];
92
+ listTaskLists(): TaskListRecord[];
93
+ groupedTasks(filter?: {
94
+ state?: TaskState | TaskState[];
95
+ list?: string;
96
+ }): Array<{
97
+ list: TaskListRecord;
98
+ tasks: TaskRecord[];
99
+ }>;
100
+ createTaskList(input: {
101
+ actor: TaskRoomActor;
102
+ name: string;
103
+ }): Promise<TaskListRecord>;
104
+ renameTaskList(input: {
105
+ actor: TaskRoomActor;
106
+ name: string;
107
+ newName: string;
108
+ }): Promise<TaskListRecord>;
109
+ moveTask(input: {
110
+ actor: TaskRoomActor;
111
+ taskId: string;
112
+ list: string;
113
+ }): Promise<TaskRecord>;
114
+ deleteTaskList(input: {
115
+ actor: TaskRoomActor;
116
+ name: string;
117
+ destination?: string;
118
+ }): Promise<{
119
+ deleted: TaskListRecord;
120
+ moved: number;
121
+ destination?: TaskListRecord;
122
+ }>;
88
123
  getTask(taskId: string): {
89
124
  task: TaskRecord;
90
125
  orchestration: RoomOrchestrationRecord | undefined;
@@ -3,7 +3,8 @@ import { ConfigError, loadConfig } from '../config.js';
3
3
  import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
4
4
  import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
5
5
  import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
6
- import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
6
+ import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
7
+ import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
7
8
  import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
8
9
  import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
9
10
  import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
@@ -34,11 +35,13 @@ export class TaskRoomApplicationService {
34
35
  name: template.name, version: template.version, content_hash: template.content_hash,
35
36
  };
36
37
  const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
37
- let task = persistTask({
38
- title: request.title, brief, brief_file: request.briefFile, template: ref,
39
- origin: request.origin, idempotency_key: request.idempotencyKey,
40
- start: !request.backlog,
41
- no_room: request.noRoom,
38
+ let task = await withTaskListsLock(() => {
39
+ const list = resolveTaskList(request.list ?? 'default');
40
+ return persistTask({
41
+ title: request.title, brief, brief_file: request.briefFile, template: ref,
42
+ origin: request.origin, idempotency_key: request.idempotencyKey,
43
+ start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
44
+ });
42
45
  });
43
46
  if (!request.backlog && !request.noRoom && ref && !task.room_id) {
44
47
  try {
@@ -82,7 +85,46 @@ export class TaskRoomApplicationService {
82
85
  return task;
83
86
  }
84
87
  listTasks(filter) {
85
- return readTasks(filter);
88
+ const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
89
+ return readTasks({ state: filter?.state, listId });
90
+ }
91
+ listTaskLists() { return readTaskLists(); }
92
+ groupedTasks(filter) {
93
+ const tasks = this.listTasks(filter);
94
+ const lists = filter?.list ? [resolveTaskList(filter.list)] : readTaskLists();
95
+ return lists.map(list => ({ list, tasks: tasks.filter(task => task.list_id === list.list_id) }));
96
+ }
97
+ async createTaskList(input) {
98
+ return withTaskListsLock(() => createTaskListLocked(input.name));
99
+ }
100
+ async renameTaskList(input) {
101
+ return withTaskListsLock(() => renameTaskListLocked(input.name, input.newName));
102
+ }
103
+ async moveTask(input) {
104
+ return withTaskListsLock(() => {
105
+ const destination = resolveTaskList(input.list);
106
+ return (this.deps.moveTaskToList ?? moveTaskToList)(input.taskId, destination.list_id);
107
+ });
108
+ }
109
+ async deleteTaskList(input) {
110
+ return withTaskListsLock(() => {
111
+ const source = resolveTaskList(input.name);
112
+ if (source.list_id === DEFAULT_TASK_LIST_ID)
113
+ throw new TaskListError('default_immutable', "the 'default' list cannot be deleted");
114
+ const assigned = readTasks({ listId: source.list_id });
115
+ let destination;
116
+ if (assigned.length) {
117
+ if (!input.destination)
118
+ throw new TaskListError('destination_required', `task list '${source.name}' is not empty; destination is required`);
119
+ destination = resolveTaskList(input.destination);
120
+ if (destination.list_id === source.list_id)
121
+ throw new TaskListError('same_destination', 'deletion destination must differ from the source list');
122
+ for (const task of assigned)
123
+ (this.deps.moveTaskToList ?? moveTaskToList)(task.task_id, destination.list_id);
124
+ }
125
+ deleteTaskListRecordLocked(source.list_id);
126
+ return { deleted: source, moved: assigned.length, destination };
127
+ });
86
128
  }
87
129
  getTask(taskId) {
88
130
  const task = readTask(taskId);
@@ -117,7 +117,6 @@ export interface RoleCapabilities {
117
117
  };
118
118
  input: {
119
119
  text: boolean;
120
- rawKeys: boolean;
121
120
  interrupt: boolean;
122
121
  steering: boolean;
123
122
  };
@@ -125,12 +124,6 @@ export interface RoleCapabilities {
125
124
  observe: boolean;
126
125
  respond: boolean;
127
126
  };
128
- terminal: {
129
- available: boolean;
130
- reason?: 'wrong_backend' | 'pty_unavailable' | 'offline' | 'incompatible_runner';
131
- multiViewer: boolean;
132
- writerLease: boolean;
133
- };
134
127
  lifecycle: {
135
128
  start: boolean;
136
129
  stop: boolean;
@@ -10,6 +10,8 @@
10
10
  export interface LockDeps {
11
11
  now?(): number;
12
12
  sleep?(ms: number): Promise<void>;
13
+ /** Test hook after a complete private claim is prepared, before atomic publication. */
14
+ beforePublish?(): void | Promise<void>;
13
15
  }
14
16
  /** The syscalls a replace depends on, injectable so faults can be forced deterministically. */
15
17
  export interface WriteDeps {
@@ -18,9 +20,9 @@ export interface WriteDeps {
18
20
  closeSync?(fd: number): void;
19
21
  }
20
22
  /**
21
- * Run `fn` holding a cross-process lock. `mkdir` is atomic on every platform we
22
- * support, so the directory's existence IS the lock; the timestamp inside lets a
23
- * crashed holder's lock be broken rather than deadlocking the fleet forever.
23
+ * Run `fn` holding a cross-process lock. A fully populated private claim directory
24
+ * is atomically renamed to the canonical path, so contenders never observe partial
25
+ * ownership metadata; its timestamp lets a crashed holder be reclaimed safely.
24
26
  * Same strategy as the runner's launch gate, factored out so both use one.
25
27
  *
26
28
  * The lock is always released, including when `fn` throws.
@@ -4,9 +4,9 @@ import { randomUUID } from 'node:crypto';
4
4
  const DEFAULT_STALE_MS = 10_000;
5
5
  const POLL_MS = 25;
6
6
  /**
7
- * Run `fn` holding a cross-process lock. `mkdir` is atomic on every platform we
8
- * support, so the directory's existence IS the lock; the timestamp inside lets a
9
- * crashed holder's lock be broken rather than deadlocking the fleet forever.
7
+ * Run `fn` holding a cross-process lock. A fully populated private claim directory
8
+ * is atomically renamed to the canonical path, so contenders never observe partial
9
+ * ownership metadata; its timestamp lets a crashed holder be reclaimed safely.
10
10
  * Same strategy as the runner's launch gate, factored out so both use one.
11
11
  *
12
12
  * The lock is always released, including when `fn` throws.
@@ -15,15 +15,22 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
15
15
  const now = deps.now ?? (() => Date.now());
16
16
  const sleep = deps.sleep ?? ((ms) => new Promise(r => { setTimeout(r, ms); }));
17
17
  const stampPath = join(lockPath, 'ts');
18
+ const ownerPath = join(lockPath, 'owner.json');
19
+ const token = randomUUID();
18
20
  mkdirSync(dirname(lockPath), { recursive: true });
19
21
  for (let waited = 0;;) {
22
+ const claimPath = `${lockPath}.claim.${process.pid}.${randomUUID()}`;
20
23
  try {
21
- mkdirSync(lockPath);
22
- writeFileSync(stampPath, String(now()));
24
+ mkdirSync(claimPath);
25
+ writeFileSync(join(claimPath, 'ts'), String(now()));
26
+ writeFileSync(join(claimPath, 'owner.json'), JSON.stringify({ token, pid: process.pid }));
27
+ await deps.beforePublish?.();
28
+ renameSync(claimPath, lockPath);
23
29
  break;
24
30
  }
25
31
  catch (e) {
26
- if (e.code !== 'EEXIST')
32
+ rmSync(claimPath, { recursive: true, force: true });
33
+ if (!['EEXIST', 'ENOTEMPTY'].includes(e.code ?? ''))
27
34
  throw e;
28
35
  let held = null;
29
36
  try {
@@ -31,7 +38,19 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
31
38
  held = Number.isFinite(n) ? n : null;
32
39
  }
33
40
  catch { /* holder died between mkdir and stamp */ }
34
- if ((held !== null && now() - held > staleMs) || waited > staleMs * 2) {
41
+ let ownerAlive = false;
42
+ try {
43
+ const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
44
+ if (typeof owner.pid === 'number') {
45
+ try {
46
+ process.kill(owner.pid, 0);
47
+ ownerAlive = true;
48
+ }
49
+ catch { /* dead owner */ }
50
+ }
51
+ }
52
+ catch { /* legacy or incomplete lock */ }
53
+ if (!ownerAlive && ((held !== null && now() - held > staleMs) || waited > staleMs * 2)) {
35
54
  rmSync(lockPath, { recursive: true, force: true }); // break a dead holder's lock
36
55
  continue;
37
56
  }
@@ -43,7 +62,12 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
43
62
  return await fn();
44
63
  }
45
64
  finally {
46
- rmSync(lockPath, { recursive: true, force: true });
65
+ try {
66
+ const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
67
+ if (owner.token === token)
68
+ rmSync(lockPath, { recursive: true, force: true });
69
+ }
70
+ catch { /* never remove a lock whose ownership cannot be proved */ }
47
71
  }
48
72
  }
49
73
  /**
package/dist/briefing.js CHANGED
@@ -190,9 +190,6 @@ export function generateBriefing(role, v, opts) {
190
190
  L.push(`7. Await messages. When the monitor wakes you (or the owner requests a manual check),`);
191
191
  L.push(` call **${v.getMessagesTool}**, act on them,`);
192
192
  L.push(` and reply with ${v.sendTool}. No coordinator is configured — the owner drives you`);
193
- // NOT `tmux attach -t <name>`: each role's pane lives on its own tmux
194
- // socket (#32), so a bare attach finds no server. `ours-fleet attach`
195
- // addresses the right one.
196
193
  L.push(` via \`ours-fleet attach ${role.name}\` or by messaging "${id}".`);
197
194
  }
198
195
  if (role.oversee?.length) {
@@ -221,7 +218,7 @@ export function generateBriefing(role, v, opts) {
221
218
  L.push('idle or stalled from `readiness=idle` alone.');
222
219
  L.push('');
223
220
  L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
224
- L.push('with `ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); idle with work');
221
+ L.push('with `ours-fleet send <Name> "<text>"`; idle with work');
225
222
  L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
226
223
  L.push('stall. Escalate over ours messaging only when you cannot resolve it yourself.');
227
224
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.0.4",
3
- "buildId": "0891be00930f",
4
- "commit": "4f7c7292736219d27e8949442478996191cc2b54",
5
- "dirty": false,
6
- "builtAt": "2026-08-26T06:30:09.239Z",
2
+ "version": "1.1.0-nightly.1",
3
+ "buildId": "bf3c406ba57c",
4
+ "commit": "10d6da8bfd9903d216bce80a9ef89a42c282f8d9",
5
+ "dirty": true,
6
+ "builtAt": "2026-08-30T07:44:27.551Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -11,7 +11,6 @@ import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './
11
11
  import { findRole, loadConfig } from './config.js';
12
12
  import { formatDuration } from './duration.js';
13
13
  import { resolvedPlan } from './resolved-plan.js';
14
- import { Tmux, tmuxArgs } from './tmux.js';
15
14
  import { pickBackend } from './supervisor/index.js';
16
15
  import { up, down } from './ops.js';
17
16
  import { readRestartLedger, runSupervised, runTemp } from './runner.js';
@@ -74,7 +73,7 @@ const passthrough = (cmd, args) => new Promise(resolve => {
74
73
  });
75
74
  const program = new Command()
76
75
  .name('ours-fleet')
77
- .description('Fleet of persistent, identity-bound AI agents — selectable harness and tmux/ACP sessions.')
76
+ .description('Fleet of persistent, identity-bound AI agents — selectable harnesses over ACP sessions.')
78
77
  .enablePositionalOptions()
79
78
  .version(VERSION);
80
79
  const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'config file (default: ~/fleet.yaml + ~/fleet.d/)');
@@ -369,15 +368,11 @@ cOpt(program.command('force-restart [names...]').description('re-sync + bounce F
369
368
  });
370
369
  program.command('ls').description('list running fleet sessions')
371
370
  .action(async () => {
372
- // Each session has its own tmux server (#32), so there is no single server
373
- // to ask: the known role names ARE the list of servers to poll.
374
- const names = [];
375
371
  const acp = [];
376
372
  for (const root of [agentsRoot(), tmpRoot()]) {
377
373
  if (!existsSync(root))
378
374
  continue;
379
375
  for (const name of readdirSync(root)) {
380
- names.push(name);
381
376
  const stateDir = joinPath(root, name);
382
377
  if (!existsSync(controlSocketPath(stateDir)))
383
378
  continue;
@@ -395,14 +390,13 @@ program.command('ls').description('list running fleet sessions')
395
390
  catch { /* ignore stale sockets */ }
396
391
  }
397
392
  }
398
- const tmux = await new Tmux().list(names);
399
- console.log([tmux, ...acp].filter(Boolean).join('\n') || '(none)');
393
+ console.log(acp.join('\n') || '(none)');
400
394
  });
401
395
  program.command('attach <name>').description('open the live console (Ctrl-b d to leave)')
402
396
  .action(async (name) => {
403
397
  const stateDir = acpStateDir(name);
404
398
  if (!stateDir)
405
- process.exit(await passthrough('tmux', tmuxArgs(name, ['attach', '-t', name])));
399
+ throw new SessionControlError('offline', 'agent session is offline');
406
400
  try {
407
401
  const { socket, send } = await followControl(stateDir, message => {
408
402
  if ('event' in message)
@@ -439,7 +433,7 @@ program.command('attach <name>').description('open the live console (Ctrl-b d to
439
433
  die(e);
440
434
  }
441
435
  });
442
- /** Classify a raw tmux failure: only "no such session" proves the pane is gone. */
436
+ /** Preserve typed control failures and classify only definite missing-session errors as offline. */
443
437
  const asControlError = (e) => {
444
438
  if (e instanceof SessionControlError)
445
439
  return e;
@@ -467,23 +461,17 @@ program.command('peek <name> [lines]').description('pane snapshot without attach
467
461
  for (const event of events.slice(-(lines ? Number(lines) : 40)))
468
462
  renderSessionEvent(event);
469
463
  }
470
- else {
471
- console.log(await new Tmux().capture(name, lines ? Number(lines) : 40));
472
- }
464
+ else
465
+ throw new SessionControlError('offline', 'agent session is offline');
473
466
  }
474
467
  catch (e) {
475
468
  die(controlFailure(name, 'peek', e));
476
469
  }
477
470
  });
478
471
  program.command('send <name> [text...]').description("type into the agent's console")
479
- .option('--key <key>', 'send a raw key instead (Escape, Up, C-c, ...)')
480
472
  .action(async (name, text, opts) => {
481
473
  const stateDir = acpStateDir(name);
482
- if (stateDir && opts.key)
483
- die('--key is available only for tmux sessions');
484
- if (!stateDir && !opts.key && !text?.length)
485
- die('nothing to send: give text or --key');
486
- if (stateDir && !text?.length)
474
+ if (!text?.length)
487
475
  die('nothing to send: give text');
488
476
  try {
489
477
  if (stateDir) {
@@ -496,10 +484,8 @@ program.command('send <name> [text...]').description("type into the agent's cons
496
484
  ? `queued for ${name} behind ${queued.queuedBehind} running turn(s)`
497
485
  : `queued for ${name}`);
498
486
  }
499
- else if (opts.key)
500
- await new Tmux().sendKey(name, opts.key);
501
487
  else
502
- await new Tmux().sendText(name, text.join(' '));
488
+ throw new SessionControlError('offline', 'agent session is offline');
503
489
  }
504
490
  catch (e) {
505
491
  die(controlFailure(name, 'send', e, asControlError(e).kind === 'timeout'
@@ -1126,7 +1112,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1126
1112
  .option('--role <name>', 'role name (alternative to the positional name)')
1127
1113
  .option('--temp', 'temporary: independent transient supervisor, archived on retirement, gone on reboot')
1128
1114
  .option('--harness <id>', 'harness adapter (default: defaults.harness)')
1129
- .option('--session <backend>', 'session backend: tmux|acp (default: defaults.session or tmux)')
1115
+ .option('--session <backend>', 'session backend: acp (default: acp)')
1130
1116
  .option('--mission <text>', 'one-line mission')
1131
1117
  .option('--mission-file <path>', 'UTF-8 mission text (mutually exclusive with --mission)')
1132
1118
  .option('--identity <name>', 'ours identity to bind (default: role name)')
package/dist/config.d.ts CHANGED
@@ -25,7 +25,7 @@ export declare const NOTIFY_EVENT_TYPES: readonly ["message_received", "file_rec
25
25
  export type NotifyEventType = (typeof NOTIFY_EVENT_TYPES)[number];
26
26
  export type InjectMode = 'notification' | 'full';
27
27
  export type MonitorMode = 'fleet' | 'native';
28
- export type SessionBackendId = 'tmux' | 'acp';
28
+ export type SessionBackendId = 'acp';
29
29
  /** Public, harness-neutral permission policy. */
30
30
  export type FleetPermissionMode = 'ask' | 'auto' | 'allow';
31
31
  /** `deny` is a deprecated, fail-closed compatibility alias retained for old fleet files. */
@@ -44,9 +44,6 @@ export interface SessionOptions {
44
44
  /** ACP agent command and arguments. Defaults are supplied by the harness adapter. */
45
45
  command?: string | string[];
46
46
  };
47
- tmux?: {
48
- boot_grace_ms?: number;
49
- };
50
47
  }
51
48
  /** Resolved per-role supervisor-monitor config. */
52
49
  export interface MonitorConfig {
@@ -93,12 +90,14 @@ export interface OwnerAttachmentConfig {
93
90
  max_file_bytes: number;
94
91
  max_request_bytes: number;
95
92
  retention_ms: number;
96
- allowed_mime: string[];
97
93
  }
94
+ type OwnerAttachmentConfigInput = Partial<OwnerAttachmentConfig> & {
95
+ /** @deprecated Accepted only so existing configs keep loading; ignored. */
96
+ allowed_mime?: unknown;
97
+ };
98
98
  export type OwnerChannelConfigInput = Omit<Partial<OwnerChannelConfig>, 'attachments'> & {
99
- attachments?: Partial<OwnerAttachmentConfig>;
99
+ attachments?: OwnerAttachmentConfigInput;
100
100
  };
101
- export declare const DEFAULT_OWNER_ATTACHMENT_MIME: readonly ["application/pdf", "application/json", "text/plain", "image/png", "image/jpeg", "image/gif", "image/webp", "audio/ogg", "audio/mpeg", "audio/wav", "audio/x-wav", "audio/mp4", "audio/webm", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation"];
102
101
  /** Default wake sources when a role does not list its own. */
103
102
  export declare const DEFAULT_WAKE_SOURCES: NotifyEventType[];
104
103
  /**
@@ -236,3 +235,4 @@ export declare function resolveMonitorConfig(defMonitor: unknown, roleMonitor?:
236
235
  name?: string;
237
236
  }): MonitorConfig;
238
237
  export declare function findRole(cfg: FleetConfig, name: string): ResolvedRole;
238
+ export {};
package/dist/config.js CHANGED
@@ -21,15 +21,6 @@ export const NOTIFY_EVENT_TYPES = [
21
21
  'message_received', 'file_received', 'sibling_contact_added', 'local_contact_request',
22
22
  'pending_message', 'contact_restored', 'inbound_error', 'state_import_failed',
23
23
  ];
24
- export const DEFAULT_OWNER_ATTACHMENT_MIME = [
25
- 'application/pdf', 'application/json', 'text/plain',
26
- 'image/png', 'image/jpeg', 'image/gif', 'image/webp',
27
- 'audio/ogg', 'audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/mp4', 'audio/webm',
28
- 'application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint',
29
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
30
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
31
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
32
- ];
33
24
  /** Default wake sources when a role does not list its own. */
34
25
  export const DEFAULT_WAKE_SOURCES = ['message_received', 'file_received', 'local_contact_request', 'pending_message'];
35
26
  const MONITOR_KEYS = [
@@ -410,13 +401,6 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
410
401
  const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
411
402
  if (maxRequestBytes < maxFileBytes)
412
403
  throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.max_request_bytes must be at least max_file_bytes`);
413
- const allowedMime = attachments.allowed_mime ?? [...DEFAULT_OWNER_ATTACHMENT_MIME];
414
- if (!Array.isArray(allowedMime) || allowedMime.length < 1 || allowedMime.length > 64
415
- || allowedMime.some(mime => typeof mime !== 'string'
416
- || !/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(mime)))
417
- throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.allowed_mime must contain 1-64 lowercase MIME types`);
418
- if (new Set(allowedMime).size !== allowedMime.length)
419
- throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.allowed_mime must not contain duplicates`);
420
404
  if (session !== 'acp')
421
405
  throw new ConfigError(`${file}: role '${name}' owner_channel requires session: acp for correlated final replies`);
422
406
  return {
@@ -434,7 +418,6 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
434
418
  max_file_bytes: maxFileBytes,
435
419
  max_request_bytes: maxRequestBytes,
436
420
  retention_ms: attachments.retention_ms ?? 24 * 60 * 60 * 1_000,
437
- allowed_mime: [...allowedMime],
438
421
  },
439
422
  };
440
423
  }
@@ -541,9 +524,12 @@ export function resolveWorklogPolicy(defaults, role, file = 'config', name = 'ro
541
524
  return merged;
542
525
  }
543
526
  function resolveSession(raw, file, name) {
544
- const value = raw ?? 'tmux';
545
- if (value !== 'tmux' && value !== 'acp')
546
- throw new ConfigError(`${file}: role '${name}' session: must be one of: tmux, acp`);
527
+ const value = raw ?? 'acp';
528
+ if (value === 'tmux')
529
+ throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
530
+ + 'with the Codex or Claude Code adapter');
531
+ if (value !== 'acp')
532
+ throw new ConfigError(`${file}: role '${name}' session: must be: acp`);
547
533
  return value;
548
534
  }
549
535
  function resolveSessionOptions(defaults, role, session, file, name) {
@@ -558,33 +544,26 @@ function resolveSessionOptions(defaults, role, session, file, name) {
558
544
  ...((defaults?.acp) ?? {}),
559
545
  ...(role?.acp ?? {}),
560
546
  },
561
- tmux: {
562
- ...((defaults?.tmux) ?? {}),
563
- ...(role?.tmux ?? {}),
564
- },
565
547
  };
566
- const bad = Object.keys(merged).filter(k => k !== 'acp' && k !== 'tmux');
548
+ if (defaults?.tmux !== undefined
549
+ || role?.tmux !== undefined)
550
+ throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
551
+ + 'use session: acp with session_options.acp');
552
+ const bad = Object.keys(merged).filter(k => k !== 'acp');
567
553
  if (bad.length)
568
554
  throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
569
- if (!isPlainObject(merged.acp) || !isPlainObject(merged.tmux))
555
+ if (!isPlainObject(merged.acp))
570
556
  throw new ConfigError(`${file}: role '${name}' session_options.${session} must be a map`);
571
557
  const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
572
- const tmuxBad = Object.keys(merged.tmux).filter(k => k !== 'boot_grace_ms');
573
558
  if (acpBad.length)
574
559
  throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
575
- if (tmuxBad.length)
576
- throw new ConfigError(`${file}: role '${name}' session_options.tmux: unknown key(s) ${tmuxBad.join(', ')}`);
577
560
  const command = merged.acp.command;
578
561
  if (command !== undefined
579
562
  && !(typeof command === 'string' && command.trim())
580
563
  && !(Array.isArray(command) && command.length > 0
581
564
  && command.every(v => typeof v === 'string' && v.length > 0)))
582
565
  throw new ConfigError(`${file}: role '${name}' session_options.acp.command must be a non-empty string or string list`);
583
- const grace = merged.tmux.boot_grace_ms;
584
- if (grace !== undefined
585
- && (typeof grace !== 'number' || !Number.isFinite(grace) || grace < 0))
586
- throw new ConfigError(`${file}: role '${name}' session_options.tmux.boot_grace_ms must be a non-negative number`);
587
- return Object.keys(merged.acp).length || Object.keys(merged.tmux).length ? merged : undefined;
566
+ return Object.keys(merged.acp).length ? merged : undefined;
588
567
  }
589
568
  export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
590
569
  if (defaults !== undefined && !isPlainObject(defaults))