@ours.network/fleet 1.2.0-nightly.36 → 1.2.0-nightly.37

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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.36",
3
- "buildId": "63df63bcda45",
4
- "commit": "b4575c95acd1ae2fee42f8f8871bfe9d816a1b1a",
2
+ "version": "1.2.0-nightly.37",
3
+ "buildId": "4ab0c90048fe",
4
+ "commit": "46569b7ed2c22fd25fd6a019a3755768c817046e",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-02T18:04:01.351Z",
6
+ "builtAt": "2026-10-02T21:13:43.143Z",
7
7
  "capabilities": [
8
8
  "workspace.enroll.preserve-profile-v1",
9
9
  "cowork.http-management-v1",
@@ -6,7 +6,8 @@ import { realExec } from '../exec.js';
6
6
  import { registerAdapter } from './registry.js';
7
7
  import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
8
8
  import { harnessRuntimeDir } from '../isolation/policy.js';
9
- import { bundledAcpAgent } from './acp-agent.js';
9
+ import { bundledAcpAgent, resolveBundledAcpAgent } from './acp-agent.js';
10
+ import { hostClaude } from './claude-runtime.js';
10
11
  import { ClaudeCodeAgentSessionAdapter } from './claude-code-session.js';
11
12
  const OPTION_KEYS = [
12
13
  'plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort',
@@ -283,6 +284,13 @@ export function makeClaudeCodeAdapter(exec = realExec, transport) {
283
284
  };
284
285
  if (!memPalace)
285
286
  env.MEMPALACE_DISABLED = 'true';
287
+ // The bundled adapter runs the Claude Code shipped with its SDK unless told otherwise. A host
288
+ // Claude Code at least as new as that copy is the one the person updates, so sessions run it.
289
+ if (!customAcpCommand(role)) {
290
+ const host = hostClaude(resolveBundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp').manifestPath, { ...process.env, ...role.env }, dirs.runCwd);
291
+ if (host)
292
+ env.CLAUDE_CODE_EXECUTABLE = host.path;
293
+ }
286
294
  // Per-role harness runtime home. Created before sandbox entry so the
287
295
  // bind has something to mount; harmless for un-isolated roles.
288
296
  // Only a role that declares `isolation:` gets a sandbox, and only a
@@ -0,0 +1,12 @@
1
+ /** The Claude Code version shipped inside the adapter's own SDK dependency. */
2
+ export declare function bundledClaudeVersion(adapterManifest: string): string | undefined;
3
+ /**
4
+ * The host's own Claude Code, when ACP sessions should run it instead of the copy bundled with the adapter.
5
+ * The person signs in to, updates and checks the host Claude Code, and a newer one knows models the bundled
6
+ * one does not. It is used only when it is at least as new as the bundled copy; an explicit
7
+ * CLAUDE_CODE_EXECUTABLE, in the role or the service environment, always decides instead.
8
+ */
9
+ export declare function hostClaude(adapterManifest: string | undefined, env: NodeJS.ProcessEnv, cwd?: string, version?: (path: string, env: NodeJS.ProcessEnv) => string | undefined): {
10
+ path: string;
11
+ version: string;
12
+ } | undefined;
@@ -0,0 +1,43 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { codexVersionAtLeast, executableOnPath } from './codex-runtime.js';
6
+ const versionOf = (text) => text.match(/\b(\d+\.\d+\.\d+)\b/)?.[1];
7
+ function installedClaudeVersion(path, env) {
8
+ const result = spawnSync(path, ['--version'], { env, encoding: 'utf8', timeout: 5_000, stdio: ['ignore', 'pipe', 'ignore'] });
9
+ return result.status === 0 ? versionOf(result.stdout ?? '') : undefined;
10
+ }
11
+ /** The Claude Code version shipped inside the adapter's own SDK dependency. */
12
+ export function bundledClaudeVersion(adapterManifest) {
13
+ try {
14
+ const sdk = dirname(createRequire(adapterManifest).resolve('@anthropic-ai/claude-agent-sdk'));
15
+ const version = JSON.parse(readFileSync(join(sdk, 'package.json'), 'utf8')).claudeCodeVersion;
16
+ return typeof version === 'string' ? version : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /**
23
+ * The host's own Claude Code, when ACP sessions should run it instead of the copy bundled with the adapter.
24
+ * The person signs in to, updates and checks the host Claude Code, and a newer one knows models the bundled
25
+ * one does not. It is used only when it is at least as new as the bundled copy; an explicit
26
+ * CLAUDE_CODE_EXECUTABLE, in the role or the service environment, always decides instead.
27
+ */
28
+ export function hostClaude(adapterManifest, env, cwd = process.cwd(), version = installedClaudeVersion) {
29
+ if (env.CLAUDE_CODE_EXECUTABLE !== undefined || !adapterManifest)
30
+ return undefined;
31
+ const bundled = bundledClaudeVersion(adapterManifest);
32
+ if (!bundled)
33
+ return undefined;
34
+ let path;
35
+ try {
36
+ path = executableOnPath('claude', env, cwd);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ const found = version(path, env);
42
+ return found && codexVersionAtLeast(found, bundled) ? { path, version: found } : undefined;
43
+ }
@@ -53,6 +53,8 @@ export declare function validateCatalog(value: BrainCatalog, path?: string): Bra
53
53
  export declare function readInitSettings(path: string): InitAnswers;
54
54
  export declare function askInitQuestions(prompter: InitPrompter, configuration: string): Promise<InitAnswers | undefined>;
55
55
  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;
56
58
  /** Build the exact, deterministic default experience without writing to disk. */
57
59
  export declare function generateSetup(answers: InitAnswers): GeneratedSetup;
58
60
  /** Run the mutation boundary only after the complete questionnaire is accepted. */
@@ -173,6 +173,7 @@ export function formatSetupSummary(answers, configuration) {
173
173
  ' Team: LocalCoordinator + Developer + Critic',
174
174
  ' Gated engineering: Engineer (gate pipeline) + independent Critic',
175
175
  ' FleetCoordinator: coordination model',
176
+ ' Brains: the packaged presets; each prepared agent references the preset of its chosen model',
176
177
  '',
177
178
  'After the final Yes, Fleet performs host service setup before publishing the complete configuration.',
178
179
  'A hard termination after that Yes can leave host integration or private stage/recovery evidence to inspect.',
@@ -181,6 +182,11 @@ export function formatSetupSummary(answers, configuration) {
181
182
  function preset(relative) {
182
183
  return readFileSync(join(packagedPresetRoot(), 'fleet', relative), 'utf8');
183
184
  }
185
+ /** The packaged Brain preset for a catalog model at one effort; the same naming the presets are generated with. */
186
+ export function brainPresetId(model, effort) {
187
+ return `${model.harness === 'claude-code' ? 'claude' : 'codex'}-${model.model
188
+ .replace(/^claude-/, '').replace(/\./g, '-').toLowerCase()}-${effort}`;
189
+ }
184
190
  /** Build the exact, deterministic default experience without writing to disk. */
185
191
  export function generateSetup(answers) {
186
192
  const effort = REASONING_EFFORT[answers.reasoning];
@@ -201,6 +207,7 @@ export function generateSetup(answers) {
201
207
  ' permissions: { approval: ask, filesystem: workspace, unattended: deny }',
202
208
  '',
203
209
  ].join('\n'));
210
+ const assigned = {};
204
211
  for (const work of WORK_KINDS) {
205
212
  const proposed = answers.models[work];
206
213
  if (!proposed || !answers.subscriptions.includes(subscriptionFor(proposed)))
@@ -214,14 +221,14 @@ export function generateSetup(answers) {
214
221
  throw new Error(`${work} contains tampered model capabilities`);
215
222
  if (!model.efforts.includes(effort))
216
223
  throw new Error(`${MODEL_LABELS[model.model]} does not support ${answers.reasoning} reasoning`);
217
- files.set(`brains/${work}.yaml`, [
218
- `harness: ${model.harness}`,
219
- `session: ${model.session}`,
220
- `model: ${model.model}`,
221
- `effort: ${effort}`,
222
- '',
223
- ].join('\n'));
224
+ // The chosen model is assigned by reference to its packaged Brain preset; setup defines no Brain of its own.
225
+ const id = brainPresetId(model, effort);
226
+ if (!existsSync(join(packagedPresetRoot(), 'fleet', 'brains', `${id}.yaml`)))
227
+ throw new Error(`no packaged Brain preset for ${MODEL_LABELS[model.model]} at ${effort} reasoning`);
228
+ assigned[work] = id;
224
229
  }
230
+ for (const name of readdirSync(join(packagedPresetRoot(), 'fleet', 'brains')).filter(name => name.endsWith('.yaml')).sort())
231
+ files.set(`brains/${name}`, preset(`brains/${name}`));
225
232
  if (answers.assignmentStrategy === 'one-model') {
226
233
  const tuples = new Set(WORK_KINDS.map(work => {
227
234
  const model = answers.models[work];
@@ -236,7 +243,7 @@ export function generateSetup(answers) {
236
243
  continue;
237
244
  files.set(`agent_templates/${role}.yaml`, [
238
245
  `role: { ref: ${role} }`,
239
- `brain: { ref: ${ROLE_WORK[role]} }`,
246
+ `brain: { ref: ${assigned[ROLE_WORK[role]]} }`,
240
247
  'coordinator: FleetCoordinator',
241
248
  // Temporary default while the product is being tested: prepared agents work without asking.
242
249
  'permissions: { approval: allow, filesystem: unrestricted, unattended: deny }',
@@ -245,7 +252,7 @@ export function generateSetup(answers) {
245
252
  ].join('\n'));
246
253
  }
247
254
  files.set('agents/FleetCoordinator.yaml', preset('agents/FleetCoordinator.yaml')
248
- .replace('brain: { ref: claude-default }', 'brain: { ref: coordination }'));
255
+ .replace('brain: { ref: claude-default }', `brain: { ref: ${assigned.coordination} }`));
249
256
  for (const name of ['single', 'pair', 'team', 'engineering']) {
250
257
  files.set(`room_templates/${name}.yaml`, preset(`room_templates/${name}.yaml`));
251
258
  files.set(`room_layouts/${name}.yaml`, preset(`room_layouts/${name}.yaml`));
@@ -67,6 +67,14 @@ const V5_GENERATED_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
67
67
  'agent_templates/Developer.yaml': 'fdd953aab72d042e62780dd273598745aae59521b39edf4876d7676bb7189c83',
68
68
  'agent_templates/Critic.yaml': '3e98a28f17876061cfadf984351aa59fdbe19629e514eff52c74546545c64a91',
69
69
  });
70
+ // The packaged Coordinator role of revisions 8 and 9 addressed a named owner channel that a
71
+ // fresh installation does not have. Untouched copies adopt the current role; edited ones stay.
72
+ const OWNER_CHANNEL_COORDINATOR_FINGERPRINTS = Object.freeze({
73
+ 'roles/Coordinator.yaml': [
74
+ '8c8ad28d9bb9ddf9dba8c5c9c7859f25edd8fa10ebf833e2340124a6e49583ec',
75
+ 'f0d8b169ad0a18051b57e5280b93cf01e05faa5c12f4e12bdf89973edf2ef58a',
76
+ ],
77
+ });
70
78
  const CURRENT_ROLE_DEFAULTS = new Set([
71
79
  'roles/Coordinator.yaml', 'roles/LocalCoordinator.yaml', 'roles/Developer.yaml', 'roles/Critic.yaml',
72
80
  'roles/Engineer.yaml',
@@ -196,6 +204,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
196
204
  ...Object.keys(V3_ROLE_DEFAULT_FINGERPRINTS),
197
205
  ...Object.keys(V4_ROLE_DEFAULT_FINGERPRINTS),
198
206
  ...Object.keys(V5_ROLE_DEFAULT_FINGERPRINTS),
207
+ ...Object.keys(OWNER_CHANNEL_COORDINATOR_FINGERPRINTS),
199
208
  ]);
200
209
  for (const relative of known) {
201
210
  const path = join(root, relative);
@@ -209,6 +218,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
209
218
  V4_GENERATED_ROLE_DEFAULT_FINGERPRINTS[relative],
210
219
  V5_ROLE_DEFAULT_FINGERPRINTS[relative],
211
220
  V5_GENERATED_ROLE_DEFAULT_FINGERPRINTS[relative],
221
+ ...(OWNER_CHANNEL_COORDINATOR_FINGERPRINTS[relative] ?? []),
212
222
  ].some(expected => expected !== undefined && actual === expected);
213
223
  if (recognized)
214
224
  (CURRENT_ROLE_DEFAULTS.has(relative) ? replacements : removals).push(path);
@@ -339,7 +339,10 @@ export declare class AcpSession implements AgentSession {
339
339
  * signal. Never infer commentary from text, message order, or unknown meta.
340
340
  */
341
341
  private codexMessagePhase;
342
- /** Normalize every ACP update losslessly into the durable ledger. */
342
+ /**
343
+ * Normalize every ACP update losslessly into the durable ledger. The ledger is the conversation the
344
+ * owner reads, so the agent's commentary between its tools is kept as written, like its final answer.
345
+ */
343
346
  private recordConversationUpdate;
344
347
  taskNoticeBinding(): TaskNoticeBinding | undefined;
345
348
  recordTaskCreated(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
@@ -38,7 +38,6 @@ const TERMINAL_TOOL_STATUSES = new Set(['completed', 'failed']);
38
38
  /** Consumed only by Fleet's authenticated bundled-Codex app-server proxy. */
39
39
  export const CODEX_DISABLE_INHERITED_MCP_ENV = 'OURS_FLEET_CODEX_DISABLE_INHERITED_MCP';
40
40
  const SCHEDULED_LOOP_REDACTION = '[scheduled-loop content redacted]';
41
- const OWNER_COMMENTARY_REDACTION = '[assistant commentary redacted]';
42
41
  const MAX_CANONICAL_SYMLINK_DEPTH = 40;
43
42
  const scheduledTurn = (turn) => turn?.origin?.kind === 'scheduled-loop';
44
43
  /**
@@ -1711,7 +1710,7 @@ export class AcpSession {
1711
1710
  const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
1712
1711
  const messagePhase = update.sessionUpdate === 'agent_message_chunk'
1713
1712
  ? this.codexMessagePhase(update) : undefined;
1714
- this.recordConversationUpdate(update, scheduled, messagePhase === 'commentary');
1713
+ this.recordConversationUpdate(update, scheduled);
1715
1714
  if (update.sessionUpdate === 'config_option_update')
1716
1715
  this.captureRuntimeMetadata(update.configOptions);
1717
1716
  if (update.sessionUpdate === 'usage_update') {
@@ -1795,13 +1794,15 @@ export class AcpSession {
1795
1794
  return undefined;
1796
1795
  return phase === 'commentary' || phase === 'final_answer' ? phase : 'ambiguous';
1797
1796
  }
1798
- /** Normalize every ACP update losslessly into the durable ledger. */
1799
- recordConversationUpdate(update, scheduled, commentary = false) {
1797
+ /**
1798
+ * Normalize every ACP update losslessly into the durable ledger. The ledger is the conversation the
1799
+ * owner reads, so the agent's commentary between its tools is kept as written, like its final answer.
1800
+ */
1801
+ recordConversationUpdate(update, scheduled) {
1800
1802
  const normalized = normalizeSessionUpdate(update, scheduled ? {
1801
1803
  redactText: SCHEDULED_LOOP_REDACTION,
1802
1804
  redactToolCallId: 'scheduled-loop-tool',
1803
- }
1804
- : commentary ? { redactText: OWNER_COMMENTARY_REDACTION } : {});
1805
+ } : {});
1805
1806
  this.conversation.appendSafe({
1806
1807
  kind: normalized.kind,
1807
1808
  sessionGeneration: this.sessionGeneration,
@@ -14,7 +14,6 @@ const INTERRUPT_TERMINATE_GRACE_MS = 5_000;
14
14
  const TERMINAL_RECONCILE_QUIET_MS = 2_000;
15
15
  const TERMINAL_RECONCILE_REQUEST_TIMEOUT_MS = 5_000;
16
16
  const SCHEDULED_REDACTION = '[scheduled-loop content redacted]';
17
- const COMMENTARY_REDACTION = '[assistant commentary redacted]';
18
17
  const OWNER_ADMIN_CONTEXT_KEY = 'ours-fleet://prompt-provenance?source=owner_admin_console';
19
18
  const OWNER_CHANNEL_CONTEXT_KEY = 'ours-fleet://prompt-provenance?source=owner_channel';
20
19
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -719,7 +718,7 @@ export class CodexAppServerSession {
719
718
  promptId: this.activeTurn?.promptId, turnId: this.activeTurn?.promptId,
720
719
  messageId: id, source: 'agent',
721
720
  payload: {
722
- role: 'assistant', content: textBlock(delta, scheduled ? SCHEDULED_REDACTION : phase === 'commentary' ? COMMENTARY_REDACTION : undefined),
721
+ role: 'assistant', content: textBlock(delta, scheduled ? SCHEDULED_REDACTION : undefined),
723
722
  },
724
723
  });
725
724
  }
@@ -202,9 +202,13 @@ export async function buildWebServer(services, boundary, options = {}) {
202
202
  throw new FleetError('backend_failure', `Fleet setup did not complete: ${error.message}`);
203
203
  }
204
204
  // Setup preserves files it finds, so confirm the published models are the ones this request chose.
205
- const brains = configuration.read(true).model.brains ?? {};
205
+ // The prepared agents reference packaged Brain presets, so resolve what each of them now points at:
206
+ // a preserved file of a packaged name may hold another model, session or reasoning than its name says.
207
+ const published = configuration.read(true).model, brains = published.brains ?? {};
208
+ const brainOf = (holder) => brains[String(holder?.brain?.ref ?? '')];
209
+ const assigned = { coordination: brainOf(published.agents.FleetCoordinator), development: brainOf(published.agent_templates?.Developer), review: brainOf(published.agent_templates?.Critic) };
206
210
  for (const work of ONBOARDING_WORK)
207
- if (brains[work]?.harness !== models[work].harness || brains[work]?.model !== models[work].model)
211
+ if (assigned[work]?.harness !== models[work].harness || assigned[work]?.model !== models[work].model || assigned[work]?.session !== models[work].session || assigned[work]?.effort !== 'medium')
208
212
  throw new FleetError('conflict', 'Fleet was set up with different models; review its configuration');
209
213
  return { configured: true };
210
214
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.36",
3
+ "version": "1.2.0-nightly.37",
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",
@@ -2,16 +2,12 @@ mission: Keep fleet development moving by managing the task backlog, rooms, and
2
2
  persona: |
3
3
  You are the fleet Coordinator. Own progress and workflow, not execution quality or results.
4
4
 
5
- Each time your session starts, before anything else, use ours `send_message` to tell
6
- `Coordinator Owner Channel` in two or three plain sentences that you are the Fleet Coordinator,
7
- that you have started and are ready, and that the owner can ask you anything about the fleet:
8
- how the app works, and how to set up new agents, tasks and rooms. Send this introduction once
9
- per start; it is not a permission request and needs no reply.
10
-
11
- Immediately before each meaningful action, proactively use ours `send_message` to contact
12
- `Coordinator Owner Channel` with a short, plain one-sentence statement of what you are about to do.
13
- This is an informational intent notice, never a permission request; proceed without waiting unless
14
- a separate safety or authority rule genuinely requires approval.
5
+ Each time your session starts, before anything else, introduce yourself in your first reply in
6
+ this conversation, in two or three plain sentences: you are the Fleet Coordinator, you have
7
+ started and are ready, and the owner can ask you anything about the fleet: how the app works, and
8
+ how to set up new agents, tasks and rooms. Do this once per start; it is not a permission request
9
+ and needs no reply. The owner reads this conversation: do not send the introduction, or any other
10
+ notice, to a separate contact or channel, and never report a missing contact or channel as a blocker.
15
11
 
16
12
  Out of scope: do not personally implement, inspect, analyze, research, review, test, validate,
17
13
  or otherwise execute task work. Delegate all execution to temporary agents in managed rooms.
@@ -43,13 +39,12 @@ persona: |
43
39
  For every new task:
44
40
  1. Record it first with `ours-fleet task create --title "<title>" --brief "<brief>" --backlog --no-room`.
45
41
  2. Ask the owner which room template to use: `single`, `pair`, or `team`.
46
- 3. Resolve the exact launch plan without creating the room. Immediately before room creation, send
47
- one proactive message to `Coordinator Owner Channel` stating that you are about to create the
48
- room and listing the task, selected template, every member role and count, and each member's
49
- Agent Template, Brain, harness, session, model, reasoning effort, permissions, and working
50
- directory. Wait for `send_message` to confirm delivery. This detailed message replaces the
51
- generic meaningful-action notice for room creation. If delivery fails, do not create the room.
52
- 4. Only after confirmed delivery, start it with
42
+ 3. Resolve the exact launch plan without creating the room. Immediately before room creation,
43
+ state in this conversation that you are about to create the room, listing the task, selected
44
+ template, every member role and count, and each member's Agent Template,
45
+ Brain, harness, session, model, reasoning effort, permissions, and working directory.
46
+ This is a notice, not a permission request; do not wait for a reply.
47
+ 4. Then start it with
53
48
  `ours-fleet task start <task-id> --template <template>`.
54
49
 
55
50
  Keep the backlog current with `ours-fleet task list --state all` and use `task show`, `task block`,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "preset_revision": 9,
3
+ "preset_revision": 10,
4
4
  "brain_catalog_revision": 4,
5
5
  "description": "ours-fleet standard configuration presets"
6
6
  }