@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,14 +1,15 @@
1
1
  import { type Socket } from 'node:net';
2
- import type { ControlFailureKind, SessionEvent, SessionHandle, SessionSnapshot } from './types.js';
2
+ import type { AgentSession, ControlFailureKind, SessionEvent, SessionSnapshot } from './types.js';
3
3
  import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
4
4
  import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
5
5
  import type { SpawnOpts } from '../spawn.js';
6
6
  import type { ManagedFleetSpawnResult } from '../fleet-proxy.js';
7
+ import { type FleetAuditPresentation, type FleetCommandOutcomeClass } from '../fleet-command-audit.js';
7
8
  export interface ControlRequest {
8
9
  version: 1 | 2 | 3;
9
10
  id: string;
10
11
  token: string;
11
- command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn';
12
+ command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_finish';
12
13
  text?: string;
13
14
  permissionId?: string;
14
15
  optionId?: string;
@@ -30,6 +31,16 @@ export interface ControlRequest {
30
31
  sessionGeneration?: string;
31
32
  /** Typed managed-spawn request accepted only over the authenticated role control plane. */
32
33
  spawn?: SpawnOpts;
34
+ audit?: {
35
+ requestId?: string;
36
+ argv?: string[];
37
+ correlationId?: string;
38
+ class?: FleetCommandOutcomeClass;
39
+ exitCode?: number;
40
+ effect?: 'not_started' | 'completed' | 'unknown';
41
+ resourceIds?: Record<string, string>;
42
+ presentations?: FleetAuditPresentation[];
43
+ };
33
44
  }
34
45
  export interface ControlResponse {
35
46
  version: 1;
@@ -48,7 +59,7 @@ export interface RetainedEventPage {
48
59
  truncated: boolean;
49
60
  }
50
61
  /** The one retained-range projection shared by polling and live-follow admission. */
51
- export declare function retainedEventPage(session: SessionHandle, since: number): RetainedEventPage;
62
+ export declare function retainedEventPage(session: AgentSession, since: number): RetainedEventPage;
52
63
  /**
53
64
  * One line saying what a control failure does — and does not — prove about the
54
65
  * agent. Only `offline` is evidence that it is gone; every other kind used to
@@ -105,7 +116,8 @@ export declare class RoleControlServer {
105
116
  private loopManager?;
106
117
  private reloadConfig?;
107
118
  private fleetSpawner?;
108
- constructor(stateDir: string, session: SessionHandle, log: (line: string) => void);
119
+ private fleetAuditor?;
120
+ constructor(stateDir: string, session: AgentSession, log: (line: string) => void);
109
121
  start(): Promise<void>;
110
122
  close(): Promise<void>;
111
123
  /** Attach only the already-started supervisor-owned channel client. */
@@ -113,6 +125,7 @@ export declare class RoleControlServer {
113
125
  setLoopManager(loopManager: ScheduledLoopManagerHandle | undefined): void;
114
126
  setConfigReloader(reloadConfig: (() => Promise<unknown>) | undefined): void;
115
127
  setFleetSpawner(fleetSpawner: ((options: SpawnOpts) => Promise<ManagedFleetSpawnResult>) | undefined): void;
128
+ setFleetAuditor(auditor: RoleControlServer['fleetAuditor']): void;
116
129
  private accept;
117
130
  private handle;
118
131
  /** Version and capability gate shared by every conversation v3 command. */
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node
3
3
  import { createConnection, createServer } from 'node:net';
4
4
  import { join } from 'node:path';
5
5
  import { SessionControlError } from './types.js';
6
+ import { validateFleetAuditBegin, validateFleetAuditFinish, } from '../fleet-command-audit.js';
6
7
  import { interruptSession, queueSessionPrompt, respondSessionPermission, respondSessionPermissionV2, } from '../application/session-mutations.js';
7
8
  const MAX_LINE_BYTES = 64 * 1024;
8
9
  /** Commands that require protocol version 3. */
@@ -119,6 +120,7 @@ export class RoleControlServer {
119
120
  loopManager;
120
121
  reloadConfig;
121
122
  fleetSpawner;
123
+ fleetAuditor;
122
124
  constructor(stateDir, session, log) {
123
125
  this.session = session;
124
126
  this.log = log;
@@ -164,6 +166,7 @@ export class RoleControlServer {
164
166
  setFleetSpawner(fleetSpawner) {
165
167
  this.fleetSpawner = fleetSpawner;
166
168
  }
169
+ setFleetAuditor(auditor) { this.fleetAuditor = auditor; }
167
170
  accept(socket) {
168
171
  this.sockets.add(socket);
169
172
  socket.setEncoding('utf8');
@@ -311,6 +314,38 @@ export class RoleControlServer {
311
314
  this.write(socket, { version: 1, id: request.id, ok: true, result });
312
315
  return;
313
316
  }
317
+ case 'fleet_audit_begin': {
318
+ if (!this.fleetAuditor)
319
+ throw new SessionControlError('rejected', 'fleet auditing is unavailable');
320
+ try {
321
+ validateFleetAuditBegin(request.audit);
322
+ }
323
+ catch {
324
+ throw new SessionControlError('rejected', 'valid fleet audit begin fields are required');
325
+ }
326
+ const result = await this.fleetAuditor.begin(request.audit.requestId, request.audit.argv);
327
+ this.write(socket, { version: 1, id: request.id, ok: true, result });
328
+ return;
329
+ }
330
+ case 'fleet_audit_finish': {
331
+ if (!this.fleetAuditor)
332
+ throw new SessionControlError('rejected', 'fleet auditing is unavailable');
333
+ try {
334
+ validateFleetAuditFinish(request.audit);
335
+ }
336
+ catch {
337
+ throw new SessionControlError('rejected', 'valid fleet audit finish fields are required');
338
+ }
339
+ const audit = request.audit;
340
+ const result = await this.fleetAuditor.finish({
341
+ correlationId: audit.correlationId, class: audit.class, effect: audit.effect,
342
+ ...(audit.exitCode === undefined ? {} : { exitCode: audit.exitCode }),
343
+ ...(audit.resourceIds ? { resourceIds: audit.resourceIds } : {}),
344
+ ...(audit.presentations ? { presentations: audit.presentations } : {}),
345
+ });
346
+ this.write(socket, { version: 1, id: request.id, ok: true, result });
347
+ return;
348
+ }
314
349
  case 'owner_channel_manage': {
315
350
  if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
316
351
  throw new SessionControlError('rejected', 'owner-channel management action is required');
@@ -96,7 +96,7 @@ export interface InterruptOutcome {
96
96
  reasonCode?: string;
97
97
  }
98
98
  /**
99
- * What a `SessionHandle.interrupt` implementation may resolve. Before 0.17.1 the
99
+ * What an `AgentSession.interrupt` implementation may resolve. Before 0.17.1 the
100
100
  * contract was `Promise<void>`, and resolving at all meant the cancellation had
101
101
  * taken effect cooperatively — so an implementation written against that
102
102
  * contract stays valid and keeps its exact meaning. Only in-tree consumers read
@@ -146,7 +146,7 @@ export interface ExitRecord {
146
146
  code?: number;
147
147
  /** Signal that killed it, when one did. */
148
148
  signal?: string;
149
- /** Raw wait status as the pane shell saw it (tmux only). */
149
+ /** Raw wait status retained when migrating a legacy shell-exit record. */
150
150
  status?: number;
151
151
  at?: string;
152
152
  /** One line an operator can read. */
@@ -194,8 +194,7 @@ export interface SessionSnapshot {
194
194
  };
195
195
  /**
196
196
  * Observed agent activity, independent of turn occupancy: the evidence a
197
- * human-facing surface needs before calling a role idle. Absent on backends
198
- * that cannot observe the agent at all (tmux), which is itself honest — no
197
+ * human-facing surface needs before calling a role idle. When absent, no
199
198
  * evidence is not evidence of inactivity.
200
199
  */
201
200
  activity?: SessionActivity;
@@ -259,7 +258,12 @@ export interface ConversationHandlePage {
259
258
  hasMore: boolean;
260
259
  snapshot: ConversationSnapshot;
261
260
  }
262
- export interface SessionHandle {
261
+ /**
262
+ * The live, harness-neutral contract between Fleet and one agent session.
263
+ * Harness adapters construct this handle; orchestration code must not depend
264
+ * on the ACP transport which currently implements it.
265
+ */
266
+ export interface AgentSession {
263
267
  readonly backend: SessionBackendId;
264
268
  readonly pid: number;
265
269
  isAlive(): boolean;
@@ -301,3 +305,5 @@ export interface SessionHandle {
301
305
  exitResult(): ExitRecord | null;
302
306
  close(): Promise<void>;
303
307
  }
308
+ /** @deprecated Public compatibility name; use AgentSession. */
309
+ export type SessionHandle = AgentSession;
package/dist/spawn.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { IsolationConfig } from './isolation/types.js';
2
- import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type RoleConfig, type MonitorConfig, type SessionBackendId, type UnattendedMode, type RoomMemberStartup } from './config.js';
2
+ import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type MonitorConfig, type UnattendedMode, type RoomMemberStartup, type AgentDefinition, type AgentSelection } from './config.js';
3
3
  import { type OpsDeps } from './ops.js';
4
4
  import { type CreationDeps, type CreationProvenance } from './creation.js';
5
5
  import './harness/claude-code.js';
@@ -13,34 +13,18 @@ export declare let lastProvenance: CreationProvenance | undefined;
13
13
  export interface SpawnOpts {
14
14
  name: string;
15
15
  temp?: boolean;
16
- harness?: string;
17
- session?: SessionBackendId;
18
- mission?: string;
19
- missionFile?: string;
16
+ brain?: AgentSelection;
17
+ role?: AgentSelection;
18
+ /** Trusted canonical definition used by room provisioning; never a second schema. */
19
+ agentDefinition?: AgentDefinition;
20
20
  identity?: string;
21
21
  cwd?: string;
22
22
  coordinator?: string;
23
- /** null explicitly selects the harness default; undefined retains normal fleet inheritance. */
24
- model?: string | null;
25
- permissionMode?: string;
26
23
  approval?: ApprovalMode;
27
24
  filesystem?: FilesystemMode;
28
25
  unattended?: UnattendedMode;
29
- sandbox?: string;
30
- profile?: string;
31
- launcher?: string;
32
- search?: boolean;
33
- codexConfig?: Record<string, string | number | boolean>;
34
- reasoningEffort?: string | null;
35
- addDirs?: string[];
36
- monitor?: boolean;
37
26
  /** Typed external monitor configuration used by trusted creation surfaces. */
38
27
  monitorConfig?: Partial<MonitorConfig>;
39
- bioFile?: string;
40
- personaFile?: string;
41
- /** Inline profile values for trusted typed callers such as the local web service. */
42
- bio?: string;
43
- persona?: string;
44
28
  /** Internal, non-sensitive provenance correlation for typed presentation layers. */
45
29
  surface?: 'cli' | 'web' | 'agent';
46
30
  creationActionId?: string;
@@ -61,12 +45,7 @@ export interface SpawnOpts {
61
45
  dryRun?: boolean;
62
46
  json?: boolean;
63
47
  }
64
- export declare function profileValues(o: SpawnOpts): {
65
- bio?: string;
66
- persona?: string;
67
- };
68
- /** Pure option-to-role mapping shared by CLI and application services. */
69
- export declare function buildRoleConfig(o: SpawnOpts, defaultHarness?: string): RoleConfig;
48
+ export declare function agentDefinitionFromSpawn(o: SpawnOpts): AgentDefinition;
70
49
  /**
71
50
  * Read and validate an `--isolation-file`. The file is the existing
72
51
  * `isolation:` mapping and nothing else — the same schema, the same validator
@@ -78,16 +57,12 @@ export declare function buildRoleConfig(o: SpawnOpts, defaultHarness?: string):
78
57
  */
79
58
  export declare function readIsolationFile(path: string): IsolationConfig;
80
59
  export declare function validateSpawnOpts(o: SpawnOpts): void;
81
- /** Read mission text without trimming or newline rewriting. */
82
- export declare function readMissionFile(path: string): string;
83
60
  /** The ours identity a spawn will bind: explicit, else the role name. */
84
61
  export declare const effectiveIdentity: (o: SpawnOpts) => string;
85
62
  export interface SpawnDryRun {
86
63
  schemaVersion: 1;
87
64
  warning: string;
88
- roleDocument: {
89
- roles: Record<string, RoleConfig>;
90
- };
65
+ roleDocument: Record<string, unknown>;
91
66
  resolvedRole: ResolvedRole;
92
67
  }
93
68
  /**
@@ -95,7 +70,7 @@ export interface SpawnDryRun {
95
70
  * or writing state. Collision checks are necessarily a point-in-time snapshot.
96
71
  */
97
72
  export declare function spawnDryRun(o: SpawnOpts): SpawnDryRun;
98
- /** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
73
+ /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
99
74
  export declare function spawnPermanent(o: SpawnOpts, deps: OpsDeps, creation?: CreationDeps): Promise<string>;
100
75
  export type { SupervisorLauncher } from './temp-lifecycle.js';
101
76
  /** Temp spawn: live state under ~/.ours-fleet/tmp, independent transient supervision. */
package/dist/spawn.js CHANGED
@@ -2,98 +2,56 @@ 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';
8
- import { resolveRoleModelEnv } from './model-env.js';
7
+ import { loadConfig, findRole, splitRootFor, validateMonitorConfig, } from './config.js';
9
8
  import { applyRole, up } from './ops.js';
10
9
  import { START_STAGGER_FILE } from './runner.js';
11
10
  import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
12
11
  import { VERSION } from './version.js';
12
+ import { recordGeneratedAgentSource } from './generated-agent-source.js';
13
13
  import './harness/claude-code.js';
14
14
  import './harness/codex.js';
15
- import { getAdapter } from './harness/registry.js';
16
15
  import { archiveTempState, makeTempSupervisorLauncher, prepareTempSupervisor, reclaimStaleTempState, } from './temp-lifecycle.js';
17
16
  /**
18
17
  * The provenance record written by the most recent spawn in this process, so
19
18
  * the CLI can print the same summary it persisted rather than rebuilding it.
20
19
  */
21
20
  export let lastProvenance;
22
- export function profileValues(o) {
23
- if (o.bio !== undefined && o.bioFile)
24
- throw new Error('bio and bioFile are mutually exclusive');
25
- if (o.persona !== undefined && o.personaFile)
26
- throw new Error('persona and personaFile are mutually exclusive');
21
+ export function agentDefinitionFromSpawn(o) {
22
+ if (o.agentDefinition) {
23
+ if (o.brain !== undefined || o.role !== undefined || o.cwd !== undefined
24
+ || o.coordinator !== undefined || o.approval !== undefined || o.filesystem !== undefined
25
+ || o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined)
26
+ throw new Error('canonical agentDefinition conflicts with separate Agent fields');
27
+ const definition = structuredClone(o.agentDefinition);
28
+ if (o.identity)
29
+ definition.identity = o.identity;
30
+ return definition;
31
+ }
32
+ if (!o.role)
33
+ throw new Error('--role is required (declared ID or inline mapping)');
34
+ if (!o.brain)
35
+ throw new Error('--brain is required (declared ID or inline mapping)');
36
+ const permissions = o.approval || o.filesystem || o.unattended ? {
37
+ ...(o.approval ? { approval: o.approval } : {}),
38
+ ...(o.filesystem ? { filesystem: o.filesystem } : {}),
39
+ ...(o.unattended ? { unattended: o.unattended } : {}),
40
+ } : undefined;
27
41
  return {
28
- bio: o.bio !== undefined ? o.bio.trim()
29
- : o.bioFile ? readFileSync(o.bioFile, 'utf8').trim() : undefined,
30
- persona: o.persona !== undefined ? o.persona.trim()
31
- : o.personaFile ? readFileSync(o.personaFile, 'utf8').trim() : undefined,
42
+ role: structuredClone(o.role), brain: structuredClone(o.brain),
43
+ ...(o.identity ? { identity: o.identity } : {}),
44
+ ...(o.cwd ? { cwd: o.cwd } : {}),
45
+ ...(o.coordinator ? { coordinator: o.coordinator } : {}),
46
+ ...(permissions ? { permissions } : {}),
47
+ ...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
48
+ ...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
32
49
  };
33
50
  }
34
- /** Pure option-to-role mapping shared by CLI and application services. */
35
- export function buildRoleConfig(o, defaultHarness) {
36
- const r = {};
37
- if (o.harness)
38
- r.harness = o.harness;
39
- if (o.session)
40
- r.session = o.session;
41
- if (o.identity)
42
- r.identity = o.identity;
43
- if (o.cwd)
44
- r.cwd = o.cwd;
45
- if (o.coordinator)
46
- r.coordinator = o.coordinator;
47
- if (o.missionFile)
48
- r.mission = readMissionFile(o.missionFile);
49
- else if (o.mission !== undefined)
50
- r.mission = o.mission;
51
- if (o.model === null)
52
- r.model = null;
53
- else if (o.model?.trim())
54
- r.model = o.model.trim();
55
- const harness = o.harness ?? defaultHarness;
56
- const harnessOptions = {};
57
- if (o.permissionMode)
58
- harnessOptions[harness === 'claude-code' ? 'permission_mode' : 'approval'] = o.permissionMode;
59
- if (o.sandbox)
60
- harnessOptions.sandbox = o.sandbox;
61
- if (o.profile)
62
- harnessOptions.profile = o.profile;
63
- if (o.launcher)
64
- harnessOptions.launcher = o.launcher;
65
- if (o.search === true)
66
- harnessOptions.search = true;
67
- const codexConfig = { ...(o.codexConfig ?? {}) };
68
- if (o.reasoningEffort && harness === 'codex')
69
- codexConfig.model_reasoning_effort = o.reasoningEffort;
70
- if (Object.keys(codexConfig).length)
71
- harnessOptions.config = codexConfig;
72
- if (o.reasoningEffort && harness === 'claude-code')
73
- harnessOptions.effort = o.reasoningEffort;
74
- if (o.addDirs?.length)
75
- harnessOptions.add_dirs = o.addDirs;
76
- if (o.monitor === true)
77
- harnessOptions.monitor = true;
78
- if (Object.keys(harnessOptions).length)
79
- r.harness_options = harnessOptions;
80
- if (o.approval || o.filesystem || o.unattended) {
81
- r.permissions = {
82
- ...(o.approval ? { approval: o.approval } : {}),
83
- ...(o.filesystem ? { filesystem: o.filesystem } : {}),
84
- ...(o.unattended ? { unattended: o.unattended } : {}),
85
- };
86
- }
87
- const profile = profileValues(o);
88
- if (profile.bio)
89
- r.bio = profile.bio;
90
- if (profile.persona)
91
- r.persona = profile.persona;
92
- if (o.isolationFile)
93
- r.isolation = readIsolationFile(o.isolationFile);
94
- if (o.monitorConfig)
95
- r.monitor = { ...o.monitorConfig };
96
- return r;
51
+ function resolvedSpawn(o) {
52
+ const definition = agentDefinitionFromSpawn(o);
53
+ const cfg = loadConfig(o.configPath, { additionalAgent: { id: o.name, definition } });
54
+ return { definition, role: findRole(cfg, o.name) };
97
55
  }
98
56
  /**
99
57
  * Read and validate an `--isolation-file`. The file is the existing
@@ -121,10 +79,6 @@ export function readIsolationFile(path) {
121
79
  return cfg;
122
80
  }
123
81
  export function validateSpawnOpts(o) {
124
- if (o.mission !== undefined && o.missionFile)
125
- 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`);
128
82
  if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
129
83
  throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
130
84
  if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
@@ -135,23 +89,12 @@ export function validateSpawnOpts(o) {
135
89
  throw new Error(`invalid role name '${o.name}'`);
136
90
  if (o.identity && !/^[A-Za-z0-9_-]+$/.test(o.identity))
137
91
  throw new Error(`invalid identity name '${o.identity}'`);
138
- if (o.model && (o.model.length > 128 || /[\0-\x1f\x7f]/.test(o.model)))
139
- throw new Error('model must be printable and at most 128 characters');
140
92
  if (o.monitorConfig) {
141
93
  const problems = validateMonitorConfig(o.monitorConfig);
142
94
  if (problems.length)
143
95
  throw new Error(problems.join('; '));
144
96
  }
145
97
  }
146
- /** Read mission text without trimming or newline rewriting. */
147
- export function readMissionFile(path) {
148
- try {
149
- return readFileSync(path, 'utf8');
150
- }
151
- catch (e) {
152
- throw new Error(`--mission-file ${path}: ${e.message}`);
153
- }
154
- }
155
98
  /**
156
99
  * Reject names that are already USED. This is a precondition, not a claim: it
157
100
  * runs INSIDE the creation transaction, after both names are reserved, so the
@@ -180,62 +123,13 @@ export function spawnDryRun(o) {
180
123
  validateSpawnOpts(o);
181
124
  if (o.isolationFile)
182
125
  readIsolationFile(o.isolationFile);
183
- if (o.missionFile)
184
- readMissionFile(o.missionFile);
185
126
  assertNameFree(o);
186
- const cfg = loadConfig(o.configPath);
187
- const raw = buildRoleConfig(o, cfg.defaults.harness);
188
- const harnessOptions = {
189
- ...(cfg.defaults.harness_options ?? {}),
190
- ...(raw.harness_options ?? {}),
191
- };
192
- const harness = raw.harness ?? cfg.defaults.harness ?? 'claude-code';
193
- const defaultHarness = cfg.defaults.harness ?? 'claude-code';
194
- const inheritsModelDefaults = harness === defaultHarness && raw.model !== null;
195
- const authProxy = resolveAuthProxy(cfg.defaults.auth_proxy, raw.auth_proxy);
196
- // One resolution for the environment and the model it pins (src/model-env.ts).
197
- const modelEnv = resolveRoleModelEnv({
198
- harness,
199
- model: resolveRoleModel(raw.model, raw.harness, cfg.defaults),
200
- modelWasExplicit: raw.model !== undefined,
201
- defaultsEnv: (cfg.defaults.env ?? {}),
202
- roleEnv: raw.env,
203
- ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
204
- });
205
- const model = modelEnv.model;
206
- const session = raw.session ?? cfg.defaults.session ?? 'tmux';
207
- const resolvedRole = {
208
- ...raw,
209
- name: o.name,
210
- sourceFile: o.temp ? '(temp dry-run)' : join(fleetDDir(), `${o.name}.yaml`),
211
- harness,
212
- session,
213
- session_options: raw.session_options,
214
- permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
215
- permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
216
- identity: effectiveIdentity(o),
217
- model,
218
- model_chain: resolveModelChain(model, raw.model_chain ?? (inheritsModelDefaults
219
- ? cfg.defaults.model_chain
220
- : undefined)),
221
- harness_options: Object.keys(harnessOptions).length ? harnessOptions : undefined,
222
- isolation: raw.isolation ?? cfg.defaults.isolation,
223
- monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
224
- owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
225
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, raw.worklog),
226
- auth_proxy: authProxy,
227
- };
228
- resolvedRole.env = modelEnv.env;
229
- const adapter = getAdapter(resolvedRole.harness);
230
- if (resolvedRole.auth_proxy && resolvedRole.harness !== 'claude-code')
231
- throw new Error('auth_proxy is supported only by claude-code');
232
- const optionProblems = adapter.validateOptions(resolvedRole.harness_options, resolvedRole);
233
- if (optionProblems.length)
234
- throw new Error(optionProblems.map(problem => `${problem.path}: ${problem.message}`).join('; '));
127
+ const resolved = resolvedSpawn(o);
128
+ const resolvedRole = resolved.role;
235
129
  return {
236
130
  schemaVersion: 1,
237
131
  warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
238
- roleDocument: { roles: { [o.name]: raw } },
132
+ roleDocument: structuredClone(resolved.definition),
239
133
  resolvedRole,
240
134
  };
241
135
  }
@@ -251,22 +145,15 @@ function provenanceSettings(o, defaults) {
251
145
  const perms = (defaults.permissions ?? {});
252
146
  const callerDefaults = new Set(o.inheritedFromCaller ?? []);
253
147
  const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
254
- const explicitModel = typeof o.model === 'string' ? o.model.trim() : undefined;
255
- const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
148
+ const selection = (value) => value && 'ref' in value ? `ref:${value.ref}` : value ? 'inline' : undefined;
256
149
  return {
257
- harness: tagged('harness', provenanceOf(o.harness, defaults.harness, 'claude-code')),
258
- session: tagged('session', provenanceOf(o.session, defaults.session, 'tmux')),
150
+ brain: tagged('brain', provenanceOf(selection(o.brain), undefined, undefined)),
151
+ role: tagged('role', provenanceOf(selection(o.role), undefined, undefined)),
259
152
  identity: o.identity
260
153
  ? { value: o.identity, source: 'cli' }
261
154
  : { value: o.name, source: 'built-in' }, // defaults to the role name
262
155
  cwd: tagged('cwd', provenanceOf(o.cwd, undefined, undefined)),
263
- model: tagged('model', o.model === null
264
- ? { value: undefined, source: 'cli' }
265
- : explicitModel
266
- ? { value: explicitModel, source: 'cli' }
267
- : { value: inheritedModel, source: inheritedModel ? 'fleet-default' : 'built-in' }),
268
156
  coordinator: tagged('coordinator', provenanceOf(o.coordinator, undefined, undefined)),
269
- permission_mode: provenanceOf(o.permissionMode, undefined, undefined),
270
157
  approval: tagged('approval', provenanceOf(o.approval, perms.approval, 'ask')),
271
158
  filesystem: tagged('filesystem', provenanceOf(o.filesystem, perms.filesystem, 'workspace')),
272
159
  unattended: tagged('unattended', provenanceOf(o.unattended, perms.unattended, 'deny')),
@@ -276,13 +163,12 @@ function provenanceSettings(o, defaults) {
276
163
  monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
277
164
  };
278
165
  }
279
- /** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
166
+ /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
280
167
  export async function spawnPermanent(o, deps, creation = {}) {
281
168
  validateSpawnOpts(o);
282
169
  if (o.isolationFile)
283
170
  readIsolationFile(o.isolationFile); // fail before reserving
284
- if (o.missionFile)
285
- readMissionFile(o.missionFile); // fail before reserving
171
+ const prepared = resolvedSpawn(o); // canonical validation before mutation
286
172
  // Reserve name and identity together before anything is written or started.
287
173
  // A loser of the race creates no config, state, or service.
288
174
  creation.onStage?.('reserving');
@@ -292,7 +178,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
292
178
  // Establish the identity BEFORE the service is enabled, and record
293
179
  // what was actually guaranteed so the briefing can say something true.
294
180
  creation.onStage?.('checking_identity');
295
- const guarantee = await ensureIdentity(effectiveIdentity(o), profileValues(o), creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
181
+ const guarantee = await ensureIdentity(effectiveIdentity(o), { bio: prepared.role.bio, persona: prepared.role.persona }, creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
296
182
  creation.onStage?.('checking_identity', {
297
183
  result: guarantee.evidence, guarantee: guarantee.state,
298
184
  });
@@ -305,12 +191,11 @@ export async function spawnPermanent(o, deps, creation = {}) {
305
191
  await creation.identityProvisioner?.remove?.(effectiveIdentity(o));
306
192
  },
307
193
  });
308
- mkdirSync(fleetDDir(), { recursive: true });
194
+ const agentRoot = join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents');
195
+ mkdirSync(agentRoot, { recursive: true });
309
196
  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
- }));
197
+ const file = join(agentRoot, `${o.name}.yaml`);
198
+ writeRoleFile(tx, file, stringify(agentDefinitionFromSpawn(o)));
314
199
  // `up` materialises the state dir and registers the service. Journal the
315
200
  // dir before it exists so a failure leaves the name genuinely reusable
316
201
  // rather than blocked by a half-built directory.
@@ -341,6 +226,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
341
226
  });
342
227
  mkdirSync(agentDir(o.name), { recursive: true });
343
228
  writeProvenance(agentDir(o.name), provenance);
229
+ recordGeneratedAgentSource(agentDir(o.name), o.configPath ?? defaultConfigPath(), file);
344
230
  creation.onStage?.('registering_supervisor');
345
231
  await up(loadConfig(o.configPath), [o.name], {
346
232
  ...deps,
@@ -378,8 +264,7 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
378
264
  validateSpawnOpts(o);
379
265
  if (o.isolationFile)
380
266
  readIsolationFile(o.isolationFile); // fail before reserving
381
- if (o.missionFile)
382
- readMissionFile(o.missionFile); // fail before reserving
267
+ const prepared = resolvedSpawn(o); // canonical validation before mutation
383
268
  // Retire only supervisors whose recorded owner is definitively stopped. This
384
269
  // bounded pass keeps the active roster clean without deleting old evidence.
385
270
  await reclaimStaleTempState();
@@ -392,56 +277,14 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
392
277
  creation.onStage?.('checking_identity', {
393
278
  result: 'unknown', guarantee: 'unverified',
394
279
  });
395
- return spawnTempInner(o, binPath, launch, tx, creation.onStage);
280
+ return spawnTempInner(o, prepared.role, binPath, launch, tx, creation.onStage);
396
281
  }, creation);
397
282
  }
398
- async function spawnTempInner(o, binPath, launch, tx, onStage) {
283
+ async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
399
284
  const cfg = loadConfig(o.configPath);
400
- const defaultHarness = cfg.defaults.harness;
401
- const fromOpts = buildRoleConfig(o, defaultHarness);
402
- const mergedHarnessOptions = {
403
- ...(cfg.defaults.harness_options ?? {}),
404
- ...(fromOpts.harness_options ?? {}),
405
- };
406
- const harness = o.harness ?? defaultHarness ?? 'claude-code';
407
- const inheritsModelDefaults = harness === (defaultHarness ?? 'claude-code') && o.model !== null;
408
- const tempAuthProxy = resolveAuthProxy(cfg.defaults.auth_proxy, fromOpts.auth_proxy);
409
- // An explicitly requested --model must reach the child, not just the banner
410
- // (src/model-env.ts).
411
- const modelEnv = resolveRoleModelEnv({
412
- harness,
413
- model: resolveRoleModel(o.model, o.harness, cfg.defaults),
414
- modelWasExplicit: o.model !== undefined,
415
- defaultsEnv: (cfg.defaults.env ?? {}),
416
- roleEnv: fromOpts.env,
417
- ...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
418
- });
419
- const model = modelEnv.model;
420
- const session = o.session ?? cfg.defaults.session ?? 'tmux';
421
285
  const role = {
422
- ...fromOpts, // includes `isolation` when --isolation-file was given
423
- name: o.name,
424
- harness,
425
- session,
426
- identity: o.identity ?? o.name,
427
- model,
428
- model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
429
- ? cfg.defaults.model_chain
430
- : undefined)),
431
- harness_options: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
432
- permissions: resolvePermissions(cfg.defaults.permissions, fromOpts.permissions),
433
- permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
434
- // Temp agents inherit the fleet-wide monitor defaults via the snapshot.
435
- monitor: resolveMonitorConfig(cfg.defaults.monitor, fromOpts.monitor),
436
- owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, fromOpts.owner_channel, session),
437
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, fromOpts.worklog),
438
- auth_proxy: tempAuthProxy,
439
- sourceFile: '(temp)',
440
- roomMemberStartup: o.roomMemberStartup,
286
+ ...preparedRole, sourceFile: '(temp)', loops: undefined, roomMemberStartup: o.roomMemberStartup,
441
287
  };
442
- role.env = modelEnv.env;
443
- if (role.auth_proxy && role.harness !== 'claude-code')
444
- throw new Error('auth_proxy is supported only by claude-code');
445
288
  onStage?.('writing_role');
446
289
  const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
447
290
  const provenance = buildProvenance({
@@ -467,10 +310,7 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
467
310
  if (cfg.startStaggerMs > 0)
468
311
  writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
469
312
  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
313
+ // Run the supervisor independently. On a service-managed host the temp
474
314
  // runner gets its own transient unit/job, so stopping the coordinator's unit
475
315
  // cannot kill a live worker in the coordinator's cgroup.
476
316
  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
  */