@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
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;
@@ -201,10 +201,21 @@ export declare class TaskRoomApplicationService {
201
201
  list: TaskListRecord;
202
202
  tasks: TaskRecord[];
203
203
  }>;
204
+ /** Run the one-time mission key migration now (service start, `task migrate-keys`). */
205
+ prepareMissionKeys(): Promise<void>;
206
+ /** One-time key migration under the list lock, honoring `tasks.default_prefix`. */
207
+ private ensureMissionKeys;
204
208
  createTaskList(input: {
205
209
  actor: TaskRoomActor;
206
210
  name: string;
211
+ prefix?: string;
207
212
  }): Promise<TaskListRecord>;
213
+ setTaskListPrefix(input: {
214
+ actor: TaskRoomActor;
215
+ name: string;
216
+ prefix: string;
217
+ }): Promise<TaskListRecord>;
218
+ suggestTaskListPrefix(name: string): Promise<string>;
208
219
  renameTaskList(input: {
209
220
  actor: TaskRoomActor;
210
221
  name: string;
@@ -7,8 +7,8 @@ import { ConfigError, loadConfig } from '../config.js';
7
7
  import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
8
8
  import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
9
9
  import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
10
- import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, updateTaskBrief, updateTaskLayout, } from '../rooms-tasks/task-state.js';
11
- import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
10
+ import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, migrateMissionKeysLocked, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, updateTaskBrief, updateTaskLayout, } from '../rooms-tasks/task-state.js';
11
+ import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, setTaskListPrefixLocked, suggestTaskListPrefix, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
12
12
  import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
13
13
  import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
14
14
  import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
@@ -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() {
@@ -100,7 +100,7 @@ export class TaskRoomApplicationService {
100
100
  const task = await withTaskListsLock(() => persistTask({
101
101
  title: request.title, brief, brief_file: request.briefFile, layout,
102
102
  origin: request.origin, idempotency_key: request.idempotencyKey,
103
- start: !request.backlog, listId: resolveTaskList(request.list ?? 'default').list_id,
103
+ start: !request.backlog, list: request.list ?? 'default',
104
104
  }));
105
105
  recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
106
106
  title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
@@ -200,6 +200,7 @@ export class TaskRoomApplicationService {
200
200
  return this.layouts().view(task);
201
201
  }
202
202
  async createTask(request) {
203
+ await this.ensureMissionKeys();
203
204
  if (request.layout !== undefined)
204
205
  return this.createLayoutTask(request);
205
206
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
@@ -229,15 +230,12 @@ export class TaskRoomApplicationService {
229
230
  plan_hash: preparedPlan.planHash };
230
231
  }
231
232
  const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
232
- task = await withTaskListsLock(() => {
233
- const list = resolveTaskList(request.list ?? 'default');
234
- return persistTask({
235
- title: request.title, brief, brief_file: request.briefFile, template: ref,
236
- execution_plan: executionPlan,
237
- origin: request.origin, idempotency_key: request.idempotencyKey,
238
- start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
239
- });
240
- });
233
+ task = await withTaskListsLock(() => persistTask({
234
+ title: request.title, brief, brief_file: request.briefFile, template: ref,
235
+ execution_plan: executionPlan,
236
+ origin: request.origin, idempotency_key: request.idempotencyKey,
237
+ start: !request.backlog, no_room: request.noRoom, list: request.list ?? 'default',
238
+ }));
241
239
  }
242
240
  catch (error) {
243
241
  unlock?.();
@@ -317,8 +315,31 @@ export class TaskRoomApplicationService {
317
315
  const lists = filter?.list ? [resolveTaskList(filter.list)] : readTaskLists();
318
316
  return lists.map(list => ({ list, tasks: tasks.filter(task => task.list_id === list.list_id) }));
319
317
  }
318
+ /** Run the one-time mission key migration now (service start, `task migrate-keys`). */
319
+ prepareMissionKeys() { return this.ensureMissionKeys(); }
320
+ /** One-time key migration under the list lock, honoring `tasks.default_prefix`. */
321
+ ensureMissionKeys() {
322
+ const configured = (() => {
323
+ try {
324
+ return (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath).tasks?.default_prefix;
325
+ }
326
+ catch {
327
+ return undefined;
328
+ }
329
+ })();
330
+ return withTaskListsLock(() => migrateMissionKeysLocked(configured));
331
+ }
320
332
  async createTaskList(input) {
321
- return withTaskListsLock(() => createTaskListLocked(input.name));
333
+ await this.ensureMissionKeys();
334
+ return withTaskListsLock(() => createTaskListLocked(input.name, input.prefix));
335
+ }
336
+ async setTaskListPrefix(input) {
337
+ await this.ensureMissionKeys();
338
+ return withTaskListsLock(() => setTaskListPrefixLocked(input.name, input.prefix));
339
+ }
340
+ async suggestTaskListPrefix(name) {
341
+ await this.ensureMissionKeys();
342
+ return suggestTaskListPrefix(name);
322
343
  }
323
344
  async renameTaskList(input) {
324
345
  return withTaskListsLock(() => renameTaskListLocked(input.name, input.newName));
@@ -1125,7 +1146,9 @@ export class TaskRoomApplicationService {
1125
1146
  throw new ConfigError('rooms: configuration is required before creating or querying rooms');
1126
1147
  return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
1127
1148
  })();
1128
- const roomName = task.task_id ? deriveTaskRoomName(task.title, task.task_id) : task.title;
1149
+ // Only tasks keyed at creation lead with the key: a room create interrupted across the
1150
+ // upgrade must keep the name its earlier attempt used.
1151
+ const roomName = task.task_id ? deriveTaskRoomName(task.title, task.task_id, task.key && !task.key_from_migration ? task.key : undefined) : task.title;
1129
1152
  const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
1130
1153
  let launchTemplate;
1131
1154
  let room;
@@ -47,6 +47,8 @@ export interface ReplaceOptions {
47
47
  */
48
48
  durable?: boolean;
49
49
  }
50
+ /** Make completed renames in `dir` durable; throws where the platform cannot. */
51
+ export declare function fsyncDirectory(dir: string): void;
50
52
  /**
51
53
  * Run `fn` holding a cross-process lock. A fully populated private claim directory
52
54
  * is atomically renamed to the canonical path, so contenders never observe partial
@@ -19,7 +19,8 @@ export class FileLockOwnerUnverifiableError extends Error {
19
19
  this.pid = pid;
20
20
  }
21
21
  }
22
- function fsyncDirectory(dir) {
22
+ /** Make completed renames in `dir` durable; throws where the platform cannot. */
23
+ export function fsyncDirectory(dir) {
23
24
  const dirFd = openSync(dir, 'r');
24
25
  try {
25
26
  fsyncSync(dirFd);
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "2.2.6",
3
- "buildId": "ce1d73f64f81",
4
- "commit": "2f3d0b2224caee214965d30a2fbad2c985510b53",
2
+ "version": "2.3.1",
3
+ "buildId": "2fbb7f11f70a",
4
+ "commit": "f7b1dda8a2cb911367ce11617ef38759ba78c5e6",
5
5
  "dirty": false,
6
- "builtAt": "2026-10-30T14:29:44.502Z",
6
+ "builtAt": "2026-10-30T19:03:10.573Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
package/dist/config.d.ts CHANGED
@@ -272,6 +272,12 @@ export interface FleetConfig {
272
272
  agentDefinitions?: Record<string, AgentDefinition>;
273
273
  /** Canonical inert templates keyed by stable template ID. */
274
274
  agentTemplates?: Record<string, AgentTemplateDefinition>;
275
+ /**
276
+ * Named Role selected by each Agent Template (`role: { ref: X }`), captured
277
+ * before roles are flattened to `inline`. Naming provenance only: it is kept
278
+ * outside the definitions so launch fingerprints are unchanged.
279
+ */
280
+ agentTemplateRoleRefs?: Record<string, string>;
275
281
  rolePresets?: Record<string, Record<string, unknown>>;
276
282
  brainPresets?: Record<string, Record<string, unknown>>;
277
283
  /** Required launch-boundary resolver for effective temporary Agent definitions. */
package/dist/config.js CHANGED
@@ -427,7 +427,7 @@ function mergeTemplateDefinition(template, overrides, file, id) {
427
427
  result[key] = merge(result[key], value, key);
428
428
  return result;
429
429
  }
430
- function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, additionalAgent, templateSink = {}, roleSink = {}, brainSink = {}) {
430
+ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, additionalAgent, templateSink = {}, roleSink = {}, brainSink = {}, roleRefSink = {}) {
431
431
  if (baseDoc.api_version !== V2_API_VERSION)
432
432
  throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
433
433
  if ('roles' in baseDoc)
@@ -472,6 +472,8 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
472
472
  validateBrainValue(templateBrain.value, templateBrain.file, templateBrain.pointer);
473
473
  validateAgentScalars(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), template.file, id);
474
474
  canonicalTemplates.set(id, { file: template.file, value });
475
+ if (isPlainObject(value.role) && typeof value.role.ref === 'string')
476
+ roleRefSink[id] = value.role.ref;
475
477
  const operational = deepSubStrict(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), vars, template.file, `/agent_templates/${id}`);
476
478
  templateSink[id] = {
477
479
  role: { inline: deepSubStrict(templateRole.value, vars, templateRole.file, templateRole.pointer) },
@@ -614,6 +616,7 @@ export function loadConfig(configPath, options = {}) {
614
616
  const files = [];
615
617
  const diagnostics = [];
616
618
  const agentTemplates = {};
619
+ const agentTemplateRoleRefs = {};
617
620
  const rolePresets = {};
618
621
  const brainPresets = {};
619
622
  let docs = [];
@@ -634,7 +637,7 @@ export function loadConfig(configPath, options = {}) {
634
637
  const dd = fleetDDir();
635
638
  if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
636
639
  throw new ConfigError(`${dd}: legacy fleet.d configuration is unsupported; run 'ours-fleet doctor' and move agents to ${splitRootFor(base)}/agents/`);
637
- docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
640
+ docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets, agentTemplateRoleRefs)];
638
641
  const baseDoc = manifestDoc;
639
642
  const vars = (baseDoc.vars ?? {});
640
643
  const defaults = (baseDoc.defaults ?? {});
@@ -822,7 +825,7 @@ export function loadConfig(configPath, options = {}) {
822
825
  }
823
826
  }
824
827
  const result = {
825
- roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
828
+ roles, agentDefinitions, agentTemplates, agentTemplateRoleRefs, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
826
829
  loops: resolvedLoops.loops,
827
830
  rooms, roomTemplates, tasks, ownerInviteFingerprint,
828
831
  configMode: 'split-v2',