@ours.network/fleet 2.3.0 → 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 (108) 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 +1 -1
  11. package/dist/application/task-room-service.js +2 -2
  12. package/dist/build-info.json +4 -4
  13. package/dist/harness/claude-code.js +7 -0
  14. package/dist/harness/codex.js +7 -0
  15. package/dist/harness/hermes.js +2 -0
  16. package/dist/harness/types.d.ts +20 -0
  17. package/dist/init-wizard.d.ts +19 -12
  18. package/dist/init-wizard.js +122 -104
  19. package/dist/rooms-tasks/add-member.d.ts +15 -1
  20. package/dist/rooms-tasks/add-member.js +27 -4
  21. package/dist/web/server.js +55 -24
  22. package/package.json +1 -1
  23. package/presets/README.md +38 -18
  24. package/presets/brain-catalog.json +19 -19
  25. package/presets/legacy/generated-brains-rev4.json +497 -0
  26. package/presets/manifest.json +1 -1
  27. package/presets/fleet/brains/claude-fable-5-1-high.yaml +0 -5
  28. package/presets/fleet/brains/claude-fable-5-1-low.yaml +0 -5
  29. package/presets/fleet/brains/claude-fable-5-1-max.yaml +0 -5
  30. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +0 -5
  31. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +0 -5
  32. package/presets/fleet/brains/claude-fable-5-high.yaml +0 -5
  33. package/presets/fleet/brains/claude-fable-5-low.yaml +0 -5
  34. package/presets/fleet/brains/claude-fable-5-max.yaml +0 -5
  35. package/presets/fleet/brains/claude-fable-5-medium.yaml +0 -5
  36. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +0 -5
  37. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +0 -4
  38. package/presets/fleet/brains/claude-opus-5-5-high.yaml +0 -5
  39. package/presets/fleet/brains/claude-opus-5-5-low.yaml +0 -5
  40. package/presets/fleet/brains/claude-opus-5-5-max.yaml +0 -5
  41. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +0 -5
  42. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +0 -5
  43. package/presets/fleet/brains/claude-opus-5-high.yaml +0 -5
  44. package/presets/fleet/brains/claude-opus-5-low.yaml +0 -5
  45. package/presets/fleet/brains/claude-opus-5-max.yaml +0 -5
  46. package/presets/fleet/brains/claude-opus-5-medium.yaml +0 -5
  47. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +0 -5
  48. package/presets/fleet/brains/claude-sonnet-5-high.yaml +0 -5
  49. package/presets/fleet/brains/claude-sonnet-5-low.yaml +0 -5
  50. package/presets/fleet/brains/claude-sonnet-5-max.yaml +0 -5
  51. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +0 -5
  52. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +0 -5
  53. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +0 -5
  54. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +0 -5
  55. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +0 -5
  56. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +0 -5
  57. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +0 -5
  58. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +0 -5
  59. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +0 -5
  60. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +0 -5
  61. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +0 -5
  62. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +0 -5
  63. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +0 -5
  64. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +0 -5
  65. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +0 -5
  66. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +0 -5
  67. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +0 -5
  68. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +0 -5
  69. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +0 -5
  70. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +0 -5
  71. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +0 -5
  72. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +0 -5
  73. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +0 -5
  74. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +0 -5
  75. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +0 -5
  76. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +0 -5
  77. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +0 -5
  78. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +0 -5
  79. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +0 -5
  80. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +0 -5
  81. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +0 -5
  82. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +0 -5
  83. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +0 -5
  84. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +0 -5
  85. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +0 -5
  86. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +0 -5
  87. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +0 -5
  88. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +0 -5
  89. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +0 -5
  90. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +0 -5
  91. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +0 -5
  92. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +0 -5
  93. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +0 -5
  94. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +0 -5
  95. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +0 -5
  96. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +0 -5
  97. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +0 -5
  98. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +0 -5
  99. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +0 -5
  100. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +0 -5
  101. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +0 -5
  102. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +0 -5
  103. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +0 -5
  104. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +0 -5
  105. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +0 -5
  106. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +0 -5
  107. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +0 -5
  108. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +0 -5
package/README.md CHANGED
@@ -1853,7 +1853,7 @@ and member status and choose supported recovery. These checks do not restart age
1853
1853
  recreate identities, redeem invites, or rewrite lifecycle state. Temporary room agents
1854
1854
  remain transient across host reboot; retained launch records do not restore them.
1855
1855
 
1856
- See [predefined Brain models and runtime requirements](presets/README.md) for the verified model/effort matrix and regeneration instructions.
1856
+ See [models, efforts and Brains](presets/README.md) for the adapter model catalog, the three purpose Brains setup creates, and runtime requirements.
1857
1857
 
1858
1858
  ### YAML room layouts
1859
1859
 
@@ -0,0 +1,60 @@
1
+ import type { PrereqCheck } from '../harness/types.js';
2
+ import type { OfferedModels } from '../subscriptions/cli.js';
3
+ /**
4
+ * A selectable model. `efforts: null` means the efforts are unknown (a model the
5
+ * adapter does not maintain); `[]` means the model is known to have no effort setting.
6
+ */
7
+ export interface CapabilityModel {
8
+ id: string;
9
+ label: string;
10
+ efforts: string[] | null;
11
+ available: boolean;
12
+ reason?: string;
13
+ source: 'adapter' | 'runtime-discovery';
14
+ }
15
+ export interface HarnessCapability {
16
+ harness: string;
17
+ ready: boolean;
18
+ checks: PrereqCheck[];
19
+ sessions: string[];
20
+ modelRequired: boolean;
21
+ customModels: boolean;
22
+ models: CapabilityModel[];
23
+ discovery?: {
24
+ state: 'known';
25
+ } | {
26
+ state: 'unknown';
27
+ reason: string;
28
+ };
29
+ }
30
+ export interface HarnessCapabilitiesReport {
31
+ harnesses: HarnessCapability[];
32
+ }
33
+ export interface HarnessCapabilityDeps {
34
+ harnesses?: string[];
35
+ codexModels?: () => Promise<OfferedModels>;
36
+ /** Skip executable checks when only model facts are needed. */
37
+ checkReadiness?: boolean;
38
+ }
39
+ /** Each production adapter's maintained models, narrowed by what the active Codex account offers. */
40
+ export declare function harnessCapabilities(deps?: HarnessCapabilityDeps): Promise<HarnessCapabilitiesReport>;
41
+ export interface BrainChoice {
42
+ harness: string;
43
+ session?: string;
44
+ model?: string | null;
45
+ effort?: string | null;
46
+ }
47
+ export type BrainChoiceCheck = {
48
+ ok: true;
49
+ model?: CapabilityModel;
50
+ } | {
51
+ ok: false;
52
+ code: string;
53
+ message: string;
54
+ };
55
+ /**
56
+ * Strict check of a new harness/model/effort choice. Nothing is substituted: an
57
+ * effort is kept only when the chosen model supports it; an omitted effort keeps
58
+ * the harness default. Existing Brains are resolved by their adapter as before.
59
+ */
60
+ export declare function validateBrainChoice(report: HarnessCapabilitiesReport, choice: BrainChoice): BrainChoiceCheck;
@@ -0,0 +1,96 @@
1
+ import { getAdapter, knownAdapters, productionAdapters } from '../harness/registry.js';
2
+ /** Each production adapter's maintained models, narrowed by what the active Codex account offers. */
3
+ export async function harnessCapabilities(deps = {}) {
4
+ // Only harnesses whose adapter is registered in this process can report facts.
5
+ const ids = (deps.harnesses ?? productionAdapters()).filter(id => knownAdapters().includes(id));
6
+ const harnesses = await Promise.all(ids.map(async (id) => {
7
+ const adapter = getAdapter(id);
8
+ const capabilities = adapter.modelCapabilities?.();
9
+ if (!capabilities)
10
+ return undefined;
11
+ let ready = true, checks = [];
12
+ if (deps.checkReadiness !== false) {
13
+ try {
14
+ ({ ok: ready, checks } = await adapter.checkPrereqs());
15
+ }
16
+ catch {
17
+ ready = false;
18
+ checks = [{ name: 'availability', ok: false, detail: 'Harness checks unavailable' }];
19
+ }
20
+ }
21
+ let models = capabilities.models.map(model => ({
22
+ id: model.id, label: model.label, efforts: [...model.efforts], available: true, source: 'adapter',
23
+ }));
24
+ let discovery;
25
+ if (id === 'codex' && deps.codexModels) {
26
+ const offered = await deps.codexModels();
27
+ discovery = offered.state === 'known' ? { state: 'known' } : { state: 'unknown', reason: offered.reason };
28
+ if (offered.state === 'known') {
29
+ models = models.map(model => offered.models.includes(model.id) ? model
30
+ : { ...model, available: false, reason: 'The active Codex account does not offer this model.' });
31
+ // Offered models the adapter does not maintain: selectable, but their efforts are not known.
32
+ for (const extra of offered.models.filter(model => !models.some(known => known.id === model)))
33
+ models.push({ id: extra, label: extra, efforts: null, available: true, source: 'runtime-discovery' });
34
+ }
35
+ else {
36
+ models = models.map(model => ({ ...model, available: false,
37
+ reason: `Codex could not report the models offered to the active account: ${offered.reason}` }));
38
+ }
39
+ }
40
+ return { harness: id, ready, checks, sessions: [...capabilities.sessions], modelRequired: capabilities.modelRequired,
41
+ customModels: capabilities.customModels, models, ...(discovery ? { discovery } : {}) };
42
+ }));
43
+ return { harnesses: harnesses.filter((item) => item !== undefined) };
44
+ }
45
+ /**
46
+ * Strict check of a new harness/model/effort choice. Nothing is substituted: an
47
+ * effort is kept only when the chosen model supports it; an omitted effort keeps
48
+ * the harness default. Existing Brains are resolved by their adapter as before.
49
+ */
50
+ export function validateBrainChoice(report, choice) {
51
+ const harness = report.harnesses.find(item => item.harness === choice.harness);
52
+ if (!harness)
53
+ return { ok: false, code: 'harness_unsupported',
54
+ message: `Harness '${String(choice.harness)}' is not available; choose one of: ${report.harnesses.map(item => item.harness).join(', ')}` };
55
+ if (choice.session !== undefined && !harness.sessions.includes(choice.session))
56
+ return { ok: false, code: 'session_unsupported',
57
+ message: `${harness.harness} does not support session '${choice.session}'; choose one of: ${harness.sessions.join(', ')}` };
58
+ const effort = choice.effort ?? undefined;
59
+ if (effort !== undefined && (typeof effort !== 'string' || !effort))
60
+ return { ok: false, code: 'effort_invalid', message: 'Effort must be a non-empty string or omitted' };
61
+ const modelId = choice.model ?? undefined;
62
+ if (modelId === undefined) {
63
+ if (harness.modelRequired)
64
+ return { ok: false, code: 'model_required', message: `${harness.harness} requires an explicit model` };
65
+ if (effort !== undefined)
66
+ return { ok: false, code: 'effort_unknown',
67
+ message: `The ${harness.harness} default model has no known effort list; choose a model to set effort, or leave effort at the harness default` };
68
+ return { ok: true };
69
+ }
70
+ if (typeof modelId !== 'string' || !modelId.trim())
71
+ return { ok: false, code: 'model_invalid', message: 'Model must be a non-empty string' };
72
+ const model = harness.models.find(item => item.id === modelId);
73
+ if (!model) {
74
+ if (!harness.customModels)
75
+ return { ok: false, code: 'model_unsupported',
76
+ message: `${harness.harness} does not support model '${modelId}'` };
77
+ if (effort !== undefined)
78
+ return { ok: false, code: 'effort_unknown',
79
+ message: `Efforts for '${modelId}' are unknown to ${harness.harness}; leave effort at the harness default or choose a listed model` };
80
+ return { ok: true };
81
+ }
82
+ if (!model.available)
83
+ return { ok: false, code: 'model_unavailable', message: `${model.label}: ${model.reason ?? 'unavailable'}` };
84
+ if (effort === undefined)
85
+ return { ok: true, model };
86
+ if (model.efforts === null)
87
+ return { ok: false, code: 'effort_unknown',
88
+ message: `Efforts for ${model.label} are unknown; leave effort at the harness default` };
89
+ if (!model.efforts.length)
90
+ return { ok: false, code: 'effort_unsupported',
91
+ message: `${model.label} has no effort setting; remove the effort` };
92
+ if (!model.efforts.includes(effort))
93
+ return { ok: false, code: 'effort_unsupported',
94
+ message: `${model.label} does not support effort '${effort}'; choose one of: ${model.efforts.join(', ')}` };
95
+ return { ok: true, model };
96
+ }
@@ -10,6 +10,23 @@ export interface HarnessModelCatalog {
10
10
  models: HarnessModelOption[];
11
11
  warnings: string[];
12
12
  }
13
+ /** One model a harness adapter knows, with the exact efforts it supports ([] = no effort setting). */
14
+ export interface PackagedModel {
15
+ harness: 'codex' | 'claude-code';
16
+ session: 'acp';
17
+ model: string;
18
+ label: string;
19
+ efforts: string[];
20
+ }
21
+ export interface PackagedModelCatalog {
22
+ schema_version: 2;
23
+ catalog_revision: number;
24
+ models: PackagedModel[];
25
+ }
26
+ /** Validate the packaged model catalog maintained with the Claude Code and Codex adapters. */
27
+ export declare function validatePackagedModelCatalog(value: unknown, path?: string): PackagedModelCatalog;
28
+ /** The packaged catalog, optionally limited to one harness. */
29
+ export declare function packagedModelCatalog(harness?: PackagedModel['harness']): PackagedModel[];
13
30
  /** Read Codex's locally materialized `model/list` result; never invent fallback aliases. */
14
31
  export declare function codexModelCatalog(path?: string): HarnessModelCatalog;
15
32
  /**
@@ -1,6 +1,35 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import { packagedPresetRoot } from '../preset-bootstrap.js';
5
+ const KNOWN_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
6
+ /** Validate the packaged model catalog maintained with the Claude Code and Codex adapters. */
7
+ export function validatePackagedModelCatalog(value, path = 'packaged model catalog') {
8
+ const catalog = value;
9
+ if (!catalog || catalog.schema_version !== 2 || !Number.isInteger(catalog.catalog_revision)
10
+ || !Array.isArray(catalog.models) || catalog.models.length === 0)
11
+ throw new Error(`invalid ${path}`);
12
+ const tuples = new Set();
13
+ for (const model of catalog.models) {
14
+ if (!model || !['codex', 'claude-code'].includes(model.harness) || model.session !== 'acp'
15
+ || typeof model.model !== 'string' || !model.model
16
+ || typeof model.label !== 'string' || !model.label.trim()
17
+ || !Array.isArray(model.efforts) || new Set(model.efforts).size !== model.efforts.length
18
+ || model.efforts.some(effort => !KNOWN_EFFORTS.includes(effort)))
19
+ throw new Error(`unsupported model state in ${path}: ${String(model?.model)}`);
20
+ const tuple = `${model.harness}\0${model.session}\0${model.model}`;
21
+ if (tuples.has(tuple))
22
+ throw new Error(`duplicate supported model in ${path}: ${model.model}`);
23
+ tuples.add(tuple);
24
+ }
25
+ return catalog;
26
+ }
27
+ /** The packaged catalog, optionally limited to one harness. */
28
+ export function packagedModelCatalog(harness) {
29
+ const path = join(packagedPresetRoot(), 'brain-catalog.json');
30
+ const models = validatePackagedModelCatalog(JSON.parse(readFileSync(path, 'utf8')), path).models;
31
+ return harness ? models.filter(model => model.harness === harness) : models;
32
+ }
4
33
  const validId = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._[\]-]{0,127}$/.test(value);
5
34
  /** Read Codex's locally materialized `model/list` result; never invent fallback aliases. */
6
35
  export function codexModelCatalog(path = join(homedir(), '.codex', 'models_cache.json')) {
@@ -45,16 +74,10 @@ export function codexModelCatalog(path = join(homedir(), '.codex', 'models_cache
45
74
  * not a claim about account entitlement (which Claude validates at launch).
46
75
  */
47
76
  export function claudeModelCatalog() {
48
- const reasoningEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
49
77
  return {
50
- models: [
51
- { id: 'claude-fable-5-1', label: 'Claude Fable 5.1', reasoningEfforts, source: 'claude-adapter-2.1' },
52
- { id: 'claude-opus-5-5', label: 'Claude Opus 5.5', reasoningEfforts, source: 'claude-adapter-2.1' },
53
- { id: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', reasoningEfforts: [], source: 'claude-adapter-2.1' },
54
- { id: 'claude-fable-5', label: 'Claude Fable 5', reasoningEfforts, source: 'claude-adapter-2.1' },
55
- { id: 'claude-opus-5', label: 'Claude Opus 5', reasoningEfforts, source: 'claude-adapter-2.1' },
56
- { id: 'claude-sonnet-5', label: 'Claude Sonnet 5', reasoningEfforts, source: 'claude-adapter-2.1' },
57
- ],
78
+ models: packagedModelCatalog('claude-code').map(model => ({
79
+ id: model.model, label: model.label, reasoningEfforts: model.efforts, source: 'claude-adapter-2.1',
80
+ })),
58
81
  warnings: ['Claude Code exposes no typed model-list endpoint; choices require a current Claude runtime (verified with 2.1.281); older bundled SDKs need CLAUDE_CODE_EXECUTABLE. Entitlement is validated at launch.'],
59
82
  };
60
83
  }
@@ -5,7 +5,7 @@ export declare const PROVENANCE_KINDS: {
5
5
  readonly layout: "room_layouts";
6
6
  };
7
7
  export type ProvenanceKind = keyof typeof PROVENANCE_KINDS;
8
- export type ProvenanceStatus = 'predefined' | 'changed' | 'custom';
8
+ export type ProvenanceStatus = 'predefined' | 'changed' | 'custom' | 'legacy';
9
9
  export interface ProvenanceEntry {
10
10
  status: ProvenanceStatus;
11
11
  preset?: string;
@@ -1,7 +1,9 @@
1
1
  /** Where each configured definition came from, compared with the packaged presets.
2
2
  * A definition is `predefined` when it has the same value as the shipped preset of
3
3
  * the same name, `changed` when a preset of that name exists but the value differs,
4
- * and `custom` otherwise. Values are compared after parsing, so the formatting the
4
+ * and `custom` otherwise. A Brain whose name and value match a preset that earlier
5
+ * releases generated for every model and effort is `legacy`; one of such a name
6
+ * with another value is `changed`. Values are compared after parsing, so the formatting the
5
7
  * configuration editor writes does not count as a change. Preset text is returned
6
8
  * so users can see exactly what ships; it contains no host data.
7
9
  */
@@ -29,6 +31,14 @@ function yamlFiles(dir) {
29
31
  }
30
32
  return files;
31
33
  }
34
+ /** Brain presets earlier releases generated per model and effort; no longer shipped or installed. */
35
+ function legacyBrains(presetRoot) {
36
+ const path = join(presetRoot, '..', 'legacy', 'generated-brains-rev4.json');
37
+ if (!existsSync(path))
38
+ return new Map();
39
+ const { brains } = JSON.parse(readFileSync(path, 'utf8'));
40
+ return new Map(Object.entries(brains).map(([name, value]) => [name, Object.entries(value).map(([key, item]) => `${key}: ${item}`).join('\n') + '\n']));
41
+ }
32
42
  const parsed = (text) => { try {
33
43
  return parse(text);
34
44
  }
@@ -47,10 +57,15 @@ export class PresetProvenance {
47
57
  const result = {};
48
58
  for (const [kind, dir] of Object.entries(PROVENANCE_KINDS)) {
49
59
  const presets = yamlFiles(join(this.presetRoot, dir)), installed = yamlFiles(join(split, dir));
60
+ const legacy = kind === 'brain' ? legacyBrains(this.presetRoot) : new Map();
50
61
  result[kind] = Object.fromEntries([...installed].map(([name, text]) => {
51
62
  const preset = presets.get(name);
52
- if (preset === undefined)
53
- return [name, { status: 'custom' }];
63
+ if (preset === undefined) {
64
+ const old = legacy.get(name);
65
+ if (old === undefined)
66
+ return [name, { status: 'custom' }];
67
+ return [name, { status: isDeepStrictEqual(parsed(text), parsed(old)) ? 'legacy' : 'changed', preset: old }];
68
+ }
54
69
  return [name, { status: isDeepStrictEqual(parsed(text), parsed(preset)) ? 'predefined' : 'changed', preset }];
55
70
  }));
56
71
  }
@@ -145,7 +145,9 @@ export declare class RoleCreationService {
145
145
  private launchSync;
146
146
  capabilities(): Promise<CreationCapabilities>;
147
147
  preview(input: CreateRoleSessionRequest): Promise<CreationPreview>;
148
- create(input: CreateRoleSessionRequest, previewHash: string, idempotencyKey: string, browserSession: string): Promise<CreationAction>;
148
+ create(input: CreateRoleSessionRequest, previewHash: string, idempotencyKey: string, browserSession: string,
149
+ /** Checks a new request only; a retry of an accepted request returns its action. */
150
+ checkNewRequest?: (input: CreateRoleSessionRequest) => Promise<void>): Promise<CreationAction>;
149
151
  get(actionId: string): CreationAction | undefined;
150
152
  private run;
151
153
  private waitForReady;
@@ -184,7 +184,9 @@ export class RoleCreationService {
184
184
  identityBootstrap, previewHash,
185
185
  };
186
186
  }
187
- async create(input, previewHash, idempotencyKey, browserSession) {
187
+ async create(input, previewHash, idempotencyKey, browserSession,
188
+ /** Checks a new request only; a retry of an accepted request returns its action. */
189
+ checkNewRequest) {
188
190
  if (!/^[A-Za-z0-9_-]{22,256}$/.test(idempotencyKey))
189
191
  throw new FleetError('invalid_request', 'Idempotency-Key must contain at least 128 bits');
190
192
  const requestHash = hash(this.validate(input));
@@ -195,6 +197,7 @@ export class RoleCreationService {
195
197
  throw new FleetError('idempotency_conflict', 'Idempotency-Key was already used for another request');
196
198
  return this.actions.get(existing.actionId);
197
199
  }
200
+ await checkNewRequest?.(input);
198
201
  const preview = await this.preview(input);
199
202
  if (preview.previewHash !== previewHash)
200
203
  throw new FleetError('stale_state', 'preview is stale; review the effective plan again');
@@ -114,7 +114,7 @@ export declare class TaskRoomApplicationService {
114
114
  private readonly configurationPath?;
115
115
  private readonly deps;
116
116
  constructor(configurationPath?: string | undefined, deps?: TaskRoomServiceDeps);
117
- addMember(taskId: string, request: AddMemberRequest): Promise<{
117
+ addMember(taskId: string, request: AddMemberRequest, checkNewRequest?: (request: AddMemberRequest) => Promise<void>): Promise<{
118
118
  requestId: string;
119
119
  taskId: string;
120
120
  roleId: string;
@@ -76,11 +76,11 @@ export class TaskRoomApplicationService {
76
76
  this.configurationPath = configurationPath;
77
77
  this.deps = deps;
78
78
  }
79
- async addMember(taskId, request) {
79
+ async addMember(taskId, request, checkNewRequest) {
80
80
  const { requestMemberAddition } = await import('../rooms-tasks/add-member.js');
81
81
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
82
82
  return requestMemberAddition({ taskId, request, cfg, configPath: this.configurationPath,
83
- cowork: readTask(taskId).layout ? undefined : (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers, layouts: this.layouts() });
83
+ cowork: readTask(taskId).layout ? undefined : (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers, layouts: this.layouts(), checkNewRequest });
84
84
  }
85
85
  async memberAddition(taskId, requestId) { const { memberAddition } = await import('../rooms-tasks/add-member.js'); return memberAddition(taskId, requestId); }
86
86
  layouts() {
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "2.3.0",
3
- "buildId": "246fc0d15b6e",
4
- "commit": "ce34aa31bf25ec6d0af430270b65f013e3681900",
2
+ "version": "2.3.1",
3
+ "buildId": "2fbb7f11f70a",
4
+ "commit": "f7b1dda8a2cb911367ce11617ef38759ba78c5e6",
5
5
  "dirty": false,
6
- "builtAt": "2026-10-30T17:28:38.538Z",
6
+ "builtAt": "2026-10-30T19:03:10.573Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -9,6 +9,7 @@ import { harnessRuntimeDir } from '../isolation/policy.js';
9
9
  import { bundledAcpAgent, resolveBundledAcpAgent } from './acp-agent.js';
10
10
  import { hostClaude } from './claude-runtime.js';
11
11
  import { ClaudeCodeAgentSessionAdapter } from './claude-code-session.js';
12
+ import { packagedModelCatalog } from '../application/model-catalog.js';
12
13
  const OPTION_KEYS = [
13
14
  'plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort',
14
15
  'mcp_servers', 'mcp_servers_only',
@@ -196,6 +197,12 @@ export function makeClaudeCodeAdapter(exec = realExec, transport) {
196
197
  },
197
198
  }, transport),
198
199
  supportsResume: true,
200
+ modelCapabilities: () => ({
201
+ sessions: ['acp'],
202
+ models: packagedModelCatalog('claude-code').map(model => ({ id: model.model, label: model.label, efforts: model.efforts })),
203
+ modelRequired: false,
204
+ customModels: true,
205
+ }),
199
206
  async checkPrereqs() {
200
207
  const r = await exec('claude', ['--version']);
201
208
  const ok = r.code === 0;
@@ -9,6 +9,7 @@ import { resolveBundledAcpAgent, } from './acp-agent.js';
9
9
  import { VERIFIED_CODEX_ACP_VERSIONS, probeCodexRuntime, codexVersionAtLeast, hostCodex } from './codex-runtime.js';
10
10
  import { CodexAgentSessionAdapter } from './codex-session.js';
11
11
  import { CODEX_REASONING_EFFORTS, isCodexReasoningEffort } from './codex-reasoning.js';
12
+ import { packagedModelCatalog } from '../application/model-catalog.js';
12
13
  const OPTION_KEYS = [
13
14
  'launcher', 'sandbox', 'approval', 'permission_mode', 'search', 'profile', 'config', 'add_dirs',
14
15
  'monitor',
@@ -361,6 +362,12 @@ export function makeCodexAdapter(exec = realExec, transport, nativeTransport) {
361
362
  addDirs: role => role.harness_options?.add_dirs,
362
363
  }, transport, nativeTransport),
363
364
  supportsResume: true,
365
+ modelCapabilities: () => ({
366
+ sessions: ['acp', 'codex-app-server'],
367
+ models: packagedModelCatalog('codex').map(model => ({ id: model.model, label: model.label, efforts: model.efforts })),
368
+ modelRequired: false,
369
+ customModels: true,
370
+ }),
364
371
  async checkPrereqs() {
365
372
  const [r, hasOursCodex, plugins] = await Promise.all([
366
373
  exec('codex', ['--version']),
@@ -21,6 +21,8 @@ export function makeHermesAdapter(exec = realExec, transport) {
21
21
  },
22
22
  }),
23
23
  supportsResume: false,
24
+ // Hermes models come from the provider configured in the role home; Fleet knows none of them and sets no effort.
25
+ modelCapabilities: () => ({ sessions: ['acp'], models: [], modelRequired: true, customModels: true }),
24
26
  async checkPrereqs() {
25
27
  const result = await exec('hermes-acp', ['--help'], { timeout: 10_000 });
26
28
  const ok = result.code === 0;
@@ -112,12 +112,32 @@ export interface ValidationError {
112
112
  path: string;
113
113
  message: string;
114
114
  }
115
+ /** A model the adapter knows, with the exact efforts it supports; [] means the model has no effort setting. */
116
+ export interface HarnessKnownModel {
117
+ id: string;
118
+ label: string;
119
+ efforts: string[];
120
+ }
121
+ /**
122
+ * Models and efforts a harness adapter maintains. Brain choices made in onboarding,
123
+ * Settings and launch flows are validated against this; existing Brains are not.
124
+ */
125
+ export interface HarnessModelCapabilities {
126
+ sessions: string[];
127
+ models: HarnessKnownModel[];
128
+ /** A Brain must name a model; otherwise `model: null` selects the harness default. */
129
+ modelRequired: boolean;
130
+ /** Models outside `models` may be named; their efforts are unknown. */
131
+ customModels: boolean;
132
+ }
115
133
  export interface HarnessAdapter {
116
134
  id: string;
117
135
  /** Harness-specific construction behind Fleet's shared live-session contract. */
118
136
  agentSession: AgentSessionAdapter;
119
137
  supportsResume: boolean;
120
138
  checkPrereqs(): Promise<PrereqReport>;
139
+ /** Models and efforts this adapter supports. Optional for test adapters. */
140
+ modelCapabilities?(): HarnessModelCapabilities;
121
141
  /**
122
142
  * `role` is the SESSION-AWARE half: some harness options can only be honoured
123
143
  * on some session types, and an option that is silently dropped is worse than
@@ -2,19 +2,18 @@ import type { Stats } from 'node:fs';
2
2
  import type { ReadStream, WriteStream } from 'node:tty';
3
3
  export type Subscription = 'codex' | 'claude';
4
4
  export type WorkKind = 'development' | 'review' | 'coordination';
5
- export type ReasoningPreference = 'quick' | 'balanced' | 'thorough';
6
5
  export type AssignmentStrategy = 'one-model' | 'per-job';
7
- export interface CatalogModel {
6
+ /** The exact harness, model and effort chosen for one purpose; `effort: null` only for a model without effort. */
7
+ export interface PurposeChoice {
8
8
  harness: 'codex' | 'claude-code';
9
9
  session: 'acp';
10
10
  model: string;
11
- efforts: string[];
11
+ effort: string | null;
12
12
  }
13
13
  export interface InitAnswers {
14
14
  subscriptions: Subscription[];
15
15
  assignmentStrategy: AssignmentStrategy;
16
- models: Record<WorkKind, CatalogModel>;
17
- reasoning: ReasoningPreference;
16
+ models: Record<WorkKind, PurposeChoice>;
18
17
  }
19
18
  export interface Choice<T> {
20
19
  label: string;
@@ -26,10 +25,6 @@ export interface InitPrompter {
26
25
  select<T>(message: string, choices: Choice<T>[], initial?: number): Promise<T | undefined>;
27
26
  note(message: string): void;
28
27
  }
29
- export interface BrainCatalog {
30
- schema_version: number;
31
- models: CatalogModel[];
32
- }
33
28
  export interface GeneratedSetup {
34
29
  files: Map<string, string>;
35
30
  answers: InitAnswers;
@@ -48,15 +43,27 @@ export interface InitExecutionDeps {
48
43
  generate?(answers: InitAnswers): GeneratedSetup;
49
44
  preflight?(configuration: string): InitPathState;
50
45
  }
51
- export declare function validateCatalog(value: BrainCatalog, path?: string): BrainCatalog;
46
+ /** The three editable purpose Brains setup creates, by kind of work. */
47
+ export declare const PURPOSE_BRAINS: Record<WorkKind, {
48
+ id: string;
49
+ label: string;
50
+ }>;
52
51
  /** Read and validate the complete public InitAnswers JSON shape without side effects. */
53
52
  export declare function readInitSettings(path: string): InitAnswers;
54
53
  export declare function askInitQuestions(prompter: InitPrompter, configuration: string): Promise<InitAnswers | undefined>;
55
54
  export declare function formatSetupSummary(answers: InitAnswers, configuration: string): string;
56
- /** The packaged Brain preset for a catalog model at one effort; the same naming the presets are generated with. */
57
- export declare function brainPresetId(model: Pick<CatalogModel, 'harness' | 'model'>, effort: string): string;
55
+ /** The YAML of one purpose Brain holding exactly the chosen triple. */
56
+ export declare function purposeBrainYaml(work: WorkKind, choice: PurposeChoice): string;
58
57
  /** Build the exact, deterministic default experience without writing to disk. */
59
58
  export declare function generateSetup(answers: InitAnswers): GeneratedSetup;
59
+ /** Setup stopped because an existing purpose Brain holds other settings than the chosen ones. */
60
+ export declare class PurposeBrainConflictError extends Error {
61
+ }
62
+ /**
63
+ * Setup preserves existing files. A purpose Brain that already exists with a different
64
+ * harness, session, model or effort is never overwritten; setup stops and says so.
65
+ */
66
+ export declare function purposeBrainConflicts(configuration: string, answers: InitAnswers): string[];
60
67
  /** Run the mutation boundary only after the complete questionnaire is accepted. */
61
68
  export declare function executeInitWizard(prompter: InitPrompter, configuration: string, deps: InitExecutionDeps): Promise<InitPublishResult | undefined>;
62
69
  /** Execute already-collected answers through the same validation and mutation boundary. */