@ours.network/fleet 1.0.5 → 1.1.0-nightly.2

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 (100) hide show
  1. package/README.md +90 -112
  2. package/dist/application/capabilities.d.ts +0 -1
  3. package/dist/application/capabilities.js +3 -13
  4. package/dist/application/fleet-query-service.d.ts +0 -3
  5. package/dist/application/fleet-query-service.js +1 -24
  6. package/dist/application/role-creation-service.d.ts +5 -5
  7. package/dist/application/role-creation-service.js +14 -9
  8. package/dist/application/role-removal-service.js +6 -3
  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/types.d.ts +1 -8
  15. package/dist/briefing.js +3 -6
  16. package/dist/build-info.json +5 -5
  17. package/dist/cli.js +17 -28
  18. package/dist/config.d.ts +29 -6
  19. package/dist/config.js +320 -40
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +2 -2
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +51 -95
  24. package/dist/doctor.js +1 -8
  25. package/dist/fleet-proxy.d.ts +1 -1
  26. package/dist/generated-agent-source.d.ts +9 -0
  27. package/dist/generated-agent-source.js +53 -0
  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 +46 -0
  31. package/dist/harness/agent-session.js +1 -0
  32. package/dist/harness/claude-code-session.d.ts +22 -0
  33. package/dist/harness/claude-code-session.js +32 -0
  34. package/dist/harness/claude-code.d.ts +2 -1
  35. package/dist/harness/claude-code.js +39 -81
  36. package/dist/harness/codex-session.d.ts +22 -0
  37. package/dist/harness/codex-session.js +32 -0
  38. package/dist/harness/codex.d.ts +2 -1
  39. package/dist/harness/codex.js +49 -45
  40. package/dist/harness/registry.js +4 -0
  41. package/dist/harness/types.d.ts +6 -38
  42. package/dist/index.d.ts +3 -5
  43. package/dist/index.js +1 -3
  44. package/dist/init-guidance.d.ts +1 -0
  45. package/dist/init-guidance.js +5 -0
  46. package/dist/isolation/resources.d.ts +3 -7
  47. package/dist/isolation/resources.js +3 -7
  48. package/dist/model-env.d.ts +2 -4
  49. package/dist/model-env.js +4 -7
  50. package/dist/monitor.d.ts +1 -24
  51. package/dist/monitor.js +5 -140
  52. package/dist/ops.js +4 -2
  53. package/dist/owner-channel/channel.d.ts +2 -2
  54. package/dist/resolved-plan.d.ts +3 -2
  55. package/dist/resolved-plan.js +21 -5
  56. package/dist/runner.d.ts +5 -15
  57. package/dist/runner.js +26 -102
  58. package/dist/sensitive-config.d.ts +2 -0
  59. package/dist/sensitive-config.js +4 -0
  60. package/dist/session/acp.d.ts +2 -2
  61. package/dist/session/acp.js +2 -3
  62. package/dist/session/activity.d.ts +2 -3
  63. package/dist/session/activity.js +2 -3
  64. package/dist/session/arbiter.d.ts +4 -4
  65. package/dist/session/control.d.ts +3 -3
  66. package/dist/session/types.d.ts +11 -5
  67. package/dist/spawn.d.ts +4 -4
  68. package/dist/spawn.js +60 -32
  69. package/dist/supervisor/none.d.ts +1 -1
  70. package/dist/supervisor/none.js +2 -2
  71. package/dist/tmux.d.ts +1 -14
  72. package/dist/tmux.js +1 -51
  73. package/dist/watchdog/config.js +6 -4
  74. package/dist/watchdog/run.js +8 -23
  75. package/dist/web/auth.d.ts +1 -1
  76. package/dist/web/fleet-config-service.d.ts +11 -8
  77. package/dist/web/fleet-config-service.js +383 -183
  78. package/dist/web/runtime.js +4 -15
  79. package/dist/web/server.d.ts +1 -3
  80. package/dist/web/server.js +3 -14
  81. package/dist/web/topology-promote.js +18 -18
  82. package/dist/web/topology.js +2 -2
  83. package/dist/web/yaml-document-edit.js +2 -0
  84. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  85. package/dist/web-app/assets/index-BbE9EX6n.js +10 -0
  86. package/dist/web-app/index.html +2 -2
  87. package/examples/fleet/agents/Alice.yaml +11 -0
  88. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  89. package/examples/fleet/brains/claude-default.yaml +5 -0
  90. package/examples/fleet/roles/coordinator.yaml +5 -0
  91. package/examples/fleet/roles/developer.yaml +3 -0
  92. package/examples/fleet.yaml +74 -0
  93. package/package.json +4 -8
  94. package/dist/session/tmux.d.ts +0 -28
  95. package/dist/session/tmux.js +0 -80
  96. package/dist/web/terminal/bridge.d.ts +0 -27
  97. package/dist/web/terminal/bridge.js +0 -317
  98. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  99. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  100. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
package/dist/spawn.js CHANGED
@@ -2,14 +2,15 @@ import { spawn as spawnChild } from 'node:child_process';
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { parse, stringify } from 'yaml';
5
- import { agentDir, fleetDDir } from './paths.js';
5
+ import { agentDir, defaultConfigPath } from './paths.js';
6
6
  import { validateIsolationConfig } from './isolation/policy.js';
7
- import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolveOwnerChannelConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, validateMonitorConfig, } from './config.js';
7
+ import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolveOwnerChannelConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, splitRootFor, validateMonitorConfig, } from './config.js';
8
8
  import { resolveRoleModelEnv } from './model-env.js';
9
9
  import { applyRole, up } from './ops.js';
10
10
  import { START_STAGGER_FILE } from './runner.js';
11
11
  import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
12
12
  import { VERSION } from './version.js';
13
+ import { recordGeneratedAgentSource } from './generated-agent-source.js';
13
14
  import './harness/claude-code.js';
14
15
  import './harness/codex.js';
15
16
  import { getAdapter } from './harness/registry.js';
@@ -34,8 +35,9 @@ export function profileValues(o) {
34
35
  /** Pure option-to-role mapping shared by CLI and application services. */
35
36
  export function buildRoleConfig(o, defaultHarness) {
36
37
  const r = {};
37
- if (o.harness)
38
- r.harness = o.harness;
38
+ const harness = o.harness ?? defaultHarness;
39
+ if (harness)
40
+ r.harness = harness;
39
41
  if (o.session)
40
42
  r.session = o.session;
41
43
  if (o.identity)
@@ -52,7 +54,8 @@ export function buildRoleConfig(o, defaultHarness) {
52
54
  r.model = null;
53
55
  else if (o.model?.trim())
54
56
  r.model = o.model.trim();
55
- const harness = o.harness ?? defaultHarness;
57
+ if (o.reasoningEffort?.trim())
58
+ r.effort = o.reasoningEffort.trim();
56
59
  const harnessOptions = {};
57
60
  if (o.permissionMode)
58
61
  harnessOptions[harness === 'claude-code' ? 'permission_mode' : 'approval'] = o.permissionMode;
@@ -65,12 +68,8 @@ export function buildRoleConfig(o, defaultHarness) {
65
68
  if (o.search === true)
66
69
  harnessOptions.search = true;
67
70
  const codexConfig = { ...(o.codexConfig ?? {}) };
68
- if (o.reasoningEffort && harness === 'codex')
69
- codexConfig.model_reasoning_effort = o.reasoningEffort;
70
71
  if (Object.keys(codexConfig).length)
71
72
  harnessOptions.config = codexConfig;
72
- if (o.reasoningEffort && harness === 'claude-code')
73
- harnessOptions.effort = o.reasoningEffort;
74
73
  if (o.addDirs?.length)
75
74
  harnessOptions.add_dirs = o.addDirs;
76
75
  if (o.monitor === true)
@@ -123,8 +122,10 @@ export function readIsolationFile(path) {
123
122
  export function validateSpawnOpts(o) {
124
123
  if (o.mission !== undefined && o.missionFile)
125
124
  throw new Error('--mission and --mission-file are mutually exclusive');
126
- if (o.session && !['tmux', 'acp'].includes(o.session))
127
- throw new Error(`invalid --session '${o.session}'; allowed: tmux, acp`);
125
+ if (o.session === 'tmux')
126
+ throw new Error("invalid --session 'tmux'; tmux is no longer supported; use --session acp");
127
+ if (o.session && o.session !== 'acp')
128
+ throw new Error(`invalid --session '${o.session}'; allowed: acp`);
128
129
  if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
129
130
  throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
130
131
  if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
@@ -172,6 +173,23 @@ function assertNameFree(o) {
172
173
  }
173
174
  /** The ours identity a spawn will bind: explicit, else the role name. */
174
175
  export const effectiveIdentity = (o) => o.identity ?? o.name;
176
+ /** Bare Agent document written by v2 spawn: inline Role × inline Brain + operations. */
177
+ export function buildAgentDocument(raw) {
178
+ const role = Object.fromEntries(['mission', 'persona', 'bio', 'briefing_file']
179
+ .filter(key => raw[key] !== undefined)
180
+ .map(key => [key, raw[key]]));
181
+ const brain = Object.fromEntries([
182
+ 'harness', 'session', 'session_options', 'model', 'model_chain', 'max_tokens',
183
+ 'autocompact_pct', 'harness_options', 'effort',
184
+ ].filter(key => raw[key] !== undefined)
185
+ .map(key => [key, raw[key]]));
186
+ const operational = Object.fromEntries([
187
+ 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee', 'isolation',
188
+ 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
189
+ ].filter(key => raw[key] !== undefined)
190
+ .map(key => [key, raw[key]]));
191
+ return { role: { inline: role }, brain: { inline: brain }, ...operational };
192
+ }
175
193
  /**
176
194
  * Validate and resolve a spawn without reserving names, contacting the daemon,
177
195
  * or writing state. Collision checks are necessarily a point-in-time snapshot.
@@ -184,7 +202,7 @@ export function spawnDryRun(o) {
184
202
  readMissionFile(o.missionFile);
185
203
  assertNameFree(o);
186
204
  const cfg = loadConfig(o.configPath);
187
- const raw = buildRoleConfig(o, cfg.defaults.harness);
205
+ const raw = buildRoleConfig(o, cfg.defaults.harness ?? 'claude-code');
188
206
  const harnessOptions = {
189
207
  ...(cfg.defaults.harness_options ?? {}),
190
208
  ...(raw.harness_options ?? {}),
@@ -202,23 +220,30 @@ export function spawnDryRun(o) {
202
220
  roleEnv: raw.env,
203
221
  ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
204
222
  });
205
- const model = modelEnv.model;
206
- const session = raw.session ?? cfg.defaults.session ?? 'tmux';
223
+ const adapter = getAdapter(harness);
224
+ const brain = adapter.agentSession.resolveBrain({
225
+ model: modelEnv.model,
226
+ effort: raw.effort,
227
+ harnessOptions: Object.keys(harnessOptions).length ? harnessOptions : undefined,
228
+ });
229
+ const model = brain.model ?? undefined;
230
+ const session = raw.session ?? cfg.defaults.session ?? 'acp';
207
231
  const resolvedRole = {
208
232
  ...raw,
209
233
  name: o.name,
210
- sourceFile: o.temp ? '(temp dry-run)' : join(fleetDDir(), `${o.name}.yaml`),
234
+ sourceFile: o.temp ? '(temp dry-run)' : join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents', `${o.name}.yaml`),
211
235
  harness,
212
236
  session,
213
237
  session_options: raw.session_options,
214
238
  permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
215
239
  permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
216
240
  identity: effectiveIdentity(o),
241
+ effort: raw.effort,
217
242
  model,
218
243
  model_chain: resolveModelChain(model, raw.model_chain ?? (inheritsModelDefaults
219
244
  ? cfg.defaults.model_chain
220
245
  : undefined)),
221
- harness_options: Object.keys(harnessOptions).length ? harnessOptions : undefined,
246
+ harness_options: brain.harnessOptions,
222
247
  isolation: raw.isolation ?? cfg.defaults.isolation,
223
248
  monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
224
249
  owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
@@ -226,7 +251,6 @@ export function spawnDryRun(o) {
226
251
  auth_proxy: authProxy,
227
252
  };
228
253
  resolvedRole.env = modelEnv.env;
229
- const adapter = getAdapter(resolvedRole.harness);
230
254
  if (resolvedRole.auth_proxy && resolvedRole.harness !== 'claude-code')
231
255
  throw new Error('auth_proxy is supported only by claude-code');
232
256
  const optionProblems = adapter.validateOptions(resolvedRole.harness_options, resolvedRole);
@@ -235,7 +259,7 @@ export function spawnDryRun(o) {
235
259
  return {
236
260
  schemaVersion: 1,
237
261
  warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
238
- roleDocument: { roles: { [o.name]: raw } },
262
+ roleDocument: buildAgentDocument(raw),
239
263
  resolvedRole,
240
264
  };
241
265
  }
@@ -255,7 +279,7 @@ function provenanceSettings(o, defaults) {
255
279
  const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
256
280
  return {
257
281
  harness: tagged('harness', provenanceOf(o.harness, defaults.harness, 'claude-code')),
258
- session: tagged('session', provenanceOf(o.session, defaults.session, 'tmux')),
282
+ session: tagged('session', provenanceOf(o.session, defaults.session, 'acp')),
259
283
  identity: o.identity
260
284
  ? { value: o.identity, source: 'cli' }
261
285
  : { value: o.name, source: 'built-in' }, // defaults to the role name
@@ -276,7 +300,7 @@ function provenanceSettings(o, defaults) {
276
300
  monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
277
301
  };
278
302
  }
279
- /** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
303
+ /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
280
304
  export async function spawnPermanent(o, deps, creation = {}) {
281
305
  validateSpawnOpts(o);
282
306
  if (o.isolationFile)
@@ -305,12 +329,11 @@ export async function spawnPermanent(o, deps, creation = {}) {
305
329
  await creation.identityProvisioner?.remove?.(effectiveIdentity(o));
306
330
  },
307
331
  });
308
- mkdirSync(fleetDDir(), { recursive: true });
332
+ const agentRoot = join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents');
333
+ mkdirSync(agentRoot, { recursive: true });
309
334
  creation.onStage?.('writing_role');
310
- const file = join(fleetDDir(), `${o.name}.yaml`);
311
- writeRoleFile(tx, file, stringify({
312
- roles: { [o.name]: buildRoleConfig(o, cfg.defaults.harness) },
313
- }));
335
+ const file = join(agentRoot, `${o.name}.yaml`);
336
+ writeRoleFile(tx, file, stringify(buildAgentDocument(buildRoleConfig(o, cfg.defaults.harness ?? 'claude-code'))));
314
337
  // `up` materialises the state dir and registers the service. Journal the
315
338
  // dir before it exists so a failure leaves the name genuinely reusable
316
339
  // rather than blocked by a half-built directory.
@@ -341,6 +364,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
341
364
  });
342
365
  mkdirSync(agentDir(o.name), { recursive: true });
343
366
  writeProvenance(agentDir(o.name), provenance);
367
+ recordGeneratedAgentSource(agentDir(o.name), o.configPath ?? defaultConfigPath(), file);
344
368
  creation.onStage?.('registering_supervisor');
345
369
  await up(loadConfig(o.configPath), [o.name], {
346
370
  ...deps,
@@ -416,19 +440,26 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
416
440
  roleEnv: fromOpts.env,
417
441
  ...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
418
442
  });
419
- const model = modelEnv.model;
420
- const session = o.session ?? cfg.defaults.session ?? 'tmux';
443
+ const adapter = getAdapter(harness);
444
+ const brain = adapter.agentSession.resolveBrain({
445
+ model: modelEnv.model,
446
+ effort: fromOpts.effort,
447
+ harnessOptions: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
448
+ });
449
+ const model = brain.model ?? undefined;
450
+ const session = o.session ?? cfg.defaults.session ?? 'acp';
421
451
  const role = {
422
452
  ...fromOpts, // includes `isolation` when --isolation-file was given
423
453
  name: o.name,
424
454
  harness,
425
455
  session,
426
456
  identity: o.identity ?? o.name,
457
+ effort: fromOpts.effort,
427
458
  model,
428
459
  model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
429
460
  ? cfg.defaults.model_chain
430
461
  : undefined)),
431
- harness_options: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
462
+ harness_options: brain.harnessOptions,
432
463
  permissions: resolvePermissions(cfg.defaults.permissions, fromOpts.permissions),
433
464
  permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
434
465
  // Temp agents inherit the fleet-wide monitor defaults via the snapshot.
@@ -467,10 +498,7 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
467
498
  if (cfg.startStaggerMs > 0)
468
499
  writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
469
500
  prepareTempSupervisor(dir, o.name);
470
- // Run the supervisor independently — NOT inside a tmux session named <name>.
471
- // `_run-temp` -> runOnce() creates AND kills the tmux session <name> for the
472
- // agent itself; a supervisor sharing that session name would SIGHUP its own
473
- // process before the agent ever launches. On a service-managed host the temp
501
+ // Run the supervisor independently. On a service-managed host the temp
474
502
  // runner gets its own transient unit/job, so stopping the coordinator's unit
475
503
  // cannot kill a live worker in the coordinator's cgroup.
476
504
  onStage?.('starting_temp');
@@ -1,7 +1,7 @@
1
1
  import { type Exec } from '../exec.js';
2
2
  import type { SupervisorBackend } from './types.js';
3
3
  /**
4
- * No supervision: sessions are plain tmux, nothing survives a reboot and
4
+ * No service supervision: tmux contains the direct runner process, nothing survives a reboot and
5
5
  * nothing restarts on crash. Used for temp agents and CI tests
6
6
  * (OURS_FLEET_SUPERVISOR=none).
7
7
  */
@@ -1,7 +1,7 @@
1
1
  import { Tmux, tmuxArgs } from '../tmux.js';
2
2
  import { realExec, shq } from '../exec.js';
3
3
  /**
4
- * No supervision: sessions are plain tmux, nothing survives a reboot and
4
+ * No service supervision: tmux contains the direct runner process, nothing survives a reboot and
5
5
  * nothing restarts on crash. Used for temp agents and CI tests
6
6
  * (OURS_FLEET_SUPERVISOR=none).
7
7
  */
@@ -9,7 +9,7 @@ export function makeNoneBackend(exec = realExec) {
9
9
  const tmux = new Tmux(exec);
10
10
  return {
11
11
  id: 'none',
12
- async init() { return ['no supervisor: sessions are plain tmux (no reboot survival)']; },
12
+ async init() { return ['no service supervisor: direct runner containment only (no reboot survival)']; },
13
13
  async install(name, binPath) {
14
14
  const existed = await tmux.kill(name); // true when a session was there
15
15
  await tmux.newSession(name, process.cwd(), `${shq(binPath)} _run ${shq(name)}`);
package/dist/tmux.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare const tmuxSocket: (session: string) => string;
20
20
  * the shared default server and re-creates #32.
21
21
  */
22
22
  export declare const tmuxArgs: (session: string, args: string[]) => string[];
23
- /** Thin tmux wrapper; all session handling in the core goes through this. */
23
+ /** Thin tmux wrapper retained only for supervisor=none process containment. */
24
24
  export declare class Tmux {
25
25
  private exec;
26
26
  constructor(exec?: Exec);
@@ -32,17 +32,4 @@ export declare class Tmux {
32
32
  * program inside it exited on its own".
33
33
  */
34
34
  kill(name: string): Promise<boolean>;
35
- capture(name: string, lines?: number): Promise<string>;
36
- /** Bounded ANSI/history seed for the browser terminal projection. */
37
- captureHistory(name: string, lines?: number): Promise<string>;
38
- panePid(name: string): Promise<number | null>;
39
- /**
40
- * List the live sessions among `names`, asking each server in turn.
41
- * There is no fleet-wide `tmux ls` any more: a session per server means the
42
- * caller says which sessions to ask about. A server that is not running
43
- * answers non-zero and contributes nothing.
44
- */
45
- list(names: readonly string[]): Promise<string>;
46
- sendText(name: string, text: string): Promise<void>;
47
- sendKey(name: string, key: string): Promise<void>;
48
35
  }
package/dist/tmux.js CHANGED
@@ -20,7 +20,7 @@ export const tmuxSocket = (session) => `${TMUX_SOCKET_PREFIX}${session}`;
20
20
  * the shared default server and re-creates #32.
21
21
  */
22
22
  export const tmuxArgs = (session, args) => ['-L', tmuxSocket(session), ...args];
23
- /** Thin tmux wrapper; all session handling in the core goes through this. */
23
+ /** Thin tmux wrapper retained only for supervisor=none process containment. */
24
24
  export class Tmux {
25
25
  exec;
26
26
  constructor(exec = realExec) {
@@ -42,54 +42,4 @@ export class Tmux {
42
42
  async kill(name) {
43
43
  return (await this.exec('tmux', tmuxArgs(name, ['kill-session', '-t', name]))).code === 0;
44
44
  }
45
- async capture(name, lines = 40) {
46
- const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p']));
47
- if (r.code !== 0)
48
- throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
49
- const all = r.stdout.replace(/\n+$/, '').split('\n');
50
- return all.slice(-lines).join('\n');
51
- }
52
- /** Bounded ANSI/history seed for the browser terminal projection. */
53
- async captureHistory(name, lines = 5_000) {
54
- const bounded = Math.min(Math.max(Math.trunc(lines), 1), 20_000);
55
- const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p', '-e', '-J', '-S', `-${bounded}`]));
56
- if (r.code !== 0)
57
- throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
58
- return Buffer.from(r.stdout).subarray(0, 4 * 1024 * 1024).toString();
59
- }
60
- async panePid(name) {
61
- const r = await this.exec('tmux', tmuxArgs(name, ['list-panes', '-t', name, '-F', '#{pane_pid}']));
62
- if (r.code !== 0)
63
- return null;
64
- const pid = parseInt(r.stdout.trim().split('\n')[0], 10);
65
- return Number.isFinite(pid) ? pid : null;
66
- }
67
- /**
68
- * List the live sessions among `names`, asking each server in turn.
69
- * There is no fleet-wide `tmux ls` any more: a session per server means the
70
- * caller says which sessions to ask about. A server that is not running
71
- * answers non-zero and contributes nothing.
72
- */
73
- async list(names) {
74
- const lines = [];
75
- for (const name of names) {
76
- const r = await this.exec('tmux', tmuxArgs(name, ['ls']));
77
- if (r.code === 0 && r.stdout.trim())
78
- lines.push(r.stdout.trimEnd());
79
- }
80
- return lines.join('\n');
81
- }
82
- async sendText(name, text) {
83
- let r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, '-l', text]));
84
- if (r.code !== 0)
85
- throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
86
- r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, 'Enter']));
87
- if (r.code !== 0)
88
- throw new Error(`tmux send-keys Enter '${name}' failed: ${r.stderr.trim()}`);
89
- }
90
- async sendKey(name, key) {
91
- const r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, key]));
92
- if (r.code !== 0)
93
- throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
94
- }
95
45
  }
@@ -31,7 +31,7 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
31
31
  const roleIdentities = new Set(roles.map(r => r.identity));
32
32
  // Tracks identity -> owning watchdog name across entries:
33
33
  // two watchdogs declaring the same identity would otherwise share a temp
34
- // dir, tmux session, and run lock with no error until they collide at
34
+ // dir, agent session, and run lock with no error until they collide at
35
35
  // runtime.
36
36
  const watchdogIdentities = new Map();
37
37
  const out = [];
@@ -61,9 +61,11 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
61
61
  throw new ConfigError(`${where}: identity '${identity}' collides with watchdog '${collidingWatchdog}'`);
62
62
  watchdogIdentities.set(identity, name);
63
63
  const harness = w.harness ?? defaults.harness ?? 'claude-code';
64
- const sessionRaw = w.session ?? defaults.session ?? 'tmux';
65
- if (sessionRaw !== 'tmux' && sessionRaw !== 'acp')
66
- throw new ConfigError(`${where}: session must be 'tmux' or 'acp'`);
64
+ const sessionRaw = w.session ?? defaults.session ?? 'acp';
65
+ if (sessionRaw === 'tmux')
66
+ throw new ConfigError(`${where}: session 'tmux' is no longer supported; use session: acp`);
67
+ if (sessionRaw !== 'acp')
68
+ throw new ConfigError(`${where}: session must be 'acp'`);
67
69
  if (w.prompt_file !== undefined) {
68
70
  if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
69
71
  throw new ConfigError(`${where}: prompt_file must be an absolute path`);
@@ -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
  }
@@ -1,10 +1,13 @@
1
1
  import type { PrereqReport } from '../harness/types.js';
2
2
  export declare const REDACTED_ENV_VALUE = "__OURS_FLEET_SECRET_REDACTED__";
3
- export type EditableFleetModel = Record<string, unknown>;
3
+ export interface EditableFleetModel {
4
+ manifest: Record<string, unknown>;
5
+ agents: Record<string, Record<string, unknown>>;
6
+ }
4
7
  export interface ConfigReadResult {
5
8
  path: string;
6
- exists: boolean;
7
- firstRun: boolean;
9
+ exists: true;
10
+ firstRun: false;
8
11
  revision: string;
9
12
  model: EditableFleetModel;
10
13
  redactions: string[];
@@ -33,15 +36,15 @@ export interface ConfigWriteResult extends ConfigPreviewResult {
33
36
  export interface FleetConfigServiceOptions {
34
37
  configPath?: string;
35
38
  preflight?(configPath: string): Promise<PrereqReport>;
39
+ /** Fault-injection seam; called under lock immediately before each mutation. */
40
+ beforeMutation?(relativePath: string, index: number): void;
36
41
  }
37
42
  export declare class FleetConfigService {
38
43
  readonly path: string;
39
44
  private readonly preflight;
45
+ private readonly beforeMutation?;
40
46
  constructor(options?: FleetConfigServiceOptions);
41
47
  read(): ConfigReadResult;
42
- preview(baseRevision: string, model: unknown): Promise<ConfigPreviewResult>;
43
- write(baseRevision: string, model: unknown): Promise<ConfigWriteResult>;
44
- private readSource;
45
- private assertRevision;
46
- private validateCandidate;
48
+ preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
49
+ write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
47
50
  }