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

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 (65) hide show
  1. package/README.md +76 -93
  2. package/dist/application/role-creation-service.d.ts +2 -21
  3. package/dist/application/role-creation-service.js +15 -64
  4. package/dist/application/role-removal-service.js +6 -3
  5. package/dist/application/task-room-service.js +7 -2
  6. package/dist/application/types.d.ts +1 -1
  7. package/dist/briefing.js +5 -5
  8. package/dist/build-info.json +4 -4
  9. package/dist/cli.js +48 -38
  10. package/dist/config.d.ts +65 -2
  11. package/dist/config.js +337 -25
  12. package/dist/creation.d.ts +1 -1
  13. package/dist/creation.js +2 -2
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +48 -97
  16. package/dist/doctor.js +4 -4
  17. package/dist/fleet-proxy.js +30 -8
  18. package/dist/generated-agent-source.d.ts +9 -0
  19. package/dist/generated-agent-source.js +53 -0
  20. package/dist/harness/agent-session.d.ts +13 -0
  21. package/dist/harness/claude-code-session.d.ts +4 -0
  22. package/dist/harness/claude-code-session.js +4 -0
  23. package/dist/harness/claude-code.js +10 -0
  24. package/dist/harness/codex-session.d.ts +4 -0
  25. package/dist/harness/codex-session.js +4 -0
  26. package/dist/harness/codex.js +14 -0
  27. package/dist/harness/registry.js +4 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/init-guidance.d.ts +1 -0
  31. package/dist/init-guidance.js +5 -0
  32. package/dist/model-env.d.ts +2 -3
  33. package/dist/model-env.js +3 -5
  34. package/dist/ops.js +4 -2
  35. package/dist/owner-channel/commands.js +2 -1
  36. package/dist/resolved-plan.d.ts +3 -2
  37. package/dist/resolved-plan.js +21 -5
  38. package/dist/rooms-tasks/cli.js +2 -1
  39. package/dist/rooms-tasks/config.js +24 -9
  40. package/dist/rooms-tasks/provision.js +71 -36
  41. package/dist/rooms-tasks/templates.js +6 -6
  42. package/dist/rooms-tasks/types.d.ts +8 -15
  43. package/dist/rooms-tasks/types.js +1 -4
  44. package/dist/sensitive-config.d.ts +2 -0
  45. package/dist/sensitive-config.js +4 -0
  46. package/dist/spawn.d.ts +8 -33
  47. package/dist/spawn.js +52 -211
  48. package/dist/watchdog/config.d.ts +10 -7
  49. package/dist/watchdog/config.js +29 -19
  50. package/dist/watchdog/run.js +4 -13
  51. package/dist/web/fleet-config-service.d.ts +11 -8
  52. package/dist/web/fleet-config-service.js +383 -183
  53. package/dist/web/topology-promote.js +25 -15
  54. package/dist/web/topology.js +2 -2
  55. package/dist/web/yaml-document-edit.js +2 -0
  56. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  57. package/dist/web-app/index.html +1 -1
  58. package/examples/fleet/agents/Alice.yaml +11 -0
  59. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  60. package/examples/fleet/brains/claude-default.yaml +5 -0
  61. package/examples/fleet/roles/coordinator.yaml +5 -0
  62. package/examples/fleet/roles/developer.yaml +3 -0
  63. package/examples/fleet.yaml +74 -0
  64. package/package.json +2 -1
  65. package/dist/web-app/assets/index-DhMDVRU1.js +0 -10
@@ -100,6 +100,9 @@ export interface RoomMemberLaunchState {
100
100
  /** Expected authenticated proxy caller while adopting a post-spawn crash. */
101
101
  caller_role?: string;
102
102
  mission_sha256?: string;
103
+ /** Redacted, deterministic effective Agent launch definition retained for retry inspection. */
104
+ agent_definition?: Record<string, unknown>;
105
+ agent_fingerprint?: string;
103
106
  launch_id?: string;
104
107
  updated_at: string;
105
108
  error?: string;
@@ -189,19 +192,10 @@ export interface TemplateMemberSlot {
189
192
  slot: string;
190
193
  role: string;
191
194
  count: number;
192
- role_ref: string;
193
- overrides?: TemplateRoleOverrides;
194
- }
195
- export interface TemplateRoleOverrides {
196
- harness?: string;
197
- model?: string;
198
- model_chain?: string[];
199
- permissions?: Record<string, unknown>;
200
- isolation?: Record<string, unknown>;
201
- cwd?: string;
202
- env?: Record<string, string>;
203
- persona?: string;
204
- mission?: string;
195
+ /** Canonical Agent definition, or stable reference to a declared Agent. */
196
+ agent: import('../config.js').AgentDefinition | {
197
+ ref: string;
198
+ };
205
199
  }
206
200
  export interface TemplateRoomConfig {
207
201
  quiet_membership?: boolean;
@@ -252,5 +246,4 @@ export declare const ROOMS_COWORK_KEYS: readonly ["config"];
252
246
  export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
253
247
  export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
254
248
  export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
255
- export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "role_ref", "overrides"];
256
- export declare const TEMPLATE_OVERRIDE_KEYS: readonly ["harness", "model", "model_chain", "permissions", "isolation", "cwd", "env", "persona", "mission"];
249
+ export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "agent"];
@@ -14,7 +14,4 @@ export const ROOMS_COWORK_KEYS = ['config'];
14
14
  export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
15
15
  export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
16
16
  export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
17
- export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'role_ref', 'overrides'];
18
- export const TEMPLATE_OVERRIDE_KEYS = [
19
- 'harness', 'model', 'model_chain', 'permissions', 'isolation', 'cwd', 'env', 'persona', 'mission',
20
- ];
17
+ export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'agent'];
@@ -0,0 +1,2 @@
1
+ /** Shared recursive configuration redaction boundary for operator-facing models. */
2
+ export declare function isSensitiveConfigKey(key: string): boolean;
@@ -0,0 +1,4 @@
1
+ /** Shared recursive configuration redaction boundary for operator-facing models. */
2
+ export function isSensitiveConfigKey(key) {
3
+ return /(?:secret|token|password|credential|private[_-]?key|auth|invite|api[_-]?key)/i.test(key);
4
+ }
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,12 +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')
127
- throw new Error("invalid --session 'tmux'; tmux is no longer supported; use --session acp");
128
- if (o.session && o.session !== 'acp')
129
- throw new Error(`invalid --session '${o.session}'; allowed: acp`);
130
82
  if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
131
83
  throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
132
84
  if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
@@ -137,23 +89,12 @@ export function validateSpawnOpts(o) {
137
89
  throw new Error(`invalid role name '${o.name}'`);
138
90
  if (o.identity && !/^[A-Za-z0-9_-]+$/.test(o.identity))
139
91
  throw new Error(`invalid identity name '${o.identity}'`);
140
- if (o.model && (o.model.length > 128 || /[\0-\x1f\x7f]/.test(o.model)))
141
- throw new Error('model must be printable and at most 128 characters');
142
92
  if (o.monitorConfig) {
143
93
  const problems = validateMonitorConfig(o.monitorConfig);
144
94
  if (problems.length)
145
95
  throw new Error(problems.join('; '));
146
96
  }
147
97
  }
148
- /** Read mission text without trimming or newline rewriting. */
149
- export function readMissionFile(path) {
150
- try {
151
- return readFileSync(path, 'utf8');
152
- }
153
- catch (e) {
154
- throw new Error(`--mission-file ${path}: ${e.message}`);
155
- }
156
- }
157
98
  /**
158
99
  * Reject names that are already USED. This is a precondition, not a claim: it
159
100
  * runs INSIDE the creation transaction, after both names are reserved, so the
@@ -182,62 +123,13 @@ export function spawnDryRun(o) {
182
123
  validateSpawnOpts(o);
183
124
  if (o.isolationFile)
184
125
  readIsolationFile(o.isolationFile);
185
- if (o.missionFile)
186
- readMissionFile(o.missionFile);
187
126
  assertNameFree(o);
188
- const cfg = loadConfig(o.configPath);
189
- const raw = buildRoleConfig(o, cfg.defaults.harness);
190
- const harnessOptions = {
191
- ...(cfg.defaults.harness_options ?? {}),
192
- ...(raw.harness_options ?? {}),
193
- };
194
- const harness = raw.harness ?? cfg.defaults.harness ?? 'claude-code';
195
- const defaultHarness = cfg.defaults.harness ?? 'claude-code';
196
- const inheritsModelDefaults = harness === defaultHarness && raw.model !== null;
197
- const authProxy = resolveAuthProxy(cfg.defaults.auth_proxy, raw.auth_proxy);
198
- // One resolution for the environment and the model it pins (src/model-env.ts).
199
- const modelEnv = resolveRoleModelEnv({
200
- harness,
201
- model: resolveRoleModel(raw.model, raw.harness, cfg.defaults),
202
- modelWasExplicit: raw.model !== undefined,
203
- defaultsEnv: (cfg.defaults.env ?? {}),
204
- roleEnv: raw.env,
205
- ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
206
- });
207
- const model = modelEnv.model;
208
- const session = raw.session ?? cfg.defaults.session ?? 'acp';
209
- const resolvedRole = {
210
- ...raw,
211
- name: o.name,
212
- sourceFile: o.temp ? '(temp dry-run)' : join(fleetDDir(), `${o.name}.yaml`),
213
- harness,
214
- session,
215
- session_options: raw.session_options,
216
- permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
217
- permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
218
- identity: effectiveIdentity(o),
219
- model,
220
- model_chain: resolveModelChain(model, raw.model_chain ?? (inheritsModelDefaults
221
- ? cfg.defaults.model_chain
222
- : undefined)),
223
- harness_options: Object.keys(harnessOptions).length ? harnessOptions : undefined,
224
- isolation: raw.isolation ?? cfg.defaults.isolation,
225
- monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
226
- owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
227
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, raw.worklog),
228
- auth_proxy: authProxy,
229
- };
230
- resolvedRole.env = modelEnv.env;
231
- const adapter = getAdapter(resolvedRole.harness);
232
- if (resolvedRole.auth_proxy && resolvedRole.harness !== 'claude-code')
233
- throw new Error('auth_proxy is supported only by claude-code');
234
- const optionProblems = adapter.validateOptions(resolvedRole.harness_options, resolvedRole);
235
- if (optionProblems.length)
236
- throw new Error(optionProblems.map(problem => `${problem.path}: ${problem.message}`).join('; '));
127
+ const resolved = resolvedSpawn(o);
128
+ const resolvedRole = resolved.role;
237
129
  return {
238
130
  schemaVersion: 1,
239
131
  warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
240
- roleDocument: { roles: { [o.name]: raw } },
132
+ roleDocument: structuredClone(resolved.definition),
241
133
  resolvedRole,
242
134
  };
243
135
  }
@@ -253,22 +145,15 @@ function provenanceSettings(o, defaults) {
253
145
  const perms = (defaults.permissions ?? {});
254
146
  const callerDefaults = new Set(o.inheritedFromCaller ?? []);
255
147
  const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
256
- const explicitModel = typeof o.model === 'string' ? o.model.trim() : undefined;
257
- const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
148
+ const selection = (value) => value && 'ref' in value ? `ref:${value.ref}` : value ? 'inline' : undefined;
258
149
  return {
259
- harness: tagged('harness', provenanceOf(o.harness, defaults.harness, 'claude-code')),
260
- session: tagged('session', provenanceOf(o.session, defaults.session, 'acp')),
150
+ brain: tagged('brain', provenanceOf(selection(o.brain), undefined, undefined)),
151
+ role: tagged('role', provenanceOf(selection(o.role), undefined, undefined)),
261
152
  identity: o.identity
262
153
  ? { value: o.identity, source: 'cli' }
263
154
  : { value: o.name, source: 'built-in' }, // defaults to the role name
264
155
  cwd: tagged('cwd', provenanceOf(o.cwd, undefined, undefined)),
265
- model: tagged('model', o.model === null
266
- ? { value: undefined, source: 'cli' }
267
- : explicitModel
268
- ? { value: explicitModel, source: 'cli' }
269
- : { value: inheritedModel, source: inheritedModel ? 'fleet-default' : 'built-in' }),
270
156
  coordinator: tagged('coordinator', provenanceOf(o.coordinator, undefined, undefined)),
271
- permission_mode: provenanceOf(o.permissionMode, undefined, undefined),
272
157
  approval: tagged('approval', provenanceOf(o.approval, perms.approval, 'ask')),
273
158
  filesystem: tagged('filesystem', provenanceOf(o.filesystem, perms.filesystem, 'workspace')),
274
159
  unattended: tagged('unattended', provenanceOf(o.unattended, perms.unattended, 'deny')),
@@ -278,13 +163,12 @@ function provenanceSettings(o, defaults) {
278
163
  monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
279
164
  };
280
165
  }
281
- /** 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. */
282
167
  export async function spawnPermanent(o, deps, creation = {}) {
283
168
  validateSpawnOpts(o);
284
169
  if (o.isolationFile)
285
170
  readIsolationFile(o.isolationFile); // fail before reserving
286
- if (o.missionFile)
287
- readMissionFile(o.missionFile); // fail before reserving
171
+ const prepared = resolvedSpawn(o); // canonical validation before mutation
288
172
  // Reserve name and identity together before anything is written or started.
289
173
  // A loser of the race creates no config, state, or service.
290
174
  creation.onStage?.('reserving');
@@ -294,7 +178,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
294
178
  // Establish the identity BEFORE the service is enabled, and record
295
179
  // what was actually guaranteed so the briefing can say something true.
296
180
  creation.onStage?.('checking_identity');
297
- 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);
298
182
  creation.onStage?.('checking_identity', {
299
183
  result: guarantee.evidence, guarantee: guarantee.state,
300
184
  });
@@ -307,12 +191,11 @@ export async function spawnPermanent(o, deps, creation = {}) {
307
191
  await creation.identityProvisioner?.remove?.(effectiveIdentity(o));
308
192
  },
309
193
  });
310
- mkdirSync(fleetDDir(), { recursive: true });
194
+ const agentRoot = join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents');
195
+ mkdirSync(agentRoot, { recursive: true });
311
196
  creation.onStage?.('writing_role');
312
- const file = join(fleetDDir(), `${o.name}.yaml`);
313
- writeRoleFile(tx, file, stringify({
314
- roles: { [o.name]: buildRoleConfig(o, cfg.defaults.harness) },
315
- }));
197
+ const file = join(agentRoot, `${o.name}.yaml`);
198
+ writeRoleFile(tx, file, stringify(agentDefinitionFromSpawn(o)));
316
199
  // `up` materialises the state dir and registers the service. Journal the
317
200
  // dir before it exists so a failure leaves the name genuinely reusable
318
201
  // rather than blocked by a half-built directory.
@@ -343,6 +226,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
343
226
  });
344
227
  mkdirSync(agentDir(o.name), { recursive: true });
345
228
  writeProvenance(agentDir(o.name), provenance);
229
+ recordGeneratedAgentSource(agentDir(o.name), o.configPath ?? defaultConfigPath(), file);
346
230
  creation.onStage?.('registering_supervisor');
347
231
  await up(loadConfig(o.configPath), [o.name], {
348
232
  ...deps,
@@ -380,8 +264,7 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
380
264
  validateSpawnOpts(o);
381
265
  if (o.isolationFile)
382
266
  readIsolationFile(o.isolationFile); // fail before reserving
383
- if (o.missionFile)
384
- readMissionFile(o.missionFile); // fail before reserving
267
+ const prepared = resolvedSpawn(o); // canonical validation before mutation
385
268
  // Retire only supervisors whose recorded owner is definitively stopped. This
386
269
  // bounded pass keeps the active roster clean without deleting old evidence.
387
270
  await reclaimStaleTempState();
@@ -394,56 +277,14 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
394
277
  creation.onStage?.('checking_identity', {
395
278
  result: 'unknown', guarantee: 'unverified',
396
279
  });
397
- return spawnTempInner(o, binPath, launch, tx, creation.onStage);
280
+ return spawnTempInner(o, prepared.role, binPath, launch, tx, creation.onStage);
398
281
  }, creation);
399
282
  }
400
- async function spawnTempInner(o, binPath, launch, tx, onStage) {
283
+ async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
401
284
  const cfg = loadConfig(o.configPath);
402
- const defaultHarness = cfg.defaults.harness;
403
- const fromOpts = buildRoleConfig(o, defaultHarness);
404
- const mergedHarnessOptions = {
405
- ...(cfg.defaults.harness_options ?? {}),
406
- ...(fromOpts.harness_options ?? {}),
407
- };
408
- const harness = o.harness ?? defaultHarness ?? 'claude-code';
409
- const inheritsModelDefaults = harness === (defaultHarness ?? 'claude-code') && o.model !== null;
410
- const tempAuthProxy = resolveAuthProxy(cfg.defaults.auth_proxy, fromOpts.auth_proxy);
411
- // An explicitly requested --model must reach the child, not just the banner
412
- // (src/model-env.ts).
413
- const modelEnv = resolveRoleModelEnv({
414
- harness,
415
- model: resolveRoleModel(o.model, o.harness, cfg.defaults),
416
- modelWasExplicit: o.model !== undefined,
417
- defaultsEnv: (cfg.defaults.env ?? {}),
418
- roleEnv: fromOpts.env,
419
- ...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
420
- });
421
- const model = modelEnv.model;
422
- const session = o.session ?? cfg.defaults.session ?? 'acp';
423
285
  const role = {
424
- ...fromOpts, // includes `isolation` when --isolation-file was given
425
- name: o.name,
426
- harness,
427
- session,
428
- identity: o.identity ?? o.name,
429
- model,
430
- model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
431
- ? cfg.defaults.model_chain
432
- : undefined)),
433
- harness_options: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
434
- permissions: resolvePermissions(cfg.defaults.permissions, fromOpts.permissions),
435
- permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
436
- // Temp agents inherit the fleet-wide monitor defaults via the snapshot.
437
- monitor: resolveMonitorConfig(cfg.defaults.monitor, fromOpts.monitor),
438
- owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, fromOpts.owner_channel, session),
439
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, fromOpts.worklog),
440
- auth_proxy: tempAuthProxy,
441
- sourceFile: '(temp)',
442
- roomMemberStartup: o.roomMemberStartup,
286
+ ...preparedRole, sourceFile: '(temp)', loops: undefined, roomMemberStartup: o.roomMemberStartup,
443
287
  };
444
- role.env = modelEnv.env;
445
- if (role.auth_proxy && role.harness !== 'claude-code')
446
- throw new Error('auth_proxy is supported only by claude-code');
447
288
  onStage?.('writing_role');
448
289
  const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
449
290
  const provenance = buildProvenance({
@@ -1,19 +1,19 @@
1
- import type { FleetConfig, ResolvedRole, SessionBackendId } from '../config.js';
1
+ import type { AgentDefinition, FleetConfig, ResolvedRole } from '../config.js';
2
+ import type { SessionBackendId } from '../config.js';
2
3
  import type { IsolationConfig } from '../isolation/types.js';
3
4
  export interface WatchdogConfig {
4
5
  coordinator?: string;
5
6
  enabled?: boolean;
6
7
  interval?: string;
7
8
  watch?: string[];
8
- harness?: string;
9
- model?: string | null;
10
- session?: string;
9
+ agent?: {
10
+ ref: string;
11
+ } | AgentDefinition;
11
12
  identity?: string;
12
13
  timeout?: string;
13
14
  keep_reports?: number;
14
15
  alert_cooldown?: string;
15
16
  prompt_file?: string;
16
- isolation?: IsolationConfig;
17
17
  }
18
18
  export interface ResolvedWatchdog {
19
19
  name: string;
@@ -22,15 +22,18 @@ export interface ResolvedWatchdog {
22
22
  intervalMs: number;
23
23
  watch: string[];
24
24
  watchExplicit: boolean;
25
+ agentDefinition: AgentDefinition;
26
+ resolvedAgent: ResolvedRole;
27
+ /** Resolved/read-only runtime facts; never independent authoring fields. */
25
28
  harness: string;
26
29
  session: SessionBackendId;
27
30
  model?: string;
31
+ isolation?: IsolationConfig;
28
32
  identity: string;
29
33
  timeoutMs: number;
30
34
  keepReports: number;
31
35
  alertCooldownMs: number;
32
36
  promptFile?: string;
33
- isolation?: IsolationConfig;
34
37
  sourceFile: string;
35
38
  }
36
39
  export declare const WATCHDOG_DEFAULT_INTERVAL_MS = 600000;
@@ -38,7 +41,7 @@ export declare const WATCHDOG_MIN_INTERVAL_MS = 60000;
38
41
  export declare const WATCHDOG_DEFAULT_TIMEOUT_MS = 300000;
39
42
  export declare const WATCHDOG_DEFAULT_KEEP_REPORTS = 50;
40
43
  export declare const WATCHDOG_DEFAULT_COOLDOWN_MS = 3600000;
41
- export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, defaults: Record<string, unknown>): ResolvedWatchdog[];
44
+ export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, agentDefinitions: Record<string, AgentDefinition>, resolveInline: (name: string, definition: AgentDefinition) => ResolvedRole): ResolvedWatchdog[];
42
45
  /**
43
46
  * Split `restart`'s argument names into watchdogs (a release, handled
44
47
  * directly) and roles (handed to restartRoles). Watchdog names can't collide