@ours.network/fleet 2.2.6 → 2.3.1

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 (140) hide show
  1. package/README.md +1 -1
  2. package/dist/application/harness-capabilities.d.ts +60 -0
  3. package/dist/application/harness-capabilities.js +96 -0
  4. package/dist/application/model-catalog.d.ts +17 -0
  5. package/dist/application/model-catalog.js +32 -9
  6. package/dist/application/preset-provenance.d.ts +1 -1
  7. package/dist/application/preset-provenance.js +18 -3
  8. package/dist/application/role-creation-service.d.ts +3 -1
  9. package/dist/application/role-creation-service.js +4 -1
  10. package/dist/application/task-room-service.d.ts +12 -1
  11. package/dist/application/task-room-service.js +39 -16
  12. package/dist/atomic-file.d.ts +2 -0
  13. package/dist/atomic-file.js +2 -1
  14. package/dist/build-info.json +4 -4
  15. package/dist/config.d.ts +6 -0
  16. package/dist/config.js +6 -3
  17. package/dist/docs.d.ts +1 -1
  18. package/dist/docs.js +37 -0
  19. package/dist/harness/claude-code.js +7 -0
  20. package/dist/harness/codex.js +7 -0
  21. package/dist/harness/hermes.js +2 -0
  22. package/dist/harness/types.d.ts +20 -0
  23. package/dist/init-wizard.d.ts +19 -12
  24. package/dist/init-wizard.js +122 -104
  25. package/dist/owner-channel/commands.js +25 -12
  26. package/dist/reports/tasks.js +4 -3
  27. package/dist/rooms-tasks/add-member.d.ts +15 -1
  28. package/dist/rooms-tasks/add-member.js +44 -10
  29. package/dist/rooms-tasks/cli.js +79 -5
  30. package/dist/rooms-tasks/config.js +11 -0
  31. package/dist/rooms-tasks/layout-member-retirement.d.ts +15 -0
  32. package/dist/rooms-tasks/layout-member-retirement.js +26 -1
  33. package/dist/rooms-tasks/layout-service.d.ts +2 -1
  34. package/dist/rooms-tasks/layout-service.js +10 -4
  35. package/dist/rooms-tasks/layout.d.ts +4 -2
  36. package/dist/rooms-tasks/layout.js +21 -4
  37. package/dist/rooms-tasks/member-naming.d.ts +43 -0
  38. package/dist/rooms-tasks/member-naming.js +186 -0
  39. package/dist/rooms-tasks/member-overrides.js +1 -0
  40. package/dist/rooms-tasks/mission-keys.d.ts +83 -0
  41. package/dist/rooms-tasks/mission-keys.js +250 -0
  42. package/dist/rooms-tasks/provision.d.ts +4 -0
  43. package/dist/rooms-tasks/provision.js +37 -3
  44. package/dist/rooms-tasks/task-lists.d.ts +18 -2
  45. package/dist/rooms-tasks/task-lists.js +60 -5
  46. package/dist/rooms-tasks/task-room-name.d.ts +7 -2
  47. package/dist/rooms-tasks/task-room-name.js +13 -4
  48. package/dist/rooms-tasks/task-state.d.ts +40 -0
  49. package/dist/rooms-tasks/task-state.js +288 -45
  50. package/dist/rooms-tasks/types.d.ts +35 -1
  51. package/dist/rooms-tasks/types.js +1 -1
  52. package/dist/web/runtime.js +4 -1
  53. package/dist/web/server.js +116 -50
  54. package/package.json +1 -1
  55. package/presets/README.md +38 -18
  56. package/presets/brain-catalog.json +19 -19
  57. package/presets/legacy/generated-brains-rev4.json +497 -0
  58. package/presets/manifest.json +1 -1
  59. package/presets/fleet/brains/claude-fable-5-1-high.yaml +0 -5
  60. package/presets/fleet/brains/claude-fable-5-1-low.yaml +0 -5
  61. package/presets/fleet/brains/claude-fable-5-1-max.yaml +0 -5
  62. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +0 -5
  63. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +0 -5
  64. package/presets/fleet/brains/claude-fable-5-high.yaml +0 -5
  65. package/presets/fleet/brains/claude-fable-5-low.yaml +0 -5
  66. package/presets/fleet/brains/claude-fable-5-max.yaml +0 -5
  67. package/presets/fleet/brains/claude-fable-5-medium.yaml +0 -5
  68. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +0 -5
  69. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +0 -4
  70. package/presets/fleet/brains/claude-opus-5-5-high.yaml +0 -5
  71. package/presets/fleet/brains/claude-opus-5-5-low.yaml +0 -5
  72. package/presets/fleet/brains/claude-opus-5-5-max.yaml +0 -5
  73. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +0 -5
  74. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +0 -5
  75. package/presets/fleet/brains/claude-opus-5-high.yaml +0 -5
  76. package/presets/fleet/brains/claude-opus-5-low.yaml +0 -5
  77. package/presets/fleet/brains/claude-opus-5-max.yaml +0 -5
  78. package/presets/fleet/brains/claude-opus-5-medium.yaml +0 -5
  79. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +0 -5
  80. package/presets/fleet/brains/claude-sonnet-5-high.yaml +0 -5
  81. package/presets/fleet/brains/claude-sonnet-5-low.yaml +0 -5
  82. package/presets/fleet/brains/claude-sonnet-5-max.yaml +0 -5
  83. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +0 -5
  84. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +0 -5
  85. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +0 -5
  86. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +0 -5
  87. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +0 -5
  88. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +0 -5
  89. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +0 -5
  90. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +0 -5
  91. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +0 -5
  92. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +0 -5
  93. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +0 -5
  94. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +0 -5
  95. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +0 -5
  96. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +0 -5
  97. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +0 -5
  98. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +0 -5
  99. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +0 -5
  100. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +0 -5
  101. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +0 -5
  102. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +0 -5
  103. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +0 -5
  104. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +0 -5
  105. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +0 -5
  106. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +0 -5
  107. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +0 -5
  108. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +0 -5
  109. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +0 -5
  110. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +0 -5
  111. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +0 -5
  112. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +0 -5
  113. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +0 -5
  114. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +0 -5
  115. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +0 -5
  116. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +0 -5
  117. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +0 -5
  118. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +0 -5
  119. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +0 -5
  120. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +0 -5
  121. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +0 -5
  122. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +0 -5
  123. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +0 -5
  124. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +0 -5
  125. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +0 -5
  126. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +0 -5
  127. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +0 -5
  128. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +0 -5
  129. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +0 -5
  130. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +0 -5
  131. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +0 -5
  132. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +0 -5
  133. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +0 -5
  134. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +0 -5
  135. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +0 -5
  136. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +0 -5
  137. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +0 -5
  138. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +0 -5
  139. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +0 -5
  140. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +0 -5
@@ -174,6 +174,15 @@ export interface TaskRecord {
174
174
  notification_manifest?: import('./notification-manifest.js').TaskNotificationManifest;
175
175
  workspace?: import('./workspace.js').OwnedWorkspace;
176
176
  task_id: string;
177
+ /**
178
+ * Immutable human mission key (`MSN-1`), issued once from a durable
179
+ * per-prefix high-water mark. Absent only before the one-time migration.
180
+ */
181
+ key?: string;
182
+ /** Set when the key was backfilled for a task that existed before mission keys. */
183
+ key_from_migration?: true;
184
+ /** Durable per-mission agent name allocations; absent until first allocation or migration. */
185
+ member_naming?: MemberNaming;
177
186
  /** Stable organizational list identifier. Missing legacy values mean `default`. */
178
187
  list_id: string;
179
188
  /** Derived presentation field; repositories must never persist it. */
@@ -211,9 +220,30 @@ export interface TaskRecord {
211
220
  terminal_intent?: TaskTerminalIntent;
212
221
  deletion?: TaskDeletionIntent;
213
222
  }
223
+ /**
224
+ * Role-scoped agent names: `<KEY>-<Stem>` for the first member of a stem,
225
+ * `<KEY>-<Stem>-<n>` (n >= 2) after it. Counters only grow; an allocation is
226
+ * reused by provisioning, recovery and restart, never by a replacement.
227
+ */
228
+ export interface MemberNameAllocation {
229
+ name: string;
230
+ /** Counter bucket: the case-folded truncated stem. Absent on frozen legacy names. */
231
+ bucket?: string;
232
+ ordinal?: number;
233
+ label_source?: 'role_override' | 'template_ref' | 'room_role' | 'participant_key' | 'slot' | 'fallback';
234
+ /** A historical name kept exactly as launched before mission keys existed. */
235
+ frozen?: true;
236
+ }
237
+ export interface MemberNaming {
238
+ version: 2;
239
+ counters: Record<string, number>;
240
+ allocations: Record<string, MemberNameAllocation>;
241
+ }
214
242
  export interface TaskListRecord {
215
243
  list_id: string;
216
244
  name: string;
245
+ /** Prefix for new mission keys; absent only before the one-time key migration. */
246
+ prefix?: string;
217
247
  built_in: boolean;
218
248
  created_at: string;
219
249
  }
@@ -387,6 +417,8 @@ export interface TemplateSnapshotMember extends TemplateMemberSlot {
387
417
  id: string;
388
418
  hash: string;
389
419
  };
420
+ /** Named Role of the member's Agent Template, sealed at plan time for agent naming. */
421
+ role_label?: string;
390
422
  }
391
423
  export interface TemplateSnapshot extends Omit<TemplateDefinition, 'members'> {
392
424
  members: TemplateSnapshotMember[];
@@ -418,13 +450,15 @@ export interface TasksConfig {
418
450
  create_mode?: 'start' | 'backlog';
419
451
  close_room_on_done?: boolean;
420
452
  retain_completed_for?: string;
453
+ /** Default list prefix (default MSN); may change only before the first mission key. */
454
+ default_prefix?: string;
421
455
  }
422
456
  export type RoomTemplatesConfig = Record<string, TemplateDefinition>;
423
457
  export declare const ROOMS_KEYS: readonly ["cowork", "owner", "defaults"];
424
458
  export declare const ROOMS_OWNER_KEYS: readonly ["provider", "public_invite", "public_invite_file", "expected_cid", "role"];
425
459
  export declare const ROOMS_COWORK_KEYS: readonly ["config"];
426
460
  export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
427
- export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
461
+ export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for", "default_prefix"];
428
462
  export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
429
463
  export declare const TEMPLATE_ROOM_KEYS: readonly ["quiet_membership", "anonymous"];
430
464
  /** `agent` is accepted only so validation can emit its actionable migration error. */
@@ -23,7 +23,7 @@ export const ROOMS_KEYS = ['cowork', 'owner', 'defaults'];
23
23
  export const ROOMS_OWNER_KEYS = ['provider', 'public_invite', 'public_invite_file', 'expected_cid', 'role'];
24
24
  export const ROOMS_COWORK_KEYS = ['config'];
25
25
  export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
26
- export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
26
+ export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for', 'default_prefix'];
27
27
  export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
28
28
  export const TEMPLATE_ROOM_KEYS = ['quiet_membership', 'anonymous'];
29
29
  /** `agent` is accepted only so validation can emit its actionable migration error. */
@@ -193,13 +193,16 @@ export async function startWebConsole(options) {
193
193
  const topologyPromote = new TopologyPromoteService({
194
194
  drafts: topologyDrafts, configuration, topology: readTopology,
195
195
  });
196
+ const taskRooms = new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath });
197
+ // One-time, lock-guarded mission key migration before serving; failure leaves it to the next mutation.
198
+ await taskRooms.prepareMissionKeys().catch(error => console.warn(`mission key migration deferred: ${error.message}`));
196
199
  let server;
197
200
  try {
198
201
  server = await buildWebServer({
199
202
  query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration, observeRole,
200
203
  resourceScope: { configPath: configuration.path, subscribeMutations: listener => events.subscribeResources(listener) },
201
204
  invalidateResourceCaches: () => { watchdogConfigProvider.invalidate(); watchdogFindings.invalidate(); },
202
- taskRooms: new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath }),
205
+ taskRooms,
203
206
  roomLayouts: new RoomLayoutDefinitions(options.configPath),
204
207
  layoutBindings: new LayoutBindingGrants(new RoomLayoutService(options.configPath).supervisor()),
205
208
  presetProvenance: new PresetProvenance(options.configPath),
@@ -7,15 +7,13 @@ import { registerConversationStream } from './conversation-stream.js';
7
7
  import { conversationHasAttachment } from '../application/conversation-history.js';
8
8
  import { readDeliveredFile } from '../file-delivery/store.js';
9
9
  import { WorkspaceDeviceAuthError } from './workspace-devices.js';
10
- import { readFileSync } from 'node:fs';
11
- import { packagedPresetRoot } from '../preset-bootstrap.js';
12
- import { validateCatalog } from '../init-wizard.js';
10
+ import { PURPOSE_BRAINS, PurposeBrainConflictError } from '../init-wizard.js';
11
+ import { harnessCapabilities, validateBrainChoice } from '../application/harness-capabilities.js';
13
12
  import { getAdapter } from '../harness/registry.js';
14
13
  import { storeAgentAttachment, attachmentPrompt, MAX_ATTACHMENT_BODY, readAgentAttachment, prepareAttachmentPresentation, presentAttachmentEvent } from './agent-attachments.js';
15
14
  import { readChatIdle } from '../temp-idle.js';
16
15
  import { agentDir } from '../paths.js';
17
16
  import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
18
- import { join } from 'node:path';
19
17
  import Fastify from 'fastify';
20
18
  import fastifyWebsocket from '@fastify/websocket';
21
19
  import { createHmac, randomBytes } from 'node:crypto';
@@ -26,7 +24,8 @@ import { WebAuth } from './auth.js';
26
24
  import { FleetEventBus } from './events.js';
27
25
  import { ROLE_NAME_RE } from '../config.js';
28
26
  import { TaskListError } from '../rooms-tasks/task-lists.js';
29
- import { acknowledgeTaskProgress, acknowledgeTaskReview, contributionUnread, TaskStateError } from '../rooms-tasks/task-state.js';
27
+ import { acknowledgeTaskProgress, acknowledgeTaskReview, contributionUnread, resolveTaskRef, TaskStateError } from '../rooms-tasks/task-state.js';
28
+ import { MissionKeyError } from '../rooms-tasks/mission-keys.js';
30
29
  import { appendTaskProgress, getTaskReview, taskProgressSnapshot, publicProgressEntry, publicReview, readTaskReviewArtifact, setTaskReviewMarkdown, taskContributionSummary, TaskReviewError, } from '../rooms-tasks/task-review.js';
31
30
  import { isProvider } from '../subscriptions/store.js';
32
31
  const ONBOARDING_WORK = ['coordination', 'development', 'review'];
@@ -214,12 +213,12 @@ export async function buildWebServer(services, boundary, options = {}) {
214
213
  return { harness, ok: false, checks: [{ name: 'availability', ok: false, detail: 'Harness checks unavailable' }] };
215
214
  }
216
215
  }));
217
- const catalog = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8')));
218
- // Codex models are offered only when the runtime itself listed them for the active account.
219
- // When it could not be asked, none is offered and the reason is reported.
220
- const codexModels = services.codexModels ? await services.codexModels() : undefined;
221
- const offered = (model) => model.harness !== 'codex' || !codexModels || (codexModels.state === 'known' && codexModels.models.includes(model.model));
222
- const payload = { harnesses, catalog: catalog.models.filter(offered), ...(codexModels ? { codexModels: codexModels.state === 'known' ? { state: 'known' } : codexModels } : {}), providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator', configured: onboardingConfigured() };
216
+ // The catalog is each adapter's maintained models, narrowed to what the active Codex account offers.
217
+ const capabilities = await harnessCapabilities({ harnesses: ['codex', 'claude-code'], codexModels: services.codexModels, checkReadiness: false });
218
+ const codex = capabilities.harnesses.find(item => item.harness === 'codex');
219
+ const catalog = capabilities.harnesses.flatMap(item => item.models.filter(model => model.available && model.efforts !== null)
220
+ .map(model => ({ harness: item.harness, session: 'acp', model: model.id, label: model.label, efforts: model.efforts })));
221
+ const payload = { harnesses, catalog, ...(codex?.discovery ? { codexModels: codex.discovery } : {}), providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator', configured: onboardingConfigured() };
223
222
  if (revision === services.configuration?.read().revision) {
224
223
  onboardingObserved = { owner: browser.id, revision, payload };
225
224
  events.publish('onboarding.changed', {});
@@ -244,15 +243,25 @@ export async function buildWebServer(services, boundary, options = {}) {
244
243
  if (!services.configuration || !services.onboardingSetup)
245
244
  throw new FleetError('capability_unavailable', 'first-time Fleet setup is unavailable');
246
245
  const requested = request.body?.models;
247
- const codexModels = services.codexModels ? await services.codexModels() : undefined;
248
- const supported = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8'))).models
249
- .filter(model => model.harness !== 'codex' || !codexModels || (codexModels.state === 'known' && codexModels.models.includes(model.model)));
246
+ const capabilities = await harnessCapabilities({ harnesses: ['codex', 'claude-code'], codexModels: services.codexModels, checkReadiness: false });
250
247
  const models = {};
251
248
  for (const work of ONBOARDING_WORK) {
252
- const matches = supported.filter(model => model.harness === requested?.[work]?.harness && model.model === requested?.[work]?.model);
253
- if (matches.length !== 1)
254
- throw new FleetError('invalid_request', `choose a supported model for ${work}`);
255
- models[work] = matches[0];
249
+ const choice = requested?.[work];
250
+ const label = PURPOSE_BRAINS[work].label;
251
+ if (!choice || typeof choice !== 'object' || !('effort' in choice))
252
+ throw new FleetError('invalid_request', `choose a harness, model and effort for the ${label} Brain`);
253
+ const { harness, model, effort } = choice;
254
+ if (typeof harness !== 'string' || typeof model !== 'string' || (effort !== null && typeof effort !== 'string'))
255
+ throw new FleetError('invalid_request', `choose a harness, model and effort for the ${label} Brain`);
256
+ const check = validateBrainChoice(capabilities, { harness, session: 'acp', model, effort });
257
+ // Setup offers the adapters' maintained models only, each with an explicit effort when it has one.
258
+ if (!check.ok)
259
+ throw new FleetError('invalid_request', `${label} Brain: ${check.message}`);
260
+ if (!check.model || check.model.source !== 'adapter')
261
+ throw new FleetError('invalid_request', `${label} Brain: choose a supported model`);
262
+ if (check.model.efforts?.length && effort === null)
263
+ throw new FleetError('invalid_request', `${label} Brain: choose an effort for ${check.model.label} (${check.model.efforts.join(', ')})`);
264
+ models[work] = { harness: harness, session: 'acp', model, effort: effort };
256
265
  }
257
266
  const chosen = Object.values(models);
258
267
  const subscriptions = [...new Set(chosen.map(model => model.harness === 'codex' ? 'codex' : 'claude'))];
@@ -263,31 +272,49 @@ export async function buildWebServer(services, boundary, options = {}) {
263
272
  if (onboardingConfigured())
264
273
  throw new FleetError('conflict', 'Fleet is already set up; change its models in the configuration');
265
274
  try {
266
- await setup({ subscriptions, assignmentStrategy: oneModel ? 'one-model' : 'per-job', models, reasoning: 'balanced' });
275
+ await setup({ subscriptions, assignmentStrategy: oneModel ? 'one-model' : 'per-job', models });
267
276
  }
268
277
  catch (error) {
278
+ if (error instanceof PurposeBrainConflictError)
279
+ throw new FleetError('conflict', error.message);
269
280
  throw new FleetError('backend_failure', `Fleet setup did not complete: ${error.message}`);
270
281
  }
271
- // Setup preserves files it finds, so confirm the published models are the ones this request chose.
272
- // The prepared agents reference packaged Brain presets, so resolve what each of them now points at:
273
- // a preserved file of a packaged name may hold another model, session or reasoning than its name says.
282
+ // Setup preserves files it finds, so confirm each prepared agent now resolves to exactly the chosen triple.
274
283
  const published = configuration.read(true).model, brains = published.brains ?? {};
275
284
  const brainOf = (holder) => brains[String(holder?.brain?.ref ?? '')];
276
285
  const assigned = { coordination: brainOf(published.agents.FleetCoordinator), development: brainOf(published.agent_templates?.Developer), review: brainOf(published.agent_templates?.Critic) };
277
286
  for (const work of ONBOARDING_WORK)
278
- if (assigned[work]?.harness !== models[work].harness || assigned[work]?.model !== models[work].model || assigned[work]?.session !== models[work].session || assigned[work]?.effort !== 'medium')
287
+ if (assigned[work]?.harness !== models[work].harness || assigned[work]?.model !== models[work].model || assigned[work]?.session !== models[work].session || (assigned[work]?.effort ?? null) !== models[work].effort)
279
288
  throw new FleetError('conflict', 'Fleet was set up with different models; review its configuration');
280
289
  return { configured: true };
281
290
  });
282
291
  onboardingSetupQueue = run.catch(() => { });
283
292
  return run;
284
293
  });
294
+ app.get('/api/v1/harness-capabilities', async (request) => {
295
+ auth.authenticate(request);
296
+ return harnessCapabilities({ codexModels: services.codexModels });
297
+ });
285
298
  app.get('/api/v1/creation-capabilities', async (request) => {
286
299
  auth.authenticate(request);
287
300
  return services.creation.capabilities();
288
301
  });
302
+ // A new agent's own harness/model/effort must be one its adapter supports; saved Brains are used as they are.
303
+ const checkBrainChoice = async (brain) => {
304
+ if (!brain || typeof brain !== 'object')
305
+ return;
306
+ const value = brain;
307
+ const check = validateBrainChoice(await harnessCapabilities({ codexModels: services.codexModels, checkReadiness: false }), {
308
+ harness: String(value.harness), session: value.session,
309
+ model: value.model, effort: value.effort
310
+ });
311
+ if (!check.ok)
312
+ throw new FleetError('invalid_request', `Brain: ${check.message}`);
313
+ };
314
+ const checkInlineBrain = (creation) => checkBrainChoice(creation?.brain?.inline);
289
315
  app.post('/api/v1/roles/preview', async (request) => {
290
316
  auth.authenticate(request, true);
317
+ await checkInlineBrain(request.body);
291
318
  return services.creation.preview(request.body);
292
319
  });
293
320
  app.post('/api/v1/roles', async (request, reply) => {
@@ -296,7 +323,7 @@ export async function buildWebServer(services, boundary, options = {}) {
296
323
  const idempotencyKey = String(request.headers['idempotency-key'] ?? '');
297
324
  if (!body?.request || !body.previewHash)
298
325
  throw new FleetError('invalid_request', 'request and previewHash are required');
299
- const action = await services.creation.create(body.request, body.previewHash, idempotencyKey, session.id);
326
+ const action = await services.creation.create(body.request, body.previewHash, idempotencyKey, session.id, checkInlineBrain);
300
327
  events.publish('creation.changed', action, action.roleId);
301
328
  reply.header('Location', `/api/v1/creation-actions/${encodeURIComponent(action.actionId)}`);
302
329
  reply.code(202);
@@ -321,6 +348,8 @@ export async function buildWebServer(services, boundary, options = {}) {
321
348
  ? 'conflict' : 'invalid_request';
322
349
  throw new FleetError(code, error.message);
323
350
  }
351
+ if (error instanceof MissionKeyError)
352
+ throw new FleetError(error.code === 'invalid_prefix' ? 'invalid_request' : 'conflict', error.message, { details: { reason: error.code } });
324
353
  if (error instanceof TaskStateError && 'accepted' in error && error.accepted === false)
325
354
  return Promise.reject(Object.assign(new FleetError('invalid_request', error.message), { memberNotAccepted: true }));
326
355
  if (error instanceof TaskStateError) {
@@ -329,6 +358,20 @@ export async function buildWebServer(services, boundary, options = {}) {
329
358
  throw error;
330
359
  }
331
360
  };
361
+ /** Canonical task ID for a `:id` param holding an ID or a mission key; call only after authentication. */
362
+ const taskRef = (request, options = {}) => {
363
+ const ref = String(request.params.id ?? '');
364
+ try {
365
+ return resolveTaskRef(ref, options);
366
+ }
367
+ catch (error) {
368
+ if (error instanceof MissionKeyError)
369
+ throw new FleetError('resource_not_found', error.message);
370
+ if (error instanceof TaskStateError)
371
+ throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'invalid_request', error.message);
372
+ throw error;
373
+ }
374
+ };
332
375
  const reviewApi = async (fn) => taskApi(async () => {
333
376
  try {
334
377
  return await fn();
@@ -345,7 +388,7 @@ export async function buildWebServer(services, boundary, options = {}) {
345
388
  });
346
389
  /** Browser task payloads carry Progress/To review counts; bodies have their own routes. */
347
390
  const wireTask = (task) => {
348
- const { progress: _progress, review: _review, ...rest } = task;
391
+ const { progress: _progress, review: _review, member_naming: _naming, ...rest } = task;
349
392
  return { ...rest, ...taskContributionSummary(task) };
350
393
  };
351
394
  const wireTaskResult = (result) => {
@@ -517,15 +560,29 @@ export async function buildWebServer(services, boundary, options = {}) {
517
560
  });
518
561
  app.post('/api/v1/task-lists', async (request, reply) => {
519
562
  auth.authenticate(request, true);
520
- const name = String(request.body?.name ?? '');
521
- const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name }));
563
+ const body = request.body;
564
+ const name = String(body?.name ?? '');
565
+ if (body?.prefix !== undefined && typeof body.prefix !== 'string')
566
+ throw new FleetError('invalid_request', 'prefix must be a string');
567
+ const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name,
568
+ prefix: body?.prefix }));
522
569
  reply.code(201);
523
570
  return { list };
524
571
  });
572
+ app.get('/api/v1/task-lists/prefix-suggestion', async (request) => {
573
+ auth.authenticate(request);
574
+ const name = String(request.query.name ?? '');
575
+ return { prefix: await taskApi(() => requireTaskRooms().suggestTaskListPrefix(name)) };
576
+ });
525
577
  app.patch('/api/v1/task-lists/:name', async (request) => {
526
578
  auth.authenticate(request, true);
527
579
  const name = request.params.name;
528
- const newName = String(request.body?.name ?? '');
580
+ const body = request.body;
581
+ if (body?.prefix !== undefined && typeof body.prefix !== 'string')
582
+ throw new FleetError('invalid_request', 'prefix must be a string');
583
+ if (body?.name === undefined && typeof body?.prefix === 'string')
584
+ return { list: await taskApi(() => requireTaskRooms().setTaskListPrefix({ actor: { kind: 'local_control', surface: 'web' }, name, prefix: body.prefix })) };
585
+ const newName = String(body?.name ?? '');
529
586
  return { list: await taskApi(() => requireTaskRooms().renameTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, newName })) };
530
587
  });
531
588
  app.delete('/api/v1/task-lists/:name', async (request) => {
@@ -540,11 +597,13 @@ export async function buildWebServer(services, boundary, options = {}) {
540
597
  });
541
598
  app.delete('/api/v1/tasks/:id', async (request, reply) => {
542
599
  auth.authenticate(request, true);
543
- const taskId = request.params.id;
600
+ const submitted = request.params.id;
544
601
  const confirm = request.query.confirm;
545
- // Exact-ID-twice confirmation is validated before existence or idempotency.
546
- if (confirm !== taskId)
547
- throw new FleetError('invalid_request', 'confirm must exactly repeat the task id');
602
+ // Exact raw ref-twice confirmation is validated before existence or idempotency.
603
+ if (confirm !== submitted)
604
+ throw new FleetError('invalid_request', 'confirm must exactly repeat the task id or key');
605
+ // A deleted key still maps to its canonical ID, so a repeated DELETE stays idempotent.
606
+ const taskId = taskRef(request, { allowDeleted: true });
548
607
  const accepted = await taskApi(() => requireTaskRooms().requestTaskDeletion({
549
608
  actor: { kind: 'local_control', surface: 'web' }, taskId,
550
609
  }));
@@ -559,7 +618,7 @@ export async function buildWebServer(services, boundary, options = {}) {
559
618
  return {
560
619
  task_id: taskId, accepted: true, deletion: 'pending',
561
620
  ...(outcome.error ? { error: outcome.error } : {}),
562
- recovery: `Repeat DELETE /api/v1/tasks/${taskId}?confirm=${taskId}.`,
621
+ recovery: `Repeat DELETE /api/v1/tasks/${submitted}?confirm=${submitted}.`,
563
622
  };
564
623
  });
565
624
  app.post('/api/v1/tasks', async (request, reply) => {
@@ -588,7 +647,7 @@ export async function buildWebServer(services, boundary, options = {}) {
588
647
  });
589
648
  app.patch('/api/v1/tasks/:id/list', async (request) => {
590
649
  auth.authenticate(request, true);
591
- const taskId = request.params.id;
650
+ const taskId = taskRef(request);
592
651
  const list = String(request.body?.list ?? '');
593
652
  return { task: wireTask(await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list }))) };
594
653
  });
@@ -598,17 +657,17 @@ export async function buildWebServer(services, boundary, options = {}) {
598
657
  if (typeof body?.brief !== 'string' || typeof body.expectedBrief !== 'string' || body.brief.length > 100_000)
599
658
  throw new FleetError('invalid_request', 'brief and expectedBrief must be strings; brief maximum is 100000 characters');
600
659
  return { task: wireTask(await taskApi(() => requireTaskRooms().editTaskDescription({
601
- actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
660
+ actor: { kind: 'local_control', surface: 'web' }, taskId: taskRef(request),
602
661
  brief: body.brief, expectedBrief: body.expectedBrief,
603
662
  }))) };
604
663
  });
605
664
  app.get('/api/v1/tasks/:id', async (request) => {
606
665
  auth.authenticate(request);
607
- return wireTaskResult(await taskApi(() => requireTaskRooms().getTask(request.params.id)));
666
+ return wireTaskResult(await taskApi(() => requireTaskRooms().getTask(taskRef(request))));
608
667
  });
609
668
  app.get('/api/v1/tasks/:id/progress', async (request) => {
610
669
  auth.authenticate(request);
611
- return { progress: await reviewApi(() => taskProgressSnapshot(request.params.id)) };
670
+ return { progress: await reviewApi(() => taskProgressSnapshot(taskRef(request))) };
612
671
  });
613
672
  app.post('/api/v1/tasks/:id/progress', async (request, reply) => {
614
673
  auth.authenticate(request, true);
@@ -617,7 +676,7 @@ export async function buildWebServer(services, boundary, options = {}) {
617
676
  const requestKey = typeof body?.requestKey === 'string' ? body.requestKey : typeof header === 'string' ? header : undefined;
618
677
  if (typeof body?.text !== 'string' || (body.requestKey !== undefined && typeof body.requestKey !== 'string'))
619
678
  throw new FleetError('invalid_request', 'text must be a string; requestKey, when given, a string');
620
- const result = await reviewApi(() => appendTaskProgress(request.params.id, {
679
+ const result = await reviewApi(() => appendTaskProgress(taskRef(request), {
621
680
  text: body.text, requestKey, author: { kind: 'local_control', surface: 'web' },
622
681
  }));
623
682
  if (!result.replayed)
@@ -630,7 +689,7 @@ export async function buildWebServer(services, boundary, options = {}) {
630
689
  const ids = request.body?.ids;
631
690
  if (!Array.isArray(ids) || ids.length > 200 || ids.some(id => typeof id !== 'string' || id.length > 64))
632
691
  throw new FleetError('invalid_request', 'ids must be at most 200 progress entry IDs');
633
- return { unread: await reviewApi(() => acknowledgeTaskProgress(request.params.id, ids)) };
692
+ return { unread: await reviewApi(() => acknowledgeTaskProgress(taskRef(request), ids)) };
634
693
  });
635
694
  /** Acknowledge the To review revision the browser fetched and rendered. */
636
695
  app.post('/api/v1/tasks/:id/review/read', async (request) => {
@@ -638,11 +697,11 @@ export async function buildWebServer(services, boundary, options = {}) {
638
697
  const revision = request.body?.revision;
639
698
  if (!Number.isSafeInteger(revision) || revision < 0)
640
699
  throw new FleetError('invalid_request', 'revision must be a non-negative integer');
641
- return { unread: await reviewApi(() => acknowledgeTaskReview(request.params.id, revision)) };
700
+ return { unread: await reviewApi(() => acknowledgeTaskReview(taskRef(request), revision)) };
642
701
  });
643
702
  app.get('/api/v1/tasks/:id/review', async (request) => {
644
703
  auth.authenticate(request);
645
- const review = await reviewApi(() => getTaskReview(request.params.id));
704
+ const review = await reviewApi(() => getTaskReview(taskRef(request)));
646
705
  return { review: review ? publicReview(review) : null };
647
706
  });
648
707
  app.put('/api/v1/tasks/:id/review', async (request) => {
@@ -650,7 +709,7 @@ export async function buildWebServer(services, boundary, options = {}) {
650
709
  const body = request.body;
651
710
  if (typeof body?.markdown !== 'string' || typeof body.expectedMarkdown !== 'string')
652
711
  throw new FleetError('invalid_request', 'markdown and expectedMarkdown must be strings');
653
- const review = await reviewApi(() => setTaskReviewMarkdown(request.params.id, {
712
+ const review = await reviewApi(() => setTaskReviewMarkdown(taskRef(request), {
654
713
  markdown: body.markdown, expectedMarkdown: body.expectedMarkdown,
655
714
  author: { kind: 'local_control', surface: 'web' },
656
715
  }));
@@ -658,7 +717,7 @@ export async function buildWebServer(services, boundary, options = {}) {
658
717
  });
659
718
  app.get('/api/v1/tasks/:id/review/artifacts/:artifactId', async (request, reply) => {
660
719
  auth.authenticate(request);
661
- const { artifact, bytes } = await reviewApi(() => readTaskReviewArtifact(request.params.id, request.params.artifactId));
720
+ const { artifact, bytes } = await reviewApi(() => readTaskReviewArtifact(taskRef(request), request.params.artifactId));
662
721
  reply.header('Cache-Control', 'private, no-store');
663
722
  reply.header('X-Content-Type-Options', 'nosniff');
664
723
  reply.header('Content-Security-Policy', "default-src 'none'; sandbox");
@@ -669,7 +728,12 @@ export async function buildWebServer(services, boundary, options = {}) {
669
728
  auth.authenticate(request, true);
670
729
  const body = request.body;
671
730
  try {
672
- const result = await taskApi(() => requireTaskRooms().addMember(request.params.id, body));
731
+ // A new manual harness/model/effort must be one the adapter supports; nothing is substituted.
732
+ // A retry of an accepted request returns its receipt even if the account's models changed since.
733
+ const result = await taskApi(() => requireTaskRooms().addMember(taskRef(request), body, async (member) => {
734
+ if (typeof member.brain === 'object')
735
+ await checkBrainChoice(member.brain.inline);
736
+ }));
673
737
  reply.code(result.state === 'running' ? 202 : 200);
674
738
  return result;
675
739
  }
@@ -681,7 +745,8 @@ export async function buildWebServer(services, boundary, options = {}) {
681
745
  });
682
746
  app.get('/api/v1/tasks/:id/member-additions/:requestId', async (request) => {
683
747
  auth.authenticate(request);
684
- const { id, requestId } = request.params;
748
+ const { requestId } = request.params;
749
+ const id = taskRef(request);
685
750
  return taskApi(() => requireTaskRooms().memberAddition(id, requestId));
686
751
  });
687
752
  app.patch('/api/v1/tasks/:id/layout', async (request) => {
@@ -691,18 +756,19 @@ export async function buildWebServer(services, boundary, options = {}) {
691
756
  if (!name(body?.layout) || !(body.expectedLayout === null || name(body.expectedLayout)))
692
757
  throw new FleetError('invalid_request', 'layout must be a layout name; expectedLayout a layout name or null');
693
758
  const task = await taskApi(() => requireTaskRooms().setTaskLayout({ actor: { kind: 'local_control', surface: 'web' },
694
- taskId: request.params.id, layout: body.layout, expectedLayout: body.expectedLayout }));
759
+ taskId: taskRef(request), layout: body.layout, expectedLayout: body.expectedLayout }));
695
760
  await audit.record({ requestId: request.id, browser: session.id, action: 'task.layout.set', result: 'succeeded' });
696
761
  return { task: wireTask(requireTaskRooms().withLayoutRooms(task)) };
697
762
  });
698
763
  app.get('/api/v1/tasks/:id/layout', async (request) => {
699
764
  auth.authenticate(request);
700
- return { layout: await taskApi(async () => requireTaskRooms().taskLayout(request.params.id)) };
765
+ return { layout: await taskApi(async () => requireTaskRooms().taskLayout(taskRef(request))) };
701
766
  });
702
767
  for (const operation of ['open', 'close']) {
703
768
  app.post(`/api/v1/tasks/:id/layout/rooms/:room/${operation}`, async (request, reply) => {
704
769
  const session = auth.authenticate(request, true);
705
- const { id, room } = request.params;
770
+ const { room } = request.params;
771
+ const id = taskRef(request);
706
772
  const accepted = await taskApi(() => requireTaskRooms().launchTaskLayoutOperation({
707
773
  actor: { kind: 'local_control', surface: 'web' }, taskId: id, room,
708
774
  operation: operation === 'open' ? 'open' : 'close-room',
@@ -715,7 +781,7 @@ export async function buildWebServer(services, boundary, options = {}) {
715
781
  app.post('/api/v1/tasks/:id/layout/cleanup', async (request, reply) => {
716
782
  auth.authenticate(request, true);
717
783
  const accepted = await taskApi(() => requireTaskRooms().retryTaskLayoutCleanup({
718
- actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
784
+ actor: { kind: 'local_control', surface: 'web' }, taskId: taskRef(request),
719
785
  }));
720
786
  reply.code(202);
721
787
  return { operation: accepted };
@@ -761,7 +827,7 @@ export async function buildWebServer(services, boundary, options = {}) {
761
827
  for (const action of ['start', 'block', 'unblock', 'review', 'finish', 'cancel']) {
762
828
  app.post(`/api/v1/tasks/:id/${action}`, async (request, reply) => {
763
829
  auth.authenticate(request, true);
764
- const taskId = request.params.id;
830
+ const taskId = taskRef(request);
765
831
  const body = (request.body ?? {});
766
832
  const input = { actor: { kind: 'local_control', surface: 'web' }, taskId };
767
833
  const api = requireTaskRooms();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.2.6",
3
+ "version": "2.3.1",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
package/presets/README.md CHANGED
@@ -1,26 +1,48 @@
1
- # Predefined Brains
1
+ # Models, efforts and Brains
2
2
 
3
- `brain-catalog.json` is the source for generated `fleet/brains/*.yaml` files.
4
- Run `node scripts/generate-brain-presets.mjs` after changing it, and run the same
5
- command with `--check` to verify generated files. An empty `efforts` array generates
6
- one `-default` preset with no `effort` field. Bootstrap adds missing presets and
7
- preserves existing user files. Existing model presets remain available.
3
+ `brain-catalog.json` is the model catalog maintained with the Claude Code and
4
+ Codex harness adapters: each exact model ID, its label, and the efforts it
5
+ supports (an empty `efforts` list means the model has no effort setting). The
6
+ adapters expose it through `modelCapabilities()`, and Fleet serves it, narrowed
7
+ to the models the active Codex account offers, at `GET /api/v1/harness-capabilities`.
8
+ Onboarding, Settings and agent launch flows use that one source. Hermes models
9
+ come from the provider configured in the role home, so Hermes lists none and
10
+ sets no effort.
11
+
12
+ Setup no longer installs a Brain per model and effort. It creates three
13
+ editable purpose Brains from the choices made during setup:
14
+
15
+ | Brain | Used by |
16
+ | --- | --- |
17
+ | `development` | Developer, Engineer |
18
+ | `critic` | Critic |
19
+ | `coordination` | LocalCoordinator, FleetCoordinator |
20
+
21
+ Users change these or add more in Settings > Brains. An agent can also be
22
+ launched with a one-off harness, model and effort; that choice is kept with the
23
+ agent and saves no Brain.
24
+
25
+ Existing installations keep every Brain file and reference. The values of the
26
+ Brain presets earlier releases generated per model and effort are frozen in
27
+ `legacy/generated-brains-rev4.json`; configuration provenance reports an
28
+ untouched one as `legacy` and an edited one as `changed`. Nothing deletes them.
29
+ `fleet/brains/claude-default.yaml` (the Claude Code default model) still ships
30
+ for packaged templates and existing references; setup does not install it.
8
31
 
9
32
  ## GPT-6.1 Sol (added 2026-09-29)
10
33
 
11
- The Codex presets `codex-gpt-6-1-sol-{low,medium,high,xhigh,max,ultra}`
12
- use the exact model ID `gpt-6.1-sol` with ACP sessions. The init wizard offers
34
+ The Codex catalog entry uses the exact model ID `gpt-6.1-sol` with ACP sessions. The init wizard offers
13
35
  GPT-6.1 Sol for development, review, and coordination.
14
36
 
15
37
  [Official OpenAI documentation](https://developers.openai.com/api/docs/models/gpt-6.1-sol)
16
38
  supports low, medium, high, xhigh, and max API reasoning efforts. The local
17
39
  Codex `models_cache.json` inspected on 2026-09-29 additionally advertises
18
- `ultra`, a Codex harness mode. These presets reflect that six-effort Codex
40
+ `ultra`, a Codex harness mode. The catalog reflects that six-effort Codex
19
41
  catalog; no inference or ACP launch was performed for this addition.
20
42
 
21
43
  ## Current model additions (verified 2026-09-24)
22
44
 
23
- | Harness | Exact model ID | Preset efforts |
45
+ | Harness | Exact model ID | Efforts |
24
46
  | --- | --- | --- |
25
47
  | Codex | `gpt-6-sol` | low, medium, high, xhigh, max, ultra |
26
48
  | Codex | `gpt-6-luna` | low, medium, high, xhigh, max |
@@ -29,23 +51,21 @@ catalog; no inference or ACP launch was performed for this addition.
29
51
  | Claude Code (retained) | `claude-sonnet-5` | low, medium, high, xhigh, max |
30
52
  | Claude Code | `claude-haiku-4-5-20251001` | default, with no effort setting |
31
53
 
32
- For example, select `codex-gpt-6-sol-ultra`, `claude-opus-5-5-max`, or
33
- `claude-haiku-4-5-20251001-default` as a Brain reference. The init wizard offers
34
- models supporting its Quick/Balanced/Thorough (low/medium/high) choices; use the
35
- named Brain directly for Haiku, which does not support effort.
54
+ Setup asks for an effort only among the efforts a model supports; Haiku gets
55
+ none.
36
56
 
37
57
  ### Runtime requirements and evidence
38
58
 
39
59
  Codex CLI **0.155.0** freshly queried through `app-server` `initialize` then
40
60
  `model/list` advertised the GPT rows above. A previously populated
41
61
  `~/.codex/models_cache.json` did not contain these models; refresh runtime discovery
42
- before concluding a model is unavailable. `ours-fleet doctor` compares presets to
62
+ before concluding a model is unavailable. `ours-fleet doctor` compares configured Codex Brains to
43
63
  the local cache. The catalog is not an account entitlement guarantee.
44
64
 
45
65
  The [Sol](https://developers.openai.com/api/docs/models/gpt-6-sol) and
46
66
  [Luna](https://developers.openai.com/api/docs/models/gpt-6-luna) API model pages
47
67
  also support `none`, but the verified Codex runtime does not advertise it.
48
- Consequently there are no `none` presets. Sol's `ultra` is an advertised Codex
68
+ Consequently the catalog lists no `none` effort. Sol's `ultra` is an advertised Codex
49
69
  harness mode, not an additional API reasoning level; Luna does not advertise it.
50
70
 
51
71
  Claude Code **2.1.281** was verified using the Agent SDK `supportedModels()`
@@ -56,13 +76,13 @@ not asserted minimum versions. Provider references:
56
76
  [model IDs](https://platform.claude.com/docs/en/models/overview) and
57
77
  [Claude Code effort support](https://code.claude.com/docs/en/model-config#adjust-effort-level).
58
78
 
59
- **Check the executable used by ACP before selecting the new Claude presets.**
79
+ **Check the executable used by ACP before selecting the new Claude models.**
60
80
  The inspected `claude-agent-acp` **0.63.0** installation bundles Agent SDK
61
81
  **0.3.220**, whose default executable reports older models. To use the verified
62
82
  installed Claude CLI, set `CLAUDE_CODE_EXECUTABLE` to its absolute path in the
63
83
  launch environment (for example `/usr/bin/claude` on the verification host).
64
84
  Otherwise use a bundled SDK whose runtime advertises these models and efforts.
65
- The presets do not change your executable or restart agents. An older bundled
85
+ The catalog does not change your executable or restart agents. An older bundled
66
86
  runtime alone is not evidence of support for the new models.
67
87
 
68
88
  No-prompt ACP startup and `session/set_config_option` probes also verified all