@ours.network/fleet 1.1.0-nightly.2 → 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.
package/README.md CHANGED
@@ -126,19 +126,16 @@ ours-fleet spawn Worker --mission "own the worker repo" \
126
126
  ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
127
127
 
128
128
  # Codex role: ours-codex is preferred automatically; plain codex is the fallback
129
- ours-fleet spawn Coder --harness codex --model gpt-5.4 \
130
- --session acp --approval ask --filesystem workspace \
131
- --profile fleet --search --monitor --coordinator FleetCoordinator
132
- # Note: --monitor is legacy consent for Codex's native monitor. Choose the
133
- # wake owner separately in fleet.yaml with monitor.mode: fleet|native.
129
+ ours-fleet spawn Coder --brain codex-fleet --role developer \
130
+ --approval ask --filesystem workspace --coordinator FleetCoordinator
134
131
  ```
135
132
 
136
133
  Inside a managed ACP role, `ours-fleet spawn` is transparently routed through
137
- that role's live supervisor. `ours-fleet spawn --role DeveloperX --temp` is the
138
- minimal form: omitted harness, session, cwd, coordinator, neutral permissions,
139
- fleet monitor policy, and same-harness model inherit from the caller. Explicit
140
- flags win; changing harness without a model lets the selected harness/fleet
141
- defaults choose one. After creation succeeds, fleet can deterministically notify
134
+ that role's live supervisor. `ours-fleet spawn DeveloperX --temp` is the
135
+ minimal form: omitted Brain/Role selections, cwd, coordinator, neutral permissions,
136
+ and fleet monitor policy inherit from the caller. Identity, environment, owner routing,
137
+ room startup, and sensitive inline Brain values do not. Explicit flags win. After
138
+ creation succeeds, fleet can deterministically notify
142
139
  the caller's owner channel with the caller and spawned-role details. This is an
143
140
  honest-actor convenience and attribution path, not a security boundary; host
144
141
  shells and deliberately bypassed absolute binaries retain direct behavior.
@@ -305,7 +302,7 @@ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
305
302
  ours-fleet config [-c FILE] validate + print merged plan
306
303
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
307
304
  ours-fleet send <Name> "text"
308
- ours-fleet spawn [--temp] [<Name> | --role <Name>] [--harness --session --mission --model --approval ...]
305
+ ours-fleet spawn [--temp] [<Name> | --name <Name>] --brain <ID|inline:{...}> --role <ID|inline:{...}> [--approval ...]
309
306
  ours-fleet loops validate|list|status
310
307
  ours-fleet loops reload <Role>
311
308
  ours-fleet loops run-now|disable|enable <Role> <Loop>
@@ -359,13 +356,9 @@ defaults:
359
356
  max_kb: 1024 # rotate only above this active-log size
360
357
  keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
361
358
  max_archives: 12 # recent beside log; older preserved cold
362
- Agent document:
363
- Name: # filename stem; [A-Za-z0-9_-]+
364
- harness: claude-code
365
- session: acp # optional; ACP is the only supported session
366
- session_options:
367
- acp:
368
- command: claude-agent-acp # optional advanced override
359
+ Agent document (\`~/fleet/agents/Name.yaml\`):
360
+ role: { ref: RoleID }
361
+ brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
369
362
  identity: "Display Name" # ours identity to bind (default: Name)
370
363
  cwd: ${work_root}/repo # where the harness process runs
371
364
  coordinator: FleetCoordinator # announce target on boot
@@ -1051,10 +1044,9 @@ harness_options:
1051
1044
  config: { model_reasoning_effort: high }
1052
1045
  ```
1053
1046
 
1054
- Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
1055
- `--sandbox`, `--profile`, `--launcher`, `--search`, legacy `--monitor` (native
1056
- Codex monitor consent), repeatable
1057
- `--codex-config key=value`, and repeatable `--add-dir`. Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1047
+ One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
1048
+ sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
1049
+ Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
1058
1050
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
1059
1051
 
1060
1052
  ## Agent isolation
@@ -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';
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'>;
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';
@@ -123,7 +105,6 @@ export interface RoleCreationServiceOptions {
123
105
  journalDir?: string;
124
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,17 +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 { getAdapter } from '../harness/registry.js';
14
- import { claudeModelCatalog, codexModelCatalog, } from './model-catalog.js';
15
13
  const canonical = (value) => {
16
14
  if (Array.isArray(value))
17
15
  return `[${value.map(canonical).join(',')}]`;
@@ -81,47 +79,16 @@ export class RoleCreationService {
81
79
  async capabilities() {
82
80
  const reasons = [];
83
81
  let defaults = {};
84
- let roles = [];
85
82
  try {
86
83
  const config = loadConfig(this.options.configPath);
87
84
  defaults = config.defaults;
88
- roles = config.roles;
89
85
  }
90
86
  catch (error) {
91
87
  reasons.push(`configuration is invalid: ${error.message}`);
92
88
  }
93
- const modelsFor = (harness, catalog) => {
94
- const configured = roles.filter(role => role.harness === harness)
95
- .flatMap(role => [role.model, ...(role.model_chain ?? [])])
96
- .filter((model) => Boolean(model));
97
- const inherited = resolveRoleModel(undefined, harness, defaults);
98
- const configuredOptions = [...new Set([...(inherited ? [inherited] : []), ...configured])]
99
- .filter(id => !catalog.models.some(model => model.id === id))
100
- .map(id => ({ id, label: `${id} (configured)`, reasoningEfforts: [], source: 'fleet-config' }));
101
- const modelOptions = [...configuredOptions, ...catalog.models];
102
- return { modelOptions, models: modelOptions.map(model => model.id) };
103
- };
104
- const codexCatalog = this.options.modelCatalogs?.codex?.() ?? codexModelCatalog();
105
- const claudeCatalog = this.options.modelCatalogs?.['claude-code']?.() ?? claudeModelCatalog();
106
- const codexModels = modelsFor('codex', codexCatalog);
107
- const claudeModels = modelsFor('claude-code', claudeCatalog);
108
89
  return {
109
90
  available: reasons.length === 0,
110
91
  reasons,
111
- harnesses: [
112
- {
113
- id: 'codex', available: true, sessions: ['acp'],
114
- defaultModel: resolveRoleModel(undefined, 'codex', defaults),
115
- ...codexModels, catalogSource: 'codex-runtime-catalog', customModelAllowed: true,
116
- warnings: codexCatalog.warnings,
117
- },
118
- {
119
- id: 'claude-code', available: true, sessions: ['acp'],
120
- defaultModel: resolveRoleModel(undefined, 'claude-code', defaults),
121
- ...claudeModels, catalogSource: 'claude-adapter-2.1', customModelAllowed: true,
122
- warnings: claudeCatalog.warnings,
123
- },
124
- ],
125
92
  lifetimes: ['permanent', 'temporary'],
126
93
  identityBootstrap: {
127
94
  mode: 'current-fleet-first-boot',
@@ -145,14 +112,12 @@ export class RoleCreationService {
145
112
  const defaults = cfg.defaults;
146
113
  const opts = this.spawnOptions(request);
147
114
  validateSpawnOpts(opts);
148
- buildRoleConfig(opts, defaults.harness);
115
+ const resolved = spawnDryRun(opts).resolvedRole;
149
116
  const cwd = request.cwd ? this.resolveCwd(request.cwd) : undefined;
150
117
  const effective = {
151
118
  name: request.name, identity: request.name,
152
- harness: request.harness ?? defaults.harness ?? 'claude-code',
153
- session: request.session ?? defaults.session ?? 'acp',
154
- model: resolveRoleModel(request.model, request.harness, defaults),
155
- reasoningEffort: request.reasoningEffort ?? undefined,
119
+ harness: resolved.harness, session: resolved.session, model: resolved.model,
120
+ reasoningEffort: resolved.effort,
156
121
  cwd, lifetime: request.lifetime,
157
122
  permissions: resolvePermissions(defaults.permissions, request.permissions),
158
123
  monitor: resolveMonitorConfig(defaults.monitor, request.monitor),
@@ -184,9 +149,7 @@ export class RoleCreationService {
184
149
  if (existingIdentity === 'unknown' && !request.unverifiedIdentityAcknowledged)
185
150
  prerequisites.push('confirm creation with an unverified identity preflight');
186
151
  const provenance = {
187
- harness: 'request', session: 'request', identity: 'built-in',
188
- model: request.model !== undefined ? 'request'
189
- : resolveRoleModel(undefined, request.harness, defaults) ? 'fleet-default' : 'built-in',
152
+ brain: 'request', role: 'request', identity: 'built-in',
190
153
  cwd: request.cwd ? 'request' : 'built-in', permissions: 'request',
191
154
  monitor: request.monitor ? 'request' : defaults.monitor ? 'fleet-default' : 'built-in',
192
155
  };
@@ -297,8 +260,8 @@ export class RoleCreationService {
297
260
  }
298
261
  validate(input) {
299
262
  const allowed = new Set([
300
- 'name', 'harness', 'model', 'reasoningEffort', 'session', 'cwd', 'lifetime', 'mission',
301
- 'coordinator', 'permissions', 'bio', 'persona', 'monitor', 'openAfterCreate',
263
+ 'name', 'brain', 'role', 'cwd', 'lifetime',
264
+ 'coordinator', 'permissions', 'monitor', 'openAfterCreate',
302
265
  'highRiskAcknowledged', 'reuseExistingIdentityAcknowledged',
303
266
  'unverifiedIdentityAcknowledged',
304
267
  ]);
@@ -308,26 +271,11 @@ export class RoleCreationService {
308
271
  throw new FleetError('invalid_request', `unsupported web creation field: ${unsupported[0]}`);
309
272
  if (!ROLE_NAME_RE.test(input.name))
310
273
  throw new FleetError('invalid_request', 'invalid role name');
311
- if (!['codex', 'claude-code'].includes(input.harness))
312
- throw new FleetError('invalid_request', 'unsupported harness');
313
- if (input.session !== 'acp')
314
- throw new FleetError('invalid_request', 'unsupported session backend');
274
+ if (!input.brain || !input.role)
275
+ throw new FleetError('invalid_request', 'brain and role selections are required');
315
276
  if (!['permanent', 'temporary'].includes(input.lifetime))
316
277
  throw new FleetError('invalid_request', 'unsupported lifetime');
317
- bounded(input.model ?? undefined, 'model', 128);
318
- bounded(input.reasoningEffort ?? undefined, 'reasoning effort', 16);
319
- if (input.reasoningEffort != null) {
320
- try {
321
- getAdapter(input.harness).agentSession.resolveBrain({ effort: input.reasoningEffort });
322
- }
323
- catch (error) {
324
- throw new FleetError('invalid_request', error.message);
325
- }
326
- }
327
- bounded(input.mission, 'mission', 4_096);
328
278
  bounded(input.coordinator, 'coordinator', 128);
329
- bounded(input.bio, 'bio', 8_192);
330
- bounded(input.persona, 'persona', 16_384);
331
279
  resolvePermissions(undefined, input.permissions);
332
280
  if (input.monitor) {
333
281
  const problems = validateMonitorConfig(input.monitor);
@@ -337,10 +285,8 @@ export class RoleCreationService {
337
285
  throw new FleetError('invalid_request', 'web creation supports monitor.inject=notification only');
338
286
  }
339
287
  return {
340
- ...input, model: input.model === null ? null : input.model?.trim() || undefined,
341
- reasoningEffort: input.reasoningEffort === null ? null : input.reasoningEffort?.trim() || undefined,
342
- mission: input.mission?.trim() || undefined, coordinator: input.coordinator?.trim() || undefined,
343
- bio: input.bio?.trim() || undefined, persona: input.persona?.trim() || undefined,
288
+ ...input, brain: structuredClone(input.brain), role: structuredClone(input.role),
289
+ coordinator: input.coordinator?.trim() || undefined,
344
290
  monitor: input.monitor ? structuredClone(input.monitor) : undefined,
345
291
  };
346
292
  }
@@ -367,12 +313,10 @@ export class RoleCreationService {
367
313
  spawnOptions(request, creationActionId) {
368
314
  return {
369
315
  name: request.name, temp: request.lifetime === 'temporary',
370
- identity: request.name, harness: request.harness,
371
- model: request.model, session: request.session, cwd: request.cwd,
372
- reasoningEffort: request.reasoningEffort,
373
- mission: request.mission, coordinator: request.coordinator,
316
+ identity: request.name, brain: request.brain, role: request.role, cwd: request.cwd,
317
+ coordinator: request.coordinator,
374
318
  approval: request.permissions.approval, filesystem: request.permissions.filesystem,
375
- unattended: request.permissions.unattended, bio: request.bio, persona: request.persona,
319
+ unattended: request.permissions.unattended,
376
320
  monitorConfig: request.monitor,
377
321
  configPath: this.options.configPath,
378
322
  surface: creationActionId ? 'web' : undefined, creationActionId,
@@ -259,8 +259,13 @@ export class TaskRoomApplicationService {
259
259
  validateTemplates() {
260
260
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
261
261
  return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
262
- const issues = template.members.flatMap(member => cfg.roles.some(role => role.name === member.role_ref)
263
- ? [] : [`member ${member.slot}: role_ref '${member.role_ref}' not found in fleet roles`]);
262
+ const issues = template.members.flatMap(member => {
263
+ if (!('ref' in member.agent))
264
+ return [];
265
+ const ref = member.agent.ref;
266
+ return cfg.roles.some(role => role.name === ref)
267
+ ? [] : [`member ${member.slot}: Agent ref '${ref}' not found`];
268
+ });
264
269
  return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
265
270
  });
266
271
  }
package/dist/briefing.js CHANGED
@@ -174,10 +174,10 @@ export function generateBriefing(role, v, opts) {
174
174
  L.push('This ACP role has a supervisor-scoped ours-fleet proxy. Use the ordinary');
175
175
  L.push('`ours-fleet spawn` command; the CLI routes it through your live supervisor, which');
176
176
  L.push('records you as the caller and reports successful creation to your owner channel.');
177
- L.push('A minimal call is `ours-fleet spawn --role DeveloperName --temp`.');
178
- L.push('For omitted execution settings, the supervisor inherits your harness, session, model,');
177
+ L.push('A minimal call is `ours-fleet spawn DeveloperName --temp`.');
178
+ L.push('For omitted settings, the supervisor inherits your canonical Brain and Role selections,');
179
179
  L.push('working directory, neutral permissions, coordinator, and fleet monitor policy. Every');
180
- L.push('explicit spawn option wins. An explicit different harness does not inherit your model.');
180
+ L.push('explicit option wins; identity/session-local and secret material never inherit.');
181
181
  L.push('This proxy is attribution and convenience, not a security boundary for unisolated roles.');
182
182
  }
183
183
  if (role.coordinator) {
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.2",
3
- "buildId": "da774e6be6b1",
4
- "commit": "f8890b0449f4142a0c27a195e1351555fe599186",
2
+ "version": "1.1.0-nightly.3",
3
+ "buildId": "ecb06fff91d3",
4
+ "commit": "87f6db6848b924074b76ce19f15a6b15866c4da3",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-30T15:08:00.155Z",
6
+ "builtAt": "2026-08-30T17:21:38.816Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { VERSION } from './version.js';
9
9
  import { INIT_COMPLETION_GUIDANCE } from './init-guidance.js';
10
10
  import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel, } from './provenance.js';
11
11
  import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './paths.js';
12
- import { findRole, loadConfig } from './config.js';
12
+ import { findRole, loadConfig, ROLE_NAME_RE } from './config.js';
13
13
  import { formatDuration } from './duration.js';
14
14
  import { redactSensitive, resolvedPlan } from './resolved-plan.js';
15
15
  import { pickBackend } from './supervisor/index.js';
@@ -74,7 +74,7 @@ const passthrough = (cmd, args) => new Promise(resolve => {
74
74
  });
75
75
  const program = new Command()
76
76
  .name('ours-fleet')
77
- .description('Fleet of persistent, identity-bound AI agents — selectable harnesses over ACP sessions.')
77
+ .description('Fleet of persistent, identity-bound AI agents — canonical Brain + Role definitions over ACP sessions.')
78
78
  .enablePositionalOptions()
79
79
  .version(VERSION);
80
80
  const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'manifest (default: ~/fleet.yaml; documents under ~/fleet/)');
@@ -218,6 +218,24 @@ function parseCodexConfig(values) {
218
218
  }
219
219
  return out;
220
220
  }
221
+ function parseAgentSelection(raw, kind) {
222
+ if (raw === undefined)
223
+ return undefined;
224
+ if (ROLE_NAME_RE.test(raw))
225
+ return { ref: raw };
226
+ if (!raw.startsWith('inline:'))
227
+ throw new Error(`invalid --${kind}; use a declared ID or inline:{...}`);
228
+ let value;
229
+ try {
230
+ value = JSON.parse(raw.slice('inline:'.length));
231
+ }
232
+ catch {
233
+ throw new Error(`invalid --${kind} inline mapping`);
234
+ }
235
+ if (!value || typeof value !== 'object' || Array.isArray(value))
236
+ throw new Error(`invalid --${kind} inline mapping`);
237
+ return { inline: value };
238
+ }
221
239
  cOpt(program.command('config').description('validate + print the merged plan (no side effects)'))
222
240
  .option('--json', 'emit the stable, versioned, secret-safe resolved plan')
223
241
  .option('--yaml-mode <mode>', 'non-plain YAML policy: compat|strict', 'compat')
@@ -1112,50 +1130,32 @@ cOpt(program.command('rm <name>').description('stop + remove a role (temporary e
1112
1130
  }
1113
1131
  });
1114
1132
  cOpt(program.command('spawn [name]').description('spawn a new agent (permanent by default)'))
1115
- .option('--role <name>', 'role name (alternative to the positional name)')
1133
+ .option('--name <name>', 'agent name (alternative to the positional name)')
1134
+ .option('--brain <selection>', 'Brain ID or inline:{...} definition')
1135
+ .option('--role <selection>', 'Role ID or inline:{...} definition')
1116
1136
  .option('--temp', 'temporary: independent transient supervisor, archived on retirement, gone on reboot')
1117
- .option('--harness <id>', 'harness adapter (default: defaults.harness)')
1118
- .option('--session <backend>', 'session backend: acp (default: acp)')
1119
- .option('--mission <text>', 'one-line mission')
1120
- .option('--mission-file <path>', 'UTF-8 mission text (mutually exclusive with --mission)')
1121
- .option('--identity <name>', 'ours identity to bind (default: role name)')
1137
+ .option('--identity <name>', 'ours identity to bind (default: Agent name)')
1122
1138
  .option('--cwd <dir>', 'working directory')
1123
1139
  .option('--coordinator <name>', 'announce target')
1124
- .option('--model <id>', 'model id to launch on (e.g. claude-fable-5); default: launcher default')
1125
- .option('--permission-mode <mode>', 'harness permission mode (Codex: untrusted|on-request|never; Claude: native values)')
1126
1140
  .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (Codex ACP allow selects agent-full-access; deny is deprecated)')
1127
1141
  .option('--filesystem <mode>', 'common filesystem intent: read-only|workspace|unrestricted')
1128
1142
  .option('--unattended <mode>', 'permission behavior without a console: deny|wait')
1129
- .option('--sandbox <mode>', 'Codex sandbox: read-only|workspace-write|danger-full-access')
1130
- .option('--profile <name>', 'Codex profile file name ($CODEX_HOME/<name>.config.toml)')
1131
- .option('--launcher <mode>', 'Codex launcher: auto|ours-codex|codex (default: auto)')
1132
- .option('--search', 'enable Codex live web search')
1133
- .option('--codex-config <key=value>', 'Codex config override (repeatable)', collect, [])
1134
- .option('--add-dir <dir>', 'additional Codex writable directory (repeatable)', collect, [])
1135
- .option('--monitor', 'legacy: consent to arm Codex\'s native monitor (wake owner is monitor.mode in YAML)')
1136
- .option('--bio-file <file>', 'public bio (file)')
1137
- .option('--persona-file <file>', 'persona / operating contract (file)')
1138
1143
  .option('--isolation-file <path>', 'file holding an isolation: mapping (same schema as fleet.yaml)')
1139
1144
  .option('--dry-run', 'validate and print without reserving or creating anything')
1140
1145
  .option('--json', 'with --dry-run, emit a stable secret-safe JSON result')
1141
1146
  .action(async (name, opts) => {
1142
1147
  try {
1143
- const roleName = String(name ?? opts.role ?? '');
1148
+ const roleName = String(name ?? opts.name ?? '');
1144
1149
  if (!roleName)
1145
- throw new Error('role name is required (positional or --role)');
1146
- if (name && opts.role && name !== opts.role)
1147
- throw new Error(`positional role '${name}' conflicts with --role '${opts.role}'`);
1150
+ throw new Error('agent name is required (positional or --name)');
1151
+ if (name && opts.name && name !== opts.name)
1152
+ throw new Error(`positional agent name conflicts with --name`);
1148
1153
  const o = {
1149
- name: roleName, temp: opts.temp, harness: opts.harness, session: opts.session, mission: opts.mission,
1150
- missionFile: opts.missionFile,
1154
+ name: roleName, temp: opts.temp,
1155
+ brain: parseAgentSelection(opts.brain, 'brain'), role: parseAgentSelection(opts.role, 'role'),
1151
1156
  identity: opts.identity, cwd: opts.cwd, coordinator: opts.coordinator,
1152
- model: opts.model,
1153
- permissionMode: opts.permissionMode, approval: opts.approval,
1157
+ approval: opts.approval,
1154
1158
  filesystem: opts.filesystem, unattended: opts.unattended,
1155
- sandbox: opts.sandbox, profile: opts.profile,
1156
- launcher: opts.launcher, search: opts.search,
1157
- codexConfig: parseCodexConfig(opts.codexConfig), addDirs: opts.addDir, monitor: opts.monitor,
1158
- bioFile: opts.bioFile, personaFile: opts.personaFile,
1159
1159
  isolationFile: opts.isolationFile, configPath: opts.configuration,
1160
1160
  dryRun: opts.dryRun, json: opts.json,
1161
1161
  };
@@ -1184,7 +1184,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1184
1184
  if (proxyStateDir) {
1185
1185
  // Paths entered in the agent shell belong to that shell's cwd, not the
1186
1186
  // supervisor process. Normalize before crossing the control boundary.
1187
- for (const key of ['missionFile', 'bioFile', 'personaFile', 'isolationFile']) {
1187
+ for (const key of ['isolationFile']) {
1188
1188
  if (o[key])
1189
1189
  o[key] = resolvePath(o[key]);
1190
1190
  }
@@ -1497,4 +1497,11 @@ cOpt(program.command('_run-watchdogs', { hidden: true }))
1497
1497
  die(e);
1498
1498
  }
1499
1499
  });
1500
- program.parseAsync(process.argv);
1500
+ const spawnIndex = process.argv.indexOf('spawn');
1501
+ if (spawnIndex >= 0 && process.argv.slice(spawnIndex + 1).some(arg => arg === '--harness' || arg.startsWith('--harness=') || arg === '--model' || arg.startsWith('--model='))) {
1502
+ console.error('error: --harness and --model were removed; select a Brain with --brain');
1503
+ process.exitCode = 1;
1504
+ }
1505
+ else {
1506
+ program.parseAsync(process.argv);
1507
+ }
package/dist/config.d.ts CHANGED
@@ -137,6 +137,27 @@ export interface RoleConfig {
137
137
  worklog?: WorklogPolicyInput;
138
138
  auth_proxy?: Partial<AuthProxyConfig>;
139
139
  }
140
+ export type AgentSelection<T extends Record<string, unknown> = Record<string, unknown>> = {
141
+ ref: string;
142
+ } | {
143
+ inline: T;
144
+ };
145
+ /** Canonical authoring contract shared by configured, spawned, and room agents. */
146
+ export interface AgentDefinition {
147
+ role: AgentSelection;
148
+ brain: AgentSelection;
149
+ permissions?: Partial<CommonPermissions>;
150
+ identity?: string;
151
+ cwd?: string;
152
+ coordinator?: string;
153
+ env?: Record<string, string>;
154
+ oversee?: OverseeEntry[];
155
+ isolation?: IsolationConfig;
156
+ monitor?: Partial<MonitorConfig>;
157
+ owner_channel?: OwnerChannelConfigInput;
158
+ worklog?: WorklogPolicyInput;
159
+ auth_proxy?: Partial<AuthProxyConfig>;
160
+ }
140
161
  /** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
141
162
  export interface RoomMemberStartup {
142
163
  room_id: string;
@@ -171,6 +192,8 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
171
192
  provenance?: Record<string, FieldProvenance>;
172
193
  /** Internal/transient: never accepted as a user-authored RoleConfig key. */
173
194
  roomMemberStartup?: RoomMemberStartup;
195
+ /** Original unresolved selections only; denied operational fields never enter inheritance state. */
196
+ agentSelections?: Pick<AgentDefinition, 'role' | 'brain'>;
174
197
  }
175
198
  export interface FieldProvenance {
176
199
  sourceFile: string;
@@ -190,6 +213,8 @@ export interface FieldProvenance {
190
213
  }
191
214
  export interface FleetConfig {
192
215
  roles: ResolvedRole[];
216
+ /** Canonical, variable-resolved Agent authoring documents keyed by stable Agent ID. */
217
+ agentDefinitions?: Record<string, AgentDefinition>;
193
218
  vars: Record<string, string>;
194
219
  defaults: Record<string, unknown>;
195
220
  files: string[];
@@ -229,10 +254,22 @@ export declare function resolveRoleModel(model: string | null | undefined, harne
229
254
  */
230
255
  export declare function isolationContextFor(role: ResolvedRole): WrapContext;
231
256
  export declare const ROLE_NAME_RE: RegExp;
257
+ export declare const ROLE_PRESET_KEYS: string[];
258
+ export declare const BRAIN_PRESET_KEYS: string[];
259
+ export declare const AGENT_KEYS: string[];
260
+ type BarePreset = Record<string, unknown>;
261
+ export declare function validateRoleValue(value: BarePreset, file: string, pointer: string): void;
262
+ export declare function validateBrainValue(value: BarePreset, file: string, pointer: string): void;
263
+ export declare function assertBareKeys(value: unknown, allowed: string[], label: string): BarePreset;
232
264
  export declare function splitRootFor(base: string): string;
233
265
  /** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
234
266
  export declare function loadConfig(configPath?: string, options?: {
235
267
  yamlMode?: YamlMode;
268
+ additionalAgent?: {
269
+ id: string;
270
+ definition: AgentDefinition;
271
+ };
272
+ skipWatchdogs?: boolean;
236
273
  }): FleetConfig;
237
274
  /**
238
275
  * Canonical form of a 64-hex container ID for authorization decisions. Hex