@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
package/README.md CHANGED
@@ -19,10 +19,10 @@ turns such sessions into **roles**: long-lived agents that
19
19
  - can **spawn subagents** (permanent or temporary) and **oversee** them: peek into
20
20
  a ward's console, answer a stuck prompt, nudge it back to work.
21
21
 
22
- The whole fleet is described in one hand-written `~/fleet.yaml`
23
- ("docker-compose for agents"): who exists, what harness they run in, their mission,
24
- persona, bio, working directory, and who oversees whom. `ours-fleet up` makes
25
- reality match the file.
22
+ The fleet uses a v2 manifest plus typed documents: `~/fleet.yaml` holds
23
+ fleet-wide operational policy and automation, while `~/fleet/agents`,
24
+ `~/fleet/roles`, and `~/fleet/brains` hold bare Agent, Role, and Brain documents.
25
+ `ours-fleet up` makes reality match that trusted split source set.
26
26
 
27
27
  **Harness-agnostic by design.** The core never assumes a specific agent CLI; each
28
28
  harness is a small adapter (how to launch, how to resume, how to wire config).
@@ -31,17 +31,19 @@ public — each additional harness (Gemini CLI, OpenCode, …) is a small adapte
31
31
  A single fleet can mix harnesses per role:
32
32
 
33
33
  ```yaml
34
- roles:
35
- Reviewer: # runs in Claude Code
36
- harness: claude-code
37
- Prototyper: # runs in Codex CLI
38
- harness: codex
34
+ # ~/fleet/agents/Reviewer.yaml
35
+ role: { ref: reviewer }
36
+ brain: { inline: { harness: claude-code, session: acp } }
37
+
38
+ # ~/fleet/agents/Prototyper.yaml
39
+ role: { inline: { mission: Build prototypes } }
40
+ brain: { inline: { harness: codex, session: acp } }
39
41
  ```
40
42
 
41
43
  ## How it works
42
44
 
43
45
  ```
44
- ~/fleet.yaml + ~/fleet.d/*.yaml your declaration
46
+ ~/fleet.yaml + ~/fleet/{agents,roles,brains}/*.yaml your declaration
45
47
  │ ours-fleet up
46
48
  ▼
47
49
  briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
@@ -105,8 +107,9 @@ become that account and repeat.
105
107
  ## Quickstart
106
108
 
107
109
  ```sh
110
+ cp -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
108
111
  cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
109
- $EDITOR ~/fleet.yaml # name your roles, missions, personas
112
+ $EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
110
113
  ours-fleet up # boot the fleet (staggered)
111
114
  ours-fleet ls # running consoles
112
115
  ours-fleet attach Alice # watch one live (Ctrl-b d to leave)
@@ -123,19 +126,16 @@ ours-fleet spawn Worker --mission "own the worker repo" \
123
126
  ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
124
127
 
125
128
  # Codex role: ours-codex is preferred automatically; plain codex is the fallback
126
- ours-fleet spawn Coder --harness codex --model gpt-5.4 \
127
- --session acp --approval ask --filesystem workspace \
128
- --profile fleet --search --monitor --coordinator FleetCoordinator
129
- # Note: --monitor is legacy consent for Codex's native monitor. Choose the
130
- # 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
131
131
  ```
132
132
 
133
133
  Inside a managed ACP role, `ours-fleet spawn` is transparently routed through
134
- that role's live supervisor. `ours-fleet spawn --role DeveloperX --temp` is the
135
- minimal form: omitted harness, session, cwd, coordinator, neutral permissions,
136
- fleet monitor policy, and same-harness model inherit from the caller. Explicit
137
- flags win; changing harness without a model lets the selected harness/fleet
138
- 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
139
139
  the caller's owner channel with the caller and spawned-role details. This is an
140
140
  honest-actor convenience and attribution path, not a security boundary; host
141
141
  shells and deliberately bypassed absolute binaries retain direct behavior.
@@ -252,27 +252,19 @@ Security boundaries:
252
252
  - tests use temporary fleet homes and fake supervisors/identity providers—never
253
253
  active role state.
254
254
 
255
- Permanent spawns are written to `~/fleet.d/<Name>.yaml`; the CLI never edits your
256
- hand-written `~/fleet.yaml`. `ours-fleet rm <Name>` unspawns.
257
-
258
- The web console is the one writer that can touch the base file, and it saves the
259
- file as a whole document: its setup wizard and configuration editor may create,
260
- change or remove any top-level block, including `vars:`, `defaults:`, `roles:`,
261
- `watchdogs:` and `loops:`. (`defaults:`, `watchdogs:` and `loops:` can only live
262
- in the base file — a `~/fleet.d/*.yaml` drop-in may declare `roles:` and nothing
263
- else.) Top-level keys the console does not recognise are round-tripped untouched.
264
-
265
- Console edits are applied as surgical splices against the file's exact bytes, so
266
- an unchanged save is a byte-for-byte no-op and lines outside the edit keep their
267
- comments, spacing and quoting. One bounded exception: changing the *length* of a
268
- block sequence — adding or removing an entry under `watch:`, `oversee:`, `roles:`
269
- or `wake_sources:` — rewrites that one collection as a whole, which drops inline
270
- comments written on its individual items. The loss is confined to the collection
271
- you edited, is shown in the diff before anything is written, and can be declined
272
- by not saving. Each write is revision-guarded, reviewed as a diff of the real
273
- file, validated with the real loader, and preceded by a timestamped backup.
274
- Values under `env:` — and the `vars:` entries they interpolate — are masked in
275
- the diff and never leave the host.
255
+ Permanent spawns write a bare Agent document to
256
+ `~/fleet/agents/<Name>.yaml`; generated-source ownership is recorded so removal
257
+ never deletes an unproven hand-written neighbor. `ours-fleet rm <Name>` unspawns.
258
+
259
+ The web editor reads and writes the split document model explicitly as
260
+ `{manifest, agents}`. Role and Brain presets are visible through Agent refs but
261
+ remain read-only. Saves use one aggregate revision over the manifest and all
262
+ Agent/Role/Brain sources, validate an exact-stem private staging tree, show a
263
+ deterministic redacted diff per document, and hold one manifest-root lock.
264
+ Changed/deleted files are backed up together; any partial failure restores every
265
+ document or reports the private recovery directory. An unchanged save is a
266
+ byte-for-byte no-op and creates no backup. Nested environment, authentication,
267
+ invite, and isolation secrets never leave the host.
276
268
 
277
269
  From inside Claude Code, Codex, or Hermes with the core `ours` plugin installed,
278
270
  say **"spawn an ours agent …"**. The core skill checks for `ours-fleet`, installs
@@ -290,16 +282,16 @@ ours-fleet peek Worker # what is it doing?
290
282
  ours-fleet send Worker "continue with the tests, then report"
291
283
  ```
292
284
 
293
- Declare standing assignments in `fleet.yaml` (rendered into the overseer's
285
+ Declare standing assignments in the overseer's bare Agent document (rendered into its
294
286
  briefing) — or just write "keep an eye on Alice and Bob every 5 minutes" in a
295
287
  persona; the bundled `oversee-agents` skill defines what that means operationally:
296
288
 
297
289
  ```yaml
298
- roles:
299
- FleetCoordinator:
300
- oversee:
301
- - { role: Alice, interval: 5m }
302
- - { role: Bob, interval: 5m }
290
+ role: { ref: coordinator }
291
+ brain: { ref: claude-default }
292
+ oversee:
293
+ - { agent: Alice, interval: 5m }
294
+ - { agent: Bob, interval: 5m }
303
295
  ```
304
296
 
305
297
  ## Command reference
@@ -310,7 +302,7 @@ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
310
302
  ours-fleet config [-c FILE] validate + print merged plan
311
303
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
312
304
  ours-fleet send <Name> "text"
313
- 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 ...]
314
306
  ours-fleet loops validate|list|status
315
307
  ours-fleet loops reload <Role>
316
308
  ours-fleet loops run-now|disable|enable <Role> <Loop>
@@ -342,33 +334,31 @@ in its state dir) across supervisor-triggered restarts — systemd/launchd re-in
342
334
  agent process with no arguments, so without this the role would silently fall back to
343
335
  the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
344
336
 
345
- ## fleet.yaml reference
337
+ ## split configuration field reference
346
338
 
347
- ```yaml
339
+ The following is a schematic field inventory, not a single YAML document. Host
340
+ settings belong in `~/fleet.yaml`; reusable behavior and harness settings belong
341
+ in `~/fleet/roles/<id>.yaml` and `~/fleet/brains/<id>.yaml`; operational fields
342
+ belong in `~/fleet/agents/<id>.yaml`.
343
+
344
+ ```text
348
345
  vars: { work_root: /home/me/work } # ${var} substitution anywhere below
346
+ api_version: ours.network/fleet/v2
349
347
  start_stagger_ms: 0 # delay between agent LAUNCHES (host-wide, ms); 0 = no stagger
350
348
  defaults:
351
- harness: claude-code # for roles that don't set one
352
- session: acp # supported Fleet↔agent session path
353
349
  permissions: # common intent, translated by each harness/backend
354
350
  approval: ask # ask | auto | allow (`deny` is a deprecated alias)
355
351
  filesystem: workspace # read-only | workspace | unrestricted
356
352
  unattended: deny # deny | wait
357
- model: claude-fable-5 # default model for roles that don't set one (per-role model / --model wins)
358
- max_tokens: 500000 # session cap (harness-interpreted)
359
353
  monitor:
360
354
  mode: fleet # fleet (default) | native
361
355
  worklog: # built-ins shown; set false to opt out
362
356
  max_kb: 1024 # rotate only above this active-log size
363
357
  keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
364
358
  max_archives: 12 # recent beside log; older preserved cold
365
- roles:
366
- Name: # [A-Za-z0-9_-]+
367
- harness: claude-code
368
- session: acp # optional; ACP is the only supported session
369
- session_options:
370
- acp:
371
- 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
372
362
  identity: "Display Name" # ours identity to bind (default: Name)
373
363
  cwd: ${work_root}/repo # where the harness process runs
374
364
  coordinator: FleetCoordinator # announce target on boot
@@ -400,7 +390,7 @@ roles:
400
390
  bio: | # public card (published as bio)
401
391
  briefing_file: curated.md # replaces the generated narrative
402
392
  env: { KEY: value } # extra session env
403
- oversee: [{ role: X, interval: 5m }]
393
+ oversee: [{ agent: X, interval: 5m }]
404
394
  harness_options: # adapter-owned, adapter-validated
405
395
  plugins: { "name@marketplace": false } # claude-code: plugin overrides
406
396
  # mem_palace: false # claude-code: disable memory plugin
@@ -438,13 +428,12 @@ loops: # trusted local scheduled ACP turns
438
428
  If nothing material changed, complete silently without an owner report.
439
429
  ```
440
430
 
441
- Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
442
- error naming both files. Identities and roles are decoupled — removing a role
443
- never deletes an identity. `session` is ACP-backed for both supported harnesses.
444
- `defaults.harness_options` is shallow-merged with each
445
- role's `harness_options`, so a fleet can set common Codex permission/profile defaults
446
- and override individual keys per role. `monitor` merges the same way — a role block
447
- overrides `defaults.monitor` key-by-key.
431
+ The v2 loader reads the manifest, then sorted bare Role, Brain, and Agent
432
+ documents from its exact stem directory. Duplicate filename-derived IDs are a
433
+ hard error. Identities and Agents are decoupled—removing an Agent never deletes
434
+ an identity. Role, Brain, and operational ownership are composed explicitly;
435
+ there is no generic cross-kind merge. Manifest operational defaults such as
436
+ `monitor` and `permissions` merge only within their owning Agent fields.
448
437
 
449
438
  Every supervised role is a client of the operator-configured ours daemon. Fleet strips the
450
439
  obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
@@ -1037,33 +1026,27 @@ the structured CLI watcher, because a detached process cannot wake a Codex turn.
1037
1026
  wait is re-entered after each handled message; `ours-codex` instead wakes the idle
1038
1027
  session through its App Server integration.
1039
1028
 
1040
- The main Codex controls needed by fleet roles are available declaratively and when spawning:
1029
+ The main Codex controls are Brain-owned and also available when spawning:
1041
1030
 
1042
1031
  ```yaml
1043
- defaults:
1044
- harness: codex
1045
- model: gpt-5.4
1046
- harness_options:
1047
- launcher: auto
1048
- profile: fleet # $CODEX_HOME/fleet.config.toml
1049
- sandbox: workspace-write
1050
- approval: on-request
1051
- monitor: true # consent for Codex's native monitor; not the wake-owner selector
1052
- search: true
1053
- add_dirs: [/data/shared]
1054
- config:
1055
- model_reasoning_effort: high
1056
-
1057
- roles:
1058
- Reviewer:
1059
- harness_options:
1060
- sandbox: read-only # overrides just this default key
1032
+ # ~/fleet/brains/codex-review.yaml
1033
+ harness: codex
1034
+ session: acp
1035
+ model: gpt-5.4
1036
+ harness_options:
1037
+ launcher: auto
1038
+ profile: fleet
1039
+ sandbox: read-only
1040
+ approval: on-request
1041
+ monitor: true
1042
+ search: true
1043
+ add_dirs: [/data/shared]
1044
+ config: { model_reasoning_effort: high }
1061
1045
  ```
1062
1046
 
1063
- Equivalent one-off/permanent spawn controls include `--model`, `--permission-mode`,
1064
- `--sandbox`, `--profile`, `--launcher`, `--search`, legacy `--monitor` (native
1065
- Codex monitor consent), repeatable
1066
- `--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
1067
1050
  role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
1068
1051
 
1069
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,16 +1,15 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
3
3
  import { isAbsolute, relative } from 'node:path';
4
- import { loadConfig, NOTIFY_EVENT_TYPES, resolveMonitorConfig, resolveRoleModel, resolvePermissions, ROLE_NAME_RE, validateMonitorConfig, } from '../config.js';
4
+ import { loadConfig, NOTIFY_EVENT_TYPES, resolveMonitorConfig, resolvePermissions, ROLE_NAME_RE, validateMonitorConfig, } from '../config.js';
5
5
  import { daemonIdentityProvisioner, } from '../creation.js';
6
6
  import { replaceFileAtomically } from '../atomic-file.js';
7
7
  import { stateRoot } from '../paths.js';
8
- import { buildRoleConfig, spawnDryRun, spawnPermanent, spawnTemp, validateSpawnOpts, } from '../spawn.js';
8
+ import { spawnDryRun, spawnPermanent, spawnTemp, validateSpawnOpts, } from '../spawn.js';
9
9
  import { FleetError, normalizeError } from './errors.js';
10
10
  import { inheritCallerSpawnDefaults } from '../fleet-proxy.js';
11
11
  import { effectivePermissionMode } from '../permissions.js';
12
12
  import { effectiveRoleModel } from '../model-env.js';
13
- import { claudeModelCatalog, codexModelCatalog, } from './model-catalog.js';
14
13
  const canonical = (value) => {
15
14
  if (Array.isArray(value))
16
15
  return `[${value.map(canonical).join(',')}]`;
@@ -80,47 +79,16 @@ export class RoleCreationService {
80
79
  async capabilities() {
81
80
  const reasons = [];
82
81
  let defaults = {};
83
- let roles = [];
84
82
  try {
85
83
  const config = loadConfig(this.options.configPath);
86
84
  defaults = config.defaults;
87
- roles = config.roles;
88
85
  }
89
86
  catch (error) {
90
87
  reasons.push(`configuration is invalid: ${error.message}`);
91
88
  }
92
- const modelsFor = (harness, catalog) => {
93
- const configured = roles.filter(role => role.harness === harness)
94
- .flatMap(role => [role.model, ...(role.model_chain ?? [])])
95
- .filter((model) => Boolean(model));
96
- const inherited = resolveRoleModel(undefined, harness, defaults);
97
- const configuredOptions = [...new Set([...(inherited ? [inherited] : []), ...configured])]
98
- .filter(id => !catalog.models.some(model => model.id === id))
99
- .map(id => ({ id, label: `${id} (configured)`, reasoningEfforts: [], source: 'fleet-config' }));
100
- const modelOptions = [...configuredOptions, ...catalog.models];
101
- return { modelOptions, models: modelOptions.map(model => model.id) };
102
- };
103
- const codexCatalog = this.options.modelCatalogs?.codex?.() ?? codexModelCatalog();
104
- const claudeCatalog = this.options.modelCatalogs?.['claude-code']?.() ?? claudeModelCatalog();
105
- const codexModels = modelsFor('codex', codexCatalog);
106
- const claudeModels = modelsFor('claude-code', claudeCatalog);
107
89
  return {
108
90
  available: reasons.length === 0,
109
91
  reasons,
110
- harnesses: [
111
- {
112
- id: 'codex', available: true, sessions: ['acp'],
113
- defaultModel: resolveRoleModel(undefined, 'codex', defaults),
114
- ...codexModels, catalogSource: 'codex-runtime-catalog', customModelAllowed: true,
115
- warnings: codexCatalog.warnings,
116
- },
117
- {
118
- id: 'claude-code', available: true, sessions: ['acp'],
119
- defaultModel: resolveRoleModel(undefined, 'claude-code', defaults),
120
- ...claudeModels, catalogSource: 'claude-adapter-2.1', customModelAllowed: true,
121
- warnings: claudeCatalog.warnings,
122
- },
123
- ],
124
92
  lifetimes: ['permanent', 'temporary'],
125
93
  identityBootstrap: {
126
94
  mode: 'current-fleet-first-boot',
@@ -144,14 +112,12 @@ export class RoleCreationService {
144
112
  const defaults = cfg.defaults;
145
113
  const opts = this.spawnOptions(request);
146
114
  validateSpawnOpts(opts);
147
- buildRoleConfig(opts, defaults.harness);
115
+ const resolved = spawnDryRun(opts).resolvedRole;
148
116
  const cwd = request.cwd ? this.resolveCwd(request.cwd) : undefined;
149
117
  const effective = {
150
118
  name: request.name, identity: request.name,
151
- harness: request.harness ?? defaults.harness ?? 'claude-code',
152
- session: request.session ?? defaults.session ?? 'acp',
153
- model: resolveRoleModel(request.model, request.harness, defaults),
154
- reasoningEffort: request.reasoningEffort ?? undefined,
119
+ harness: resolved.harness, session: resolved.session, model: resolved.model,
120
+ reasoningEffort: resolved.effort,
155
121
  cwd, lifetime: request.lifetime,
156
122
  permissions: resolvePermissions(defaults.permissions, request.permissions),
157
123
  monitor: resolveMonitorConfig(defaults.monitor, request.monitor),
@@ -183,9 +149,7 @@ export class RoleCreationService {
183
149
  if (existingIdentity === 'unknown' && !request.unverifiedIdentityAcknowledged)
184
150
  prerequisites.push('confirm creation with an unverified identity preflight');
185
151
  const provenance = {
186
- harness: 'request', session: 'request', identity: 'built-in',
187
- model: request.model !== undefined ? 'request'
188
- : resolveRoleModel(undefined, request.harness, defaults) ? 'fleet-default' : 'built-in',
152
+ brain: 'request', role: 'request', identity: 'built-in',
189
153
  cwd: request.cwd ? 'request' : 'built-in', permissions: 'request',
190
154
  monitor: request.monitor ? 'request' : defaults.monitor ? 'fleet-default' : 'built-in',
191
155
  };
@@ -296,8 +260,8 @@ export class RoleCreationService {
296
260
  }
297
261
  validate(input) {
298
262
  const allowed = new Set([
299
- 'name', 'harness', 'model', 'reasoningEffort', 'session', 'cwd', 'lifetime', 'mission',
300
- 'coordinator', 'permissions', 'bio', 'persona', 'monitor', 'openAfterCreate',
263
+ 'name', 'brain', 'role', 'cwd', 'lifetime',
264
+ 'coordinator', 'permissions', 'monitor', 'openAfterCreate',
301
265
  'highRiskAcknowledged', 'reuseExistingIdentityAcknowledged',
302
266
  'unverifiedIdentityAcknowledged',
303
267
  ]);
@@ -307,20 +271,11 @@ export class RoleCreationService {
307
271
  throw new FleetError('invalid_request', `unsupported web creation field: ${unsupported[0]}`);
308
272
  if (!ROLE_NAME_RE.test(input.name))
309
273
  throw new FleetError('invalid_request', 'invalid role name');
310
- if (!['codex', 'claude-code'].includes(input.harness))
311
- throw new FleetError('invalid_request', 'unsupported harness');
312
- if (input.session !== 'acp')
313
- 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');
314
276
  if (!['permanent', 'temporary'].includes(input.lifetime))
315
277
  throw new FleetError('invalid_request', 'unsupported lifetime');
316
- bounded(input.model ?? undefined, 'model', 128);
317
- bounded(input.reasoningEffort ?? undefined, 'reasoning effort', 16);
318
- if (input.reasoningEffort != null && !['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(input.reasoningEffort))
319
- throw new FleetError('invalid_request', 'unsupported reasoning effort');
320
- bounded(input.mission, 'mission', 4_096);
321
278
  bounded(input.coordinator, 'coordinator', 128);
322
- bounded(input.bio, 'bio', 8_192);
323
- bounded(input.persona, 'persona', 16_384);
324
279
  resolvePermissions(undefined, input.permissions);
325
280
  if (input.monitor) {
326
281
  const problems = validateMonitorConfig(input.monitor);
@@ -330,10 +285,8 @@ export class RoleCreationService {
330
285
  throw new FleetError('invalid_request', 'web creation supports monitor.inject=notification only');
331
286
  }
332
287
  return {
333
- ...input, model: input.model === null ? null : input.model?.trim() || undefined,
334
- reasoningEffort: input.reasoningEffort === null ? null : input.reasoningEffort?.trim() || undefined,
335
- mission: input.mission?.trim() || undefined, coordinator: input.coordinator?.trim() || undefined,
336
- 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,
337
290
  monitor: input.monitor ? structuredClone(input.monitor) : undefined,
338
291
  };
339
292
  }
@@ -360,12 +313,10 @@ export class RoleCreationService {
360
313
  spawnOptions(request, creationActionId) {
361
314
  return {
362
315
  name: request.name, temp: request.lifetime === 'temporary',
363
- identity: request.name, harness: request.harness,
364
- model: request.model, session: request.session, cwd: request.cwd,
365
- reasoningEffort: request.reasoningEffort,
366
- mission: request.mission, coordinator: request.coordinator,
316
+ identity: request.name, brain: request.brain, role: request.role, cwd: request.cwd,
317
+ coordinator: request.coordinator,
367
318
  approval: request.permissions.approval, filesystem: request.permissions.filesystem,
368
- unattended: request.permissions.unattended, bio: request.bio, persona: request.persona,
319
+ unattended: request.permissions.unattended,
369
320
  monitorConfig: request.monitor,
370
321
  configPath: this.options.configPath,
371
322
  surface: creationActionId ? 'web' : undefined, creationActionId,
@@ -4,6 +4,7 @@ import { loadConfig, ROLE_NAME_RE } from '../config.js';
4
4
  import { agentDir, stateRoot } from '../paths.js';
5
5
  import { rmRole } from '../ops.js';
6
6
  import { FleetError } from './errors.js';
7
+ import { provesGeneratedAgentSource } from '../generated-agent-source.js';
7
8
  export class RoleRemovalService {
8
9
  options;
9
10
  constructor(options) {
@@ -45,8 +46,9 @@ export class RoleRemovalService {
45
46
  `Stop and uninstall the exact backend registration for '${requested}'.`,
46
47
  `Archive then remove ${lifetime === 'temporary' ? temporaryState : permanentState}.`,
47
48
  ];
48
- if (role?.sourceFile.includes('/fleet.d/'))
49
- effects.push(`Remove spawned configuration ${role.sourceFile}.`);
49
+ const generatedSource = role && provesGeneratedAgentSource(agentDir(requested), cfg.files[0], role.sourceFile);
50
+ if (generatedSource)
51
+ effects.push(`Remove generated Agent configuration ${role.sourceFile}.`);
50
52
  else if (role)
51
53
  effects.push(`Keep hand-written configuration ${role.sourceFile}; the role can be recreated from it.`);
52
54
  else
@@ -76,7 +78,8 @@ export class RoleRemovalService {
76
78
  mkdirSync(recoveryPath, { recursive: true, mode: 0o700 });
77
79
  if (existsSync(sourceState))
78
80
  cpSync(sourceState, join(recoveryPath, 'state'), { recursive: true });
79
- if (role?.sourceFile.includes('/fleet.d/') && existsSync(role.sourceFile))
81
+ if (role && provesGeneratedAgentSource(agentDir(preview.role), cfg.files[0], role.sourceFile)
82
+ && existsSync(role.sourceFile))
80
83
  cpSync(role.sourceFile, join(recoveryPath, basename(role.sourceFile)));
81
84
  if (role || preview.lifetime === 'temporary')
82
85
  await rmRole(cfg, preview.role, this.options.ops);
@@ -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
  }
@@ -18,7 +18,7 @@ export interface ResolvedRoleView {
18
18
  sandbox?: string;
19
19
  };
20
20
  oversee?: Array<{
21
- role: string;
21
+ agent: string;
22
22
  interval: string;
23
23
  }>;
24
24
  }
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) {
@@ -196,12 +196,12 @@ export function generateBriefing(role, v, opts) {
196
196
  L.push('', '## Oversight assignments');
197
197
  L.push('These agents are your wards — you keep them unstuck:');
198
198
  for (const o of role.oversee)
199
- L.push(`- **${o.role}** — check every ${o.interval}`);
199
+ L.push(`- **${o.agent}** — check every ${o.interval}`);
200
200
  L.push('');
201
201
  L.push('Procedure (see also the oversee-agents skill if available). On each tick, for each ward');
202
202
  L.push('run BOTH — they answer different questions:');
203
203
  for (const o of role.oversee)
204
- L.push(`\`ours-fleet status ${o.role}\` then \`ours-fleet peek ${o.role}\``);
204
+ L.push(`\`ours-fleet status ${o.agent}\` then \`ours-fleet peek ${o.agent}\``);
205
205
  L.push('');
206
206
  L.push('**One console command is not a liveness verdict.** A `peek` or `send` that fails tells');
207
207
  L.push('you what happened to YOUR REQUEST, and only one of its outcomes says the agent is gone.');
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.1",
3
- "buildId": "bf3c406ba57c",
4
- "commit": "10d6da8bfd9903d216bce80a9ef89a42c282f8d9",
2
+ "version": "1.1.0-nightly.3",
3
+ "buildId": "ecb06fff91d3",
4
+ "commit": "87f6db6848b924074b76ce19f15a6b15866c4da3",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-30T07:44:27.551Z",
6
+ "builtAt": "2026-08-30T17:21:38.816Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]