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

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 (159) hide show
  1. package/README.md +122 -133
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.d.ts +0 -1
  5. package/dist/application/capabilities.js +3 -13
  6. package/dist/application/fleet-query-service.d.ts +0 -3
  7. package/dist/application/fleet-query-service.js +14 -25
  8. package/dist/application/role-creation-service.d.ts +5 -24
  9. package/dist/application/role-creation-service.js +27 -67
  10. package/dist/application/role-removal-service.js +6 -3
  11. package/dist/application/role-repository.d.ts +1 -2
  12. package/dist/application/role-repository.js +7 -7
  13. package/dist/application/session-control.d.ts +0 -12
  14. package/dist/application/session-control.js +0 -42
  15. package/dist/application/session-mutations.d.ts +5 -5
  16. package/dist/application/task-room-service.d.ts +1 -1
  17. package/dist/application/task-room-service.js +154 -16
  18. package/dist/application/types.d.ts +1 -8
  19. package/dist/briefing.js +12 -11
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +143 -63
  22. package/dist/config-yaml.d.ts +1 -1
  23. package/dist/config.d.ts +66 -6
  24. package/dist/config.js +398 -43
  25. package/dist/creation.d.ts +1 -1
  26. package/dist/creation.js +2 -2
  27. package/dist/daemon-recovery.d.ts +93 -0
  28. package/dist/daemon-recovery.js +328 -0
  29. package/dist/docs.d.ts +1 -1
  30. package/dist/docs.js +103 -127
  31. package/dist/doctor.js +18 -11
  32. package/dist/fleet-command-audit.d.ts +160 -0
  33. package/dist/fleet-command-audit.js +433 -0
  34. package/dist/fleet-proxy.d.ts +3 -1
  35. package/dist/fleet-proxy.js +30 -8
  36. package/dist/generated-agent-source.d.ts +9 -0
  37. package/dist/generated-agent-source.js +53 -0
  38. package/dist/harness/acp-session-transport.d.ts +8 -0
  39. package/dist/harness/acp-session-transport.js +3 -0
  40. package/dist/harness/agent-session.d.ts +51 -0
  41. package/dist/harness/agent-session.js +1 -0
  42. package/dist/harness/claude-code-session.d.ts +24 -0
  43. package/dist/harness/claude-code-session.js +36 -0
  44. package/dist/harness/claude-code.d.ts +2 -1
  45. package/dist/harness/claude-code.js +41 -81
  46. package/dist/harness/codex-session.d.ts +24 -0
  47. package/dist/harness/codex-session.js +36 -0
  48. package/dist/harness/codex.d.ts +2 -1
  49. package/dist/harness/codex.js +55 -45
  50. package/dist/harness/registry.js +4 -0
  51. package/dist/harness/types.d.ts +6 -38
  52. package/dist/index.d.ts +4 -6
  53. package/dist/index.js +2 -4
  54. package/dist/init-guidance.d.ts +1 -0
  55. package/dist/init-guidance.js +6 -0
  56. package/dist/isolation/resources.d.ts +3 -7
  57. package/dist/isolation/resources.js +3 -7
  58. package/dist/model-env.d.ts +4 -6
  59. package/dist/model-env.js +5 -8
  60. package/dist/monitor.d.ts +1 -24
  61. package/dist/monitor.js +5 -140
  62. package/dist/ops.js +4 -2
  63. package/dist/owner-channel/channel.d.ts +58 -2
  64. package/dist/owner-channel/channel.js +269 -29
  65. package/dist/owner-channel/commands.d.ts +2 -0
  66. package/dist/owner-channel/commands.js +31 -13
  67. package/dist/owner-channel/state.d.ts +2 -0
  68. package/dist/owner-channel/state.js +8 -2
  69. package/dist/preset-bootstrap.d.ts +10 -0
  70. package/dist/preset-bootstrap.js +85 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.d.ts +3 -2
  84. package/dist/resolved-plan.js +21 -5
  85. package/dist/rooms-tasks/cli.js +225 -40
  86. package/dist/rooms-tasks/config.js +24 -13
  87. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  88. package/dist/rooms-tasks/external-worker.js +14 -9
  89. package/dist/rooms-tasks/provision.js +98 -36
  90. package/dist/rooms-tasks/templates.d.ts +0 -1
  91. package/dist/rooms-tasks/templates.js +9 -76
  92. package/dist/rooms-tasks/types.d.ts +10 -16
  93. package/dist/rooms-tasks/types.js +1 -4
  94. package/dist/runner.d.ts +13 -16
  95. package/dist/runner.js +110 -122
  96. package/dist/sensitive-config.d.ts +2 -0
  97. package/dist/sensitive-config.js +4 -0
  98. package/dist/session/acp.d.ts +7 -2
  99. package/dist/session/acp.js +29 -6
  100. package/dist/session/activity.d.ts +2 -3
  101. package/dist/session/activity.js +2 -3
  102. package/dist/session/arbiter.d.ts +4 -4
  103. package/dist/session/control.d.ts +17 -4
  104. package/dist/session/control.js +35 -0
  105. package/dist/session/types.d.ts +11 -5
  106. package/dist/spawn.d.ts +8 -33
  107. package/dist/spawn.js +53 -213
  108. package/dist/supervisor/none.d.ts +1 -1
  109. package/dist/supervisor/none.js +2 -2
  110. package/dist/temp-lifecycle.d.ts +1 -1
  111. package/dist/tmux.d.ts +1 -14
  112. package/dist/tmux.js +1 -51
  113. package/dist/watchdog/config.d.ts +10 -7
  114. package/dist/watchdog/config.js +30 -18
  115. package/dist/watchdog/run.js +12 -36
  116. package/dist/web/auth.d.ts +1 -1
  117. package/dist/web/fleet-config-service.d.ts +11 -8
  118. package/dist/web/fleet-config-service.js +384 -183
  119. package/dist/web/runtime.js +4 -15
  120. package/dist/web/server.d.ts +1 -3
  121. package/dist/web/server.js +3 -14
  122. package/dist/web/topology-promote.js +26 -19
  123. package/dist/web/topology.js +2 -2
  124. package/dist/web/yaml-document-edit.js +2 -0
  125. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  126. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  127. package/dist/web-app/index.html +2 -2
  128. package/examples/fleet/agents/Alice.yaml +11 -0
  129. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  130. package/examples/fleet/brains/claude-default.yaml +5 -0
  131. package/examples/fleet/roles/coordinator.yaml +5 -0
  132. package/examples/fleet/roles/developer.yaml +3 -0
  133. package/examples/fleet.yaml +74 -0
  134. package/package.json +5 -8
  135. package/presets/fleet/agents/Agent.yaml +3 -0
  136. package/presets/fleet/agents/Architect.yaml +3 -0
  137. package/presets/fleet/agents/Critic.yaml +3 -0
  138. package/presets/fleet/agents/Developer.yaml +3 -0
  139. package/presets/fleet/agents/Secretary.yaml +3 -0
  140. package/presets/fleet/agents/Tester.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/roles/Agent.yaml +6 -0
  143. package/presets/fleet/roles/Architect.yaml +6 -0
  144. package/presets/fleet/roles/Critic.yaml +6 -0
  145. package/presets/fleet/roles/Developer.yaml +6 -0
  146. package/presets/fleet/roles/Secretary.yaml +6 -0
  147. package/presets/fleet/roles/Tester.yaml +6 -0
  148. package/presets/fleet/room_templates/pair.yaml +11 -0
  149. package/presets/fleet/room_templates/single.yaml +8 -0
  150. package/presets/fleet/room_templates/team.yaml +12 -0
  151. package/presets/fleet.yaml +7 -0
  152. package/presets/manifest.json +5 -0
  153. package/dist/session/tmux.d.ts +0 -28
  154. package/dist/session/tmux.js +0 -80
  155. package/dist/web/terminal/bridge.d.ts +0 -27
  156. package/dist/web/terminal/bridge.js +0 -317
  157. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  158. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  159. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -1,30 +1,20 @@
1
1
  export function roleCapabilities(role, status, context = {}) {
2
- const backend = role.configuredBackend ?? (role.detectedBackend === 'acp' || role.detectedBackend === 'tmux' ? role.detectedBackend : undefined);
2
+ const backend = role.configuredBackend ?? (role.detectedBackend === 'acp' ? 'acp' : undefined);
3
3
  const online = status.session.reachability === 'online';
4
4
  const acp = backend === 'acp';
5
- const tmux = backend === 'tmux';
6
5
  const protocolVersion = context.controlProtocolVersion ?? 1;
7
6
  const inactive = status.overall === 'offline'
8
7
  || (!online && status.supervisor.liveness === 'stopped');
9
- const terminalAvailable = tmux && online && context.terminalPtyAvailable === true;
10
- const terminalReason = terminalAvailable ? undefined
11
- : !tmux ? 'wrong_backend'
12
- : !online ? 'offline'
13
- : 'pty_unavailable';
14
8
  return {
15
9
  protocolVersion,
16
10
  inventory: true,
17
11
  status: true,
18
12
  output: {
19
- recent: acp || tmux, stream: (acp && protocolVersion >= 2) || terminalAvailable,
13
+ recent: acp, stream: acp && protocolVersion >= 2,
20
14
  structured: acp, replayCursor: acp && protocolVersion >= 2,
21
15
  },
22
- input: { text: online && (acp || tmux), rawKeys: terminalAvailable, interrupt: online && acp, steering: false },
16
+ input: { text: online && acp, interrupt: online && acp, steering: false },
23
17
  permissions: { observe: acp, respond: acp && online },
24
- terminal: {
25
- available: terminalAvailable, reason: terminalReason,
26
- multiViewer: terminalAvailable, writerLease: terminalAvailable,
27
- },
28
18
  lifecycle: {
29
19
  start: role.lifetime === 'permanent' && status.supervisor.liveness !== 'running',
30
20
  stop: role.lifetime === 'permanent' && !inactive && status.supervisor.liveness !== 'stopped',
@@ -1,13 +1,11 @@
1
1
  import type { SupervisorBackend } from '../supervisor/types.js';
2
2
  import { controlRequest } from '../session/control.js';
3
- import { Tmux } from '../tmux.js';
4
3
  import { type CapabilityContext } from './capabilities.js';
5
4
  import { RoleRepository } from './role-repository.js';
6
5
  import type { RoleCapabilities, RoleRecord, RoleStatus } from './types.js';
7
6
  export interface FleetQueryOptions {
8
7
  repository: RoleRepository;
9
8
  supervisor: SupervisorBackend;
10
- tmux?: Tmux;
11
9
  control?: typeof controlRequest;
12
10
  capabilityContext?: CapabilityContext;
13
11
  /**
@@ -24,7 +22,6 @@ export interface FleetQueryOptions {
24
22
  }
25
23
  export declare class FleetQueryService {
26
24
  private readonly options;
27
- private readonly tmux;
28
25
  private readonly control;
29
26
  constructor(options: FleetQueryOptions);
30
27
  list(): Promise<Array<{
@@ -4,7 +4,7 @@ import { classifyActivity } from '../session/activity.js';
4
4
  import { controlRequest } from '../session/control.js';
5
5
  import { SessionControlError } from '../session/types.js';
6
6
  import { readExitRecord, readRestartLedger } from '../runner.js';
7
- import { Tmux } from '../tmux.js';
7
+ import { readDaemonRecoveryStatus } from '../daemon-recovery.js';
8
8
  import { roleCapabilities } from './capabilities.js';
9
9
  import { FleetError } from './errors.js';
10
10
  const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
@@ -47,10 +47,11 @@ function readIsolation(dir) {
47
47
  function sessionOverall(supervisor, session, restart, monitor, isolation, problems) {
48
48
  if (restart.circuit === 'open' || monitor.health === 'failed' || monitor.health === 'degraded'
49
49
  || isolation.degraded
50
- || problems.some(problem => problem.severity === 'error' || problem.source === 'watchdog')
50
+ || problems.some(problem => problem.severity === 'error'
51
+ || problem.source === 'watchdog' || problem.source === 'daemon-recovery')
51
52
  || session.readiness === 'failed')
52
53
  return 'attention';
53
- // A reachable ACP/tmux session is the user's live interaction surface. Its
54
+ // A reachable agent session is the user's live interaction surface. Its
54
55
  // evidence remains authoritative even when a service manager no longer owns
55
56
  // the process (for example, a detached ACP session with an inactive unit).
56
57
  if (session.reachability === 'online'
@@ -69,11 +70,9 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
69
70
  }
70
71
  export class FleetQueryService {
71
72
  options;
72
- tmux;
73
73
  control;
74
74
  constructor(options) {
75
75
  this.options = options;
76
- this.tmux = options.tmux ?? new Tmux();
77
76
  this.control = options.control ?? controlRequest;
78
77
  }
79
78
  async list() {
@@ -106,6 +105,16 @@ export class FleetQueryService {
106
105
  const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
107
106
  const session = await this.session(role, dir, live.state);
108
107
  const problems = [...role.problems];
108
+ const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
109
+ if (recovery && recovery.state !== 'recovered') {
110
+ const paths = ['agent', 'owner']
111
+ .filter(name => recovery.paths[name].state !== 'recovered')
112
+ .map(name => `${name}:${recovery.paths[name].state}`);
113
+ problems.push({
114
+ code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
115
+ detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
116
+ });
117
+ }
109
118
  if (live.state === 'running' && session.reachability !== 'online')
110
119
  problems.push({
111
120
  code: 'supervisor_session_disagreement', severity: 'warning',
@@ -174,26 +183,6 @@ export class FleetQueryService {
174
183
  };
175
184
  }
176
185
  }
177
- if (intended === 'tmux' || role.detectedBackend === 'tmux') {
178
- try {
179
- const has = await this.tmux.has(role.id);
180
- return {
181
- backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
182
- readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
183
- evidence: 'inferred',
184
- // tmux exposes no agent-side evidence at all, and `readiness: idle`
185
- // here is a pane-liveness inference, not an activity claim.
186
- activity: { state: 'unobservable' },
187
- };
188
- }
189
- catch (error) {
190
- return {
191
- backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
192
- evidence: 'inferred', lastError: clean(error.message),
193
- activity: { state: 'unobservable' },
194
- };
195
- }
196
- }
197
186
  return {
198
187
  backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
199
188
  readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
@@ -5,21 +5,14 @@ import type { OpsDeps } from '../ops.js';
5
5
  import type { ResolvedRole } from '../config.js';
6
6
  import { FleetError } from './errors.js';
7
7
  import { type ManagedFleetSpawnResult } from '../fleet-proxy.js';
8
- import { type HarnessModelCatalog, type HarnessModelOption } from './model-catalog.js';
9
8
  export interface CreateRoleSessionRequest {
10
9
  name: string;
11
- harness: 'codex' | 'claude-code';
12
- /** null means the selected harness's own default; web blank fields send null. */
13
- model?: string | null;
14
- reasoningEffort?: string | null;
15
- session: 'acp' | 'tmux';
10
+ brain: import('../config.js').AgentSelection;
11
+ role: import('../config.js').AgentSelection;
16
12
  cwd?: string;
17
13
  lifetime: 'permanent' | 'temporary';
18
- mission?: string;
19
14
  coordinator?: string;
20
15
  permissions: CommonPermissions;
21
- bio?: string;
22
- persona?: string;
23
16
  monitor?: WebCreationMonitor;
24
17
  openAfterCreate: boolean;
25
18
  highRiskAcknowledged?: boolean;
@@ -38,17 +31,6 @@ export type WebCreationMonitor = {
38
31
  export interface CreationCapabilities {
39
32
  available: boolean;
40
33
  reasons: string[];
41
- harnesses: Array<{
42
- id: 'codex' | 'claude-code';
43
- available: boolean;
44
- sessions: Array<'acp' | 'tmux'>;
45
- defaultModel?: string;
46
- models: string[];
47
- modelOptions: HarnessModelOption[];
48
- catalogSource: string;
49
- customModelAllowed: true;
50
- warnings: string[];
51
- }>;
52
34
  lifetimes: Array<'permanent' | 'temporary'>;
53
35
  identityBootstrap: {
54
36
  mode: 'current-fleet-first-boot';
@@ -71,7 +53,7 @@ export interface CreationPreview {
71
53
  name: string;
72
54
  identity: string;
73
55
  harness: string;
74
- session: 'acp' | 'tmux';
56
+ session: 'acp';
75
57
  model?: string;
76
58
  reasoningEffort?: string;
77
59
  cwd?: string;
@@ -95,7 +77,7 @@ export interface CreationAction {
95
77
  actionId: string;
96
78
  requestHash: string;
97
79
  roleId: string;
98
- session: 'acp' | 'tmux';
80
+ session: 'acp';
99
81
  lifetime: 'permanent' | 'temporary';
100
82
  state: CreationStage;
101
83
  stages: Array<{
@@ -121,9 +103,8 @@ export interface RoleCreationServiceOptions {
121
103
  tempLauncher?: SupervisorLauncher;
122
104
  allowedCwdRoots?: string[];
123
105
  journalDir?: string;
124
- probeReady?: (name: string, session: 'acp' | 'tmux') => Promise<'ready' | 'attention' | 'unknown'>;
106
+ probeReady?: (name: string, session: 'acp') => Promise<'ready' | 'attention' | 'unknown'>;
125
107
  onProgress?: (action: CreationAction) => void;
126
- modelCatalogs?: Partial<Record<'codex' | 'claude-code', () => HarnessModelCatalog>>;
127
108
  /** Direct/managed callers must not create or restore the web action journal. */
128
109
  journal?: boolean;
129
110
  }
@@ -1,16 +1,15 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
3
3
  import { isAbsolute, relative } from 'node:path';
4
- import { loadConfig, NOTIFY_EVENT_TYPES, resolveMonitorConfig, resolveRoleModel, resolvePermissions, ROLE_NAME_RE, validateMonitorConfig, } from '../config.js';
4
+ import { loadConfig, NOTIFY_EVENT_TYPES, resolveMonitorConfig, resolvePermissions, ROLE_NAME_RE, validateMonitorConfig, } from '../config.js';
5
5
  import { daemonIdentityProvisioner, } from '../creation.js';
6
6
  import { replaceFileAtomically } from '../atomic-file.js';
7
7
  import { stateRoot } from '../paths.js';
8
- import { buildRoleConfig, spawnDryRun, spawnPermanent, spawnTemp, validateSpawnOpts, } from '../spawn.js';
8
+ import { spawnDryRun, spawnPermanent, spawnTemp, validateSpawnOpts, } from '../spawn.js';
9
9
  import { FleetError, normalizeError } from './errors.js';
10
10
  import { inheritCallerSpawnDefaults } from '../fleet-proxy.js';
11
11
  import { effectivePermissionMode } from '../permissions.js';
12
12
  import { effectiveRoleModel } from '../model-env.js';
13
- import { claudeModelCatalog, codexModelCatalog, } from './model-catalog.js';
14
13
  const canonical = (value) => {
15
14
  if (Array.isArray(value))
16
15
  return `[${value.map(canonical).join(',')}]`;
@@ -62,6 +61,16 @@ export class RoleCreationService {
62
61
  const creationActionId = randomUUID();
63
62
  plan.options.creationActionId = creationActionId;
64
63
  const statePath = await this.launchSync(plan.options);
64
+ const selectionSummary = (kind) => {
65
+ const value = plan.options[kind];
66
+ const provenance = plan.inherited.includes(kind) ? 'inherited' : 'explicit';
67
+ if (!value)
68
+ return `unresolved (${provenance})`;
69
+ if ('ref' in value)
70
+ return `ref:${value.ref} (${provenance})`;
71
+ const fingerprint = hash(value.inline).slice(0, 16);
72
+ return `inline:sha256:${fingerprint} (${provenance})`;
73
+ };
65
74
  return {
66
75
  caller: plan.caller, role: plan.options.name,
67
76
  lifetime: plan.options.temp ? 'temporary' : 'permanent', statePath,
@@ -70,6 +79,7 @@ export class RoleCreationService {
70
79
  monitor: { mode: plan.preview.resolvedRole.monitor.mode, interrupt: plan.preview.resolvedRole.monitor.interrupt },
71
80
  permissionMode: effectivePermissionMode(plan.preview.resolvedRole), inherited: plan.inherited,
72
81
  creationActionId,
82
+ brainSummary: selectionSummary('brain'), roleSummary: selectionSummary('role'),
73
83
  };
74
84
  }
75
85
  launchSync(options, creation) {
@@ -80,47 +90,16 @@ export class RoleCreationService {
80
90
  async capabilities() {
81
91
  const reasons = [];
82
92
  let defaults = {};
83
- let roles = [];
84
93
  try {
85
94
  const config = loadConfig(this.options.configPath);
86
95
  defaults = config.defaults;
87
- roles = config.roles;
88
96
  }
89
97
  catch (error) {
90
98
  reasons.push(`configuration is invalid: ${error.message}`);
91
99
  }
92
- const modelsFor = (harness, catalog) => {
93
- const configured = roles.filter(role => role.harness === harness)
94
- .flatMap(role => [role.model, ...(role.model_chain ?? [])])
95
- .filter((model) => Boolean(model));
96
- const inherited = resolveRoleModel(undefined, harness, defaults);
97
- const configuredOptions = [...new Set([...(inherited ? [inherited] : []), ...configured])]
98
- .filter(id => !catalog.models.some(model => model.id === id))
99
- .map(id => ({ id, label: `${id} (configured)`, reasoningEfforts: [], source: 'fleet-config' }));
100
- const modelOptions = [...configuredOptions, ...catalog.models];
101
- return { modelOptions, models: modelOptions.map(model => model.id) };
102
- };
103
- const codexCatalog = this.options.modelCatalogs?.codex?.() ?? codexModelCatalog();
104
- const claudeCatalog = this.options.modelCatalogs?.['claude-code']?.() ?? claudeModelCatalog();
105
- const codexModels = modelsFor('codex', codexCatalog);
106
- const claudeModels = modelsFor('claude-code', claudeCatalog);
107
100
  return {
108
101
  available: reasons.length === 0,
109
102
  reasons,
110
- harnesses: [
111
- {
112
- id: 'codex', available: true, sessions: ['acp', 'tmux'],
113
- defaultModel: resolveRoleModel(undefined, 'codex', defaults),
114
- ...codexModels, catalogSource: 'codex-runtime-catalog', customModelAllowed: true,
115
- warnings: codexCatalog.warnings,
116
- },
117
- {
118
- id: 'claude-code', available: true, sessions: ['acp', 'tmux'],
119
- defaultModel: resolveRoleModel(undefined, 'claude-code', defaults),
120
- ...claudeModels, catalogSource: 'claude-adapter-2.1', customModelAllowed: true,
121
- warnings: claudeCatalog.warnings,
122
- },
123
- ],
124
103
  lifetimes: ['permanent', 'temporary'],
125
104
  identityBootstrap: {
126
105
  mode: 'current-fleet-first-boot',
@@ -144,14 +123,12 @@ export class RoleCreationService {
144
123
  const defaults = cfg.defaults;
145
124
  const opts = this.spawnOptions(request);
146
125
  validateSpawnOpts(opts);
147
- buildRoleConfig(opts, defaults.harness);
126
+ const resolved = spawnDryRun(opts).resolvedRole;
148
127
  const cwd = request.cwd ? this.resolveCwd(request.cwd) : undefined;
149
128
  const effective = {
150
129
  name: request.name, identity: request.name,
151
- harness: request.harness ?? defaults.harness ?? 'claude-code',
152
- session: request.session ?? defaults.session ?? 'tmux',
153
- model: resolveRoleModel(request.model, request.harness, defaults),
154
- reasoningEffort: request.reasoningEffort ?? undefined,
130
+ harness: resolved.harness, session: resolved.session, model: resolved.model,
131
+ reasoningEffort: resolved.effort,
155
132
  cwd, lifetime: request.lifetime,
156
133
  permissions: resolvePermissions(defaults.permissions, request.permissions),
157
134
  monitor: resolveMonitorConfig(defaults.monitor, request.monitor),
@@ -183,9 +160,7 @@ export class RoleCreationService {
183
160
  if (existingIdentity === 'unknown' && !request.unverifiedIdentityAcknowledged)
184
161
  prerequisites.push('confirm creation with an unverified identity preflight');
185
162
  const provenance = {
186
- harness: 'request', session: 'request', identity: 'built-in',
187
- model: request.model !== undefined ? 'request'
188
- : resolveRoleModel(undefined, request.harness, defaults) ? 'fleet-default' : 'built-in',
163
+ brain: 'request', role: 'request', identity: 'built-in',
189
164
  cwd: request.cwd ? 'request' : 'built-in', permissions: 'request',
190
165
  monitor: request.monitor ? 'request' : defaults.monitor ? 'fleet-default' : 'built-in',
191
166
  };
@@ -264,9 +239,7 @@ export class RoleCreationService {
264
239
  this.stage(action, 'waiting_for_session');
265
240
  const ready = await this.waitForReady(action.roleId, action.session);
266
241
  if (ready === 'ready') {
267
- action.openPath = action.session === 'acp'
268
- ? `/roles/${encodeURIComponent(action.roleId)}/activity`
269
- : `/roles/${encodeURIComponent(action.roleId)}/terminal`;
242
+ action.openPath = `/roles/${encodeURIComponent(action.roleId)}/activity`;
270
243
  this.stage(action, 'session_reachable', 'session is reachable; identity binding has no structured evidence in this version');
271
244
  }
272
245
  else if (ready === 'attention') {
@@ -298,8 +271,8 @@ export class RoleCreationService {
298
271
  }
299
272
  validate(input) {
300
273
  const allowed = new Set([
301
- 'name', 'harness', 'model', 'reasoningEffort', 'session', 'cwd', 'lifetime', 'mission',
302
- 'coordinator', 'permissions', 'bio', 'persona', 'monitor', 'openAfterCreate',
274
+ 'name', 'brain', 'role', 'cwd', 'lifetime',
275
+ 'coordinator', 'permissions', 'monitor', 'openAfterCreate',
303
276
  'highRiskAcknowledged', 'reuseExistingIdentityAcknowledged',
304
277
  'unverifiedIdentityAcknowledged',
305
278
  ]);
@@ -309,20 +282,11 @@ export class RoleCreationService {
309
282
  throw new FleetError('invalid_request', `unsupported web creation field: ${unsupported[0]}`);
310
283
  if (!ROLE_NAME_RE.test(input.name))
311
284
  throw new FleetError('invalid_request', 'invalid role name');
312
- if (!['codex', 'claude-code'].includes(input.harness))
313
- throw new FleetError('invalid_request', 'unsupported harness');
314
- if (!['acp', 'tmux'].includes(input.session))
315
- throw new FleetError('invalid_request', 'unsupported session backend');
285
+ if (!input.brain || !input.role)
286
+ throw new FleetError('invalid_request', 'brain and role selections are required');
316
287
  if (!['permanent', 'temporary'].includes(input.lifetime))
317
288
  throw new FleetError('invalid_request', 'unsupported lifetime');
318
- bounded(input.model ?? undefined, 'model', 128);
319
- bounded(input.reasoningEffort ?? undefined, 'reasoning effort', 16);
320
- if (input.reasoningEffort != null && !['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(input.reasoningEffort))
321
- throw new FleetError('invalid_request', 'unsupported reasoning effort');
322
- bounded(input.mission, 'mission', 4_096);
323
289
  bounded(input.coordinator, 'coordinator', 128);
324
- bounded(input.bio, 'bio', 8_192);
325
- bounded(input.persona, 'persona', 16_384);
326
290
  resolvePermissions(undefined, input.permissions);
327
291
  if (input.monitor) {
328
292
  const problems = validateMonitorConfig(input.monitor);
@@ -332,10 +296,8 @@ export class RoleCreationService {
332
296
  throw new FleetError('invalid_request', 'web creation supports monitor.inject=notification only');
333
297
  }
334
298
  return {
335
- ...input, model: input.model === null ? null : input.model?.trim() || undefined,
336
- reasoningEffort: input.reasoningEffort === null ? null : input.reasoningEffort?.trim() || undefined,
337
- mission: input.mission?.trim() || undefined, coordinator: input.coordinator?.trim() || undefined,
338
- bio: input.bio?.trim() || undefined, persona: input.persona?.trim() || undefined,
299
+ ...input, brain: structuredClone(input.brain), role: structuredClone(input.role),
300
+ coordinator: input.coordinator?.trim() || undefined,
339
301
  monitor: input.monitor ? structuredClone(input.monitor) : undefined,
340
302
  };
341
303
  }
@@ -362,12 +324,10 @@ export class RoleCreationService {
362
324
  spawnOptions(request, creationActionId) {
363
325
  return {
364
326
  name: request.name, temp: request.lifetime === 'temporary',
365
- identity: request.name, harness: request.harness,
366
- model: request.model, session: request.session, cwd: request.cwd,
367
- reasoningEffort: request.reasoningEffort,
368
- mission: request.mission, coordinator: request.coordinator,
327
+ identity: request.name, brain: request.brain, role: request.role, cwd: request.cwd,
328
+ coordinator: request.coordinator,
369
329
  approval: request.permissions.approval, filesystem: request.permissions.filesystem,
370
- unattended: request.permissions.unattended, bio: request.bio, persona: request.persona,
330
+ unattended: request.permissions.unattended,
371
331
  monitorConfig: request.monitor,
372
332
  configPath: this.options.configPath,
373
333
  surface: creationActionId ? 'web' : undefined, creationActionId,
@@ -4,6 +4,7 @@ import { loadConfig, ROLE_NAME_RE } from '../config.js';
4
4
  import { agentDir, stateRoot } from '../paths.js';
5
5
  import { rmRole } from '../ops.js';
6
6
  import { FleetError } from './errors.js';
7
+ import { provesGeneratedAgentSource } from '../generated-agent-source.js';
7
8
  export class RoleRemovalService {
8
9
  options;
9
10
  constructor(options) {
@@ -45,8 +46,9 @@ export class RoleRemovalService {
45
46
  `Stop and uninstall the exact backend registration for '${requested}'.`,
46
47
  `Archive then remove ${lifetime === 'temporary' ? temporaryState : permanentState}.`,
47
48
  ];
48
- if (role?.sourceFile.includes('/fleet.d/'))
49
- effects.push(`Remove spawned configuration ${role.sourceFile}.`);
49
+ const generatedSource = role && provesGeneratedAgentSource(agentDir(requested), cfg.files[0], role.sourceFile);
50
+ if (generatedSource)
51
+ effects.push(`Remove generated Agent configuration ${role.sourceFile}.`);
50
52
  else if (role)
51
53
  effects.push(`Keep hand-written configuration ${role.sourceFile}; the role can be recreated from it.`);
52
54
  else
@@ -76,7 +78,8 @@ export class RoleRemovalService {
76
78
  mkdirSync(recoveryPath, { recursive: true, mode: 0o700 });
77
79
  if (existsSync(sourceState))
78
80
  cpSync(sourceState, join(recoveryPath, 'state'), { recursive: true });
79
- if (role?.sourceFile.includes('/fleet.d/') && existsSync(role.sourceFile))
81
+ if (role && provesGeneratedAgentSource(agentDir(preview.role), cfg.files[0], role.sourceFile)
82
+ && existsSync(role.sourceFile))
80
83
  cpSync(role.sourceFile, join(recoveryPath, basename(role.sourceFile)));
81
84
  if (role || preview.lifetime === 'temporary')
82
85
  await rmRole(cfg, preview.role, this.options.ops);
@@ -3,9 +3,8 @@ export interface RoleRepositoryOptions {
3
3
  configPath?: string;
4
4
  permanentRoot?: string;
5
5
  temporaryRoot?: string;
6
- probeBackend?: (name: string, intended?: 'acp' | 'tmux') => Promise<{
6
+ probeBackend?: (name: string, intended?: 'acp') => Promise<{
7
7
  acp: boolean;
8
- tmux: boolean;
9
8
  }>;
10
9
  concurrency?: number;
11
10
  timeoutMs?: number;
@@ -42,7 +42,11 @@ function snapshot(path, name) {
42
42
  const raw = parse(readFileSync(path, 'utf8'));
43
43
  if (!raw || typeof raw !== 'object')
44
44
  throw new Error('snapshot must be a mapping');
45
- const session = raw.session === 'acp' || raw.session === 'tmux' ? raw.session : 'tmux';
45
+ if (raw.session === 'tmux')
46
+ throw new Error('session: tmux is no longer supported; use session: acp with the Codex or Claude Code adapter');
47
+ if (raw.session !== undefined && raw.session !== 'acp')
48
+ throw new Error(`unsupported session '${String(raw.session)}'; expected session: acp`);
49
+ const session = 'acp';
46
50
  return {
47
51
  role: {
48
52
  ...raw, name, harness: typeof raw.harness === 'string' ? raw.harness : 'claude-code',
@@ -167,14 +171,10 @@ export class RoleRepository {
167
171
  const timeoutMs = this.options.timeoutMs ?? 2_000;
168
172
  const result = await Promise.race([
169
173
  this.options.probeBackend(name, intended),
170
- new Promise(resolve => setTimeout(() => resolve({ acp: false, tmux: false }), timeoutMs)),
171
- ]).catch(() => ({ acp: false, tmux: false }));
172
- if (result.acp && result.tmux)
173
- return 'ambiguous';
174
+ new Promise(resolve => setTimeout(() => resolve({ acp: false }), timeoutMs)),
175
+ ]).catch(() => ({ acp: false }));
174
176
  if (result.acp)
175
177
  return 'acp';
176
- if (result.tmux)
177
- return 'tmux';
178
178
  return 'none';
179
179
  }
180
180
  }
@@ -1,7 +1,6 @@
1
1
  import { controlRequest } from '../session/control.js';
2
2
  import type { ConversationEventV1, ConversationSnapshot, PromptReceipt } from '../session/conversation-types.js';
3
3
  import type { SessionSnapshot } from '../session/types.js';
4
- import { Tmux } from '../tmux.js';
5
4
  import type { OutputPage, SendReceipt, SessionDescriptor } from './types.js';
6
5
  export interface ConversationPageView {
7
6
  events: ConversationEventV1[];
@@ -120,14 +119,3 @@ export declare class AcpRoleSessionAdapter implements RoleSessionControl {
120
119
  }>;
121
120
  private call;
122
121
  }
123
- export declare class TmuxRoleSessionAdapter implements RoleSessionControl {
124
- private readonly roleId;
125
- private readonly tmux;
126
- constructor(roleId: string, tmux?: Tmux);
127
- describe(): Promise<SessionDescriptor>;
128
- snapshot(): Promise<SessionSnapshot>;
129
- recentOutput(request?: {
130
- limit?: number;
131
- }): Promise<OutputPage>;
132
- sendText(text: string): Promise<SendReceipt>;
133
- }
@@ -1,6 +1,4 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { controlRequest, followConversation } from '../session/control.js';
3
- import { Tmux } from '../tmux.js';
4
2
  import { FleetError, normalizeError } from './errors.js';
5
3
  const visibleEvents = (events) => events.filter(event => event.kind !== 'thought');
6
4
  export class AcpRoleSessionAdapter {
@@ -149,43 +147,3 @@ export class AcpRoleSessionAdapter {
149
147
  }
150
148
  }
151
149
  }
152
- export class TmuxRoleSessionAdapter {
153
- roleId;
154
- tmux;
155
- constructor(roleId, tmux = new Tmux()) {
156
- this.roleId = roleId;
157
- this.tmux = tmux;
158
- }
159
- async describe() {
160
- return { backend: 'tmux', protocolVersion: 1, features: ['text', 'capture'] };
161
- }
162
- async snapshot() {
163
- const alive = await this.tmux.has(this.roleId);
164
- return { backend: 'tmux', alive, readiness: alive ? 'idle' : 'failed' };
165
- }
166
- async recentOutput(request = {}) {
167
- try {
168
- const text = await this.tmux.capture(this.roleId, Math.min(request.limit ?? 100, 500));
169
- return { events: [], text, truncated: false };
170
- }
171
- catch (error) {
172
- throw normalizeError(error);
173
- }
174
- }
175
- async sendText(text) {
176
- if (!text.trim())
177
- throw new FleetError('invalid_request', 'text is required');
178
- if (Buffer.byteLength(text) > 32 * 1024)
179
- throw new FleetError('invalid_request', 'text exceeds 32 KiB');
180
- try {
181
- await this.tmux.sendText(this.roleId, text);
182
- }
183
- catch (error) {
184
- throw normalizeError(error);
185
- }
186
- return {
187
- accepted: true, promptId: randomUUID(), queuedBehind: 0, terminalOutcomeKnown: false,
188
- detail: 'literal text and Enter sent; terminal outcome is unknown',
189
- };
190
- }
191
- }
@@ -1,7 +1,7 @@
1
- import type { InterruptOutcome, QueuedPrompt, SessionHandle, SubmitPromptOptions, TurnCancellationSource } from '../session/types.js';
1
+ import type { AgentSession, InterruptOutcome, QueuedPrompt, SubmitPromptOptions, TurnCancellationSource } from '../session/types.js';
2
2
  /** Thin in-process session mutation kernel. Callers retain every policy decision. */
3
- export declare const queueSessionPrompt: (session: SessionHandle, text: string, options?: SubmitPromptOptions) => Promise<QueuedPrompt>;
4
- export declare const interruptSession: (session: SessionHandle, source: TurnCancellationSource) => Promise<InterruptOutcome>;
5
- export declare const respondSessionPermission: (session: SessionHandle, permissionId: string, optionId: string) => boolean;
3
+ export declare const queueSessionPrompt: (session: AgentSession, text: string, options?: SubmitPromptOptions) => Promise<QueuedPrompt>;
4
+ export declare const interruptSession: (session: AgentSession, source: TurnCancellationSource) => Promise<InterruptOutcome>;
5
+ export declare const respondSessionPermission: (session: AgentSession, permissionId: string, optionId: string) => boolean;
6
6
  export type GenerationPermissionResult = 'accepted' | 'stale' | 'unavailable';
7
- export declare const respondSessionPermissionV2: (session: SessionHandle, permissionId: string, optionId: string, sessionGeneration: string) => GenerationPermissionResult;
7
+ export declare const respondSessionPermissionV2: (session: AgentSession, permissionId: string, optionId: string, sessionGeneration: string) => GenerationPermissionResult;
@@ -179,7 +179,7 @@ export declare class TaskRoomApplicationService {
179
179
  name: string;
180
180
  version: number;
181
181
  description: string;
182
- builtin?: boolean;
182
+ sourceFile?: string;
183
183
  room?: import("../rooms-tasks/types.js").TemplateRoomConfig;
184
184
  contract?: string;
185
185
  members: import("../rooms-tasks/types.js").TemplateMemberSlot[];