@ours.network/fleet 1.1.0-nightly.7 → 1.1.0-nightly.9

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 (52) hide show
  1. package/README.md +23 -4
  2. package/dist/application/role-creation-service.js +1 -1
  3. package/dist/application/task-room-service.d.ts +1 -1
  4. package/dist/application/task-room-service.js +147 -14
  5. package/dist/briefing.js +6 -6
  6. package/dist/build-info.json +4 -4
  7. package/dist/cli.js +8 -11
  8. package/dist/config-yaml.d.ts +1 -1
  9. package/dist/config.d.ts +1 -1
  10. package/dist/config.js +49 -2
  11. package/dist/docs.d.ts +1 -1
  12. package/dist/docs.js +39 -18
  13. package/dist/fleet-command-audit.d.ts +37 -8
  14. package/dist/fleet-command-audit.js +120 -62
  15. package/dist/init-guidance.d.ts +1 -1
  16. package/dist/init-guidance.js +4 -3
  17. package/dist/owner-channel/channel.d.ts +2 -2
  18. package/dist/owner-channel/channel.js +31 -38
  19. package/dist/owner-channel/commands.js +3 -9
  20. package/dist/owner-channel/state.d.ts +2 -0
  21. package/dist/owner-channel/state.js +8 -2
  22. package/dist/preset-bootstrap.d.ts +10 -0
  23. package/dist/preset-bootstrap.js +85 -0
  24. package/dist/rooms-tasks/cli.js +121 -42
  25. package/dist/rooms-tasks/config.js +0 -4
  26. package/dist/rooms-tasks/provision.js +28 -1
  27. package/dist/rooms-tasks/templates.d.ts +0 -1
  28. package/dist/rooms-tasks/templates.js +9 -76
  29. package/dist/rooms-tasks/types.d.ts +2 -1
  30. package/dist/runner.js +13 -1
  31. package/dist/session/control.d.ts +1 -1
  32. package/dist/session/control.js +1 -1
  33. package/dist/web/fleet-config-service.js +4 -3
  34. package/package.json +2 -1
  35. package/presets/fleet/agents/Agent.yaml +3 -0
  36. package/presets/fleet/agents/Architect.yaml +3 -0
  37. package/presets/fleet/agents/Critic.yaml +3 -0
  38. package/presets/fleet/agents/Developer.yaml +3 -0
  39. package/presets/fleet/agents/Secretary.yaml +3 -0
  40. package/presets/fleet/agents/Tester.yaml +3 -0
  41. package/presets/fleet/brains/claude-default.yaml +5 -0
  42. package/presets/fleet/roles/Agent.yaml +6 -0
  43. package/presets/fleet/roles/Architect.yaml +6 -0
  44. package/presets/fleet/roles/Critic.yaml +6 -0
  45. package/presets/fleet/roles/Developer.yaml +6 -0
  46. package/presets/fleet/roles/Secretary.yaml +6 -0
  47. package/presets/fleet/roles/Tester.yaml +6 -0
  48. package/presets/fleet/room_templates/pair.yaml +11 -0
  49. package/presets/fleet/room_templates/single.yaml +8 -0
  50. package/presets/fleet/room_templates/team.yaml +12 -0
  51. package/presets/fleet.yaml +7 -0
  52. package/presets/manifest.json +5 -0
@@ -43,7 +43,7 @@ function taskRoomPublicFailure(error) {
43
43
  case 'template_not_found': return {
44
44
  legacy: `template not found: ${f.template ?? 'requested-template'}`, kind: 'not_found',
45
45
  detail: `The requested template ${f.template ?? ''}`.trim() + ' was not found.',
46
- action: 'Run ours-fleet template list and retry with an available template.',
46
+ action: 'Run ours-fleet init to seed missing file-backed presets, then run ours-fleet template list.',
47
47
  };
48
48
  case 'template_mismatch': return {
49
49
  legacy: `template ${f.requested ?? 'requested-template'} does not match room ${f.room ?? 'requested-room'}'s provisioned template ${f.provisioned ?? 'recorded-template'}`,
@@ -215,35 +215,81 @@ const taskActionMarkdown = (title, task, fields = []) => {
215
215
  ],
216
216
  });
217
217
  };
218
- function safeBrainSummary(definition) {
219
- const brain = definition?.brain;
220
- if (!brain || typeof brain !== 'object' || Array.isArray(brain))
218
+ function safeSelectionSummary(definition, kind) {
219
+ const selected = definition?.[kind];
220
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
221
221
  return 'unresolved';
222
- const selection = brain;
222
+ const selection = selected;
223
+ if (selection.kind === 'ref' && typeof selection.id === 'string')
224
+ return `ref:${selection.id} (reference)`;
225
+ if (selection.kind === 'inline' && typeof selection.fingerprint === 'string')
226
+ return `inline:${selection.fingerprint} (inline)`;
223
227
  if (typeof selection.ref === 'string')
224
228
  return `ref:${selection.ref}`;
225
229
  if (!Object.hasOwn(selection, 'inline'))
226
230
  return 'unresolved';
227
- const fingerprint = createHash('sha256').update(JSON.stringify(selection.inline)).digest('hex').slice(0, 16);
231
+ const canonical = (value) => Array.isArray(value)
232
+ ? `[${value.map(canonical).join(',')}]`
233
+ : value && typeof value === 'object'
234
+ ? `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
235
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(',')}}`
236
+ : JSON.stringify(value);
237
+ const fingerprint = createHash('sha256').update(canonical(selection.inline)).digest('hex').slice(0, 16);
228
238
  return `inline:sha256:${fingerprint}`;
229
239
  }
230
- function auditTask(operation, task, previousState, newState = task.state) {
240
+ function safePermissionsSummary(definition) {
241
+ const permissions = definition?.permissions;
242
+ if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions))
243
+ return undefined;
244
+ const policy = permissions;
245
+ const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof policy[key] === 'string' ? [`${key}=${policy[key]}`] : []);
246
+ return fields.length ? fields.join(',') : undefined;
247
+ }
248
+ function auditTask(operation, task, previousState, newState = task.state, revision) {
249
+ const materialSameState = operation === 'block' || operation === 'unblock' || operation === 'settling';
250
+ if (previousState === newState && !materialSameState)
251
+ return;
231
252
  recordFleetAuditResource('task', task.task_id);
232
253
  if (task.room_id)
233
254
  recordFleetAuditResource('room', task.room_id);
234
255
  const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
235
- const brains = new Map(room?.member_seats.map(seat => [seat.role_name, safeBrainSummary(seat.launch?.agent_definition)]) ?? []);
236
- recordFleetAuditPresentation({ kind: 'task', operation, id: task.task_id, title: task.title,
256
+ const definitions = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.agent_definition]) ?? []);
257
+ const semanticOperation = operation === 'recover'
258
+ ? newState === 'active' ? 'work' : newState === 'done' ? 'done'
259
+ : newState === 'cancelled' ? 'cancel' : undefined
260
+ : operation;
261
+ if (!semanticOperation)
262
+ return;
263
+ recordFleetAuditPresentation({ kind: 'task', operation: semanticOperation, id: task.task_id, title: task.title,
237
264
  previousState, newState,
265
+ revision: revision ?? task.blocked?.at ?? task.ended_at ?? task.started_at ?? task.created_at,
266
+ list: task.list_name ?? 'default',
238
267
  template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
239
- roomId: task.room_id, agents: task.member_roles.map(member => ({ name: member.name,
240
- brain: brains.get(member.name) ?? 'unresolved', role: member.cowork_role })) });
268
+ roomId: task.room_id, agents: [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => ({ name: member.name,
269
+ brain: safeSelectionSummary(definitions.get(member.name), 'brain'),
270
+ role: safeSelectionSummary(definitions.get(member.name), 'role') === 'unresolved'
271
+ ? member.cowork_role : safeSelectionSummary(definitions.get(member.name), 'role'),
272
+ permissions: safePermissionsSummary(definitions.get(member.name)) })) });
241
273
  }
242
274
  function auditRoom(operation, room, previousState, newState = room.state) {
275
+ if (previousState === newState)
276
+ return;
243
277
  recordFleetAuditResource('room', room.room_id);
244
- recordFleetAuditPresentation({ kind: 'room', operation, id: room.room_id, previousState, newState,
278
+ const semanticOperation = operation === 'recover'
279
+ ? newState === 'active' ? 'activate' : newState === 'deleted' ? 'delete'
280
+ : newState === 'closing' ? 'close' : undefined
281
+ : operation;
282
+ if (!semanticOperation)
283
+ return;
284
+ recordFleetAuditPresentation({ kind: 'room', operation: semanticOperation, id: room.room_id, previousState, newState,
285
+ revision: room.closed_at ?? room.activated_at ?? room.close?.accepted_at ?? room.created_at,
286
+ name: room.room_name, taskId: room.task_id,
245
287
  template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
246
- participants: room.member_seats.map(member => ({ name: member.role_name, role: member.cowork_role })) });
288
+ participants: [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(member => ({ name: member.role_name, id: member.identity_cid,
289
+ brain: safeSelectionSummary(member.launch?.agent_definition, 'brain'),
290
+ role: safeSelectionSummary(member.launch?.agent_definition, 'role') === 'unresolved'
291
+ ? member.cowork_role : safeSelectionSummary(member.launch?.agent_definition, 'role'),
292
+ permissions: safePermissionsSummary(member.launch?.agent_definition) })) });
247
293
  }
248
294
  const roomActionMarkdown = (title, room, fields = []) => {
249
295
  recordFleetAuditResource('room', room.room_id);
@@ -272,6 +318,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
272
318
  actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
273
319
  recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
274
320
  });
321
+ const task = getTask(taskId);
322
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
323
+ state: task.state, category: 'settlement_failed',
324
+ eventId: task.terminal_intent?.error_at ?? task.terminal_intent?.accepted_at ?? task.created_at });
275
325
  throw error;
276
326
  }
277
327
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -284,7 +334,11 @@ async function launchTaskSettleWorker(taskId, configPath) {
284
334
  }
285
335
  await sleep(PUBLIC_SETTLE_POLL_MS);
286
336
  }
287
- return { task: getTask(taskId), timedOut: true };
337
+ const task = getTask(taskId);
338
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
339
+ state: task.state, category: 'settlement_pending',
340
+ eventId: task.terminal_intent?.accepted_at ?? task.created_at });
341
+ return { task, timedOut: true };
288
342
  }
289
343
  async function launchRoomDeleteWorker(roomId, configPath) {
290
344
  const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
@@ -296,6 +350,11 @@ async function launchRoomDeleteWorker(roomId, configPath) {
296
350
  actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
297
351
  recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
298
352
  });
353
+ const room = getRoomRecord(roomId);
354
+ if (room)
355
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
356
+ state: room.state, category: 'cleanup_failed',
357
+ eventId: room.close?.error_at ?? room.close?.accepted_at ?? room.created_at });
299
358
  throw error;
300
359
  }
301
360
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -308,7 +367,12 @@ async function launchRoomDeleteWorker(roomId, configPath) {
308
367
  }
309
368
  await sleep(PUBLIC_SETTLE_POLL_MS);
310
369
  }
311
- return { deleted: getRoomRecord(roomId) === undefined, timedOut: true };
370
+ const remaining = getRoomRecord(roomId);
371
+ if (remaining)
372
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
373
+ state: remaining.state, category: 'cleanup_pending',
374
+ eventId: remaining.close?.accepted_at ?? remaining.created_at });
375
+ return { deleted: remaining === undefined, timedOut: true };
312
376
  }
313
377
  function loadCfg(opts) {
314
378
  return loadConfig(opts.configuration);
@@ -410,21 +474,36 @@ async function provisionRoom(cfg, input) {
410
474
  setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
411
475
  ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
412
476
  : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
477
+ const failed = getRoomRecord(record.room_id);
478
+ if (failed)
479
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
480
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
481
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
413
482
  throw error;
414
483
  }
415
484
  }
416
485
  record = advanceSaga(record.room_id, 'create_members', 3);
417
486
  if (input.template && input.template.members.length > 0) {
418
- record = await provisionMembers({
419
- cfg,
420
- cowork,
421
- roomId: record.room_id,
422
- taskId: input.taskId,
423
- template: input.template,
424
- binPath: getBinPath(),
425
- brief: input.brief,
426
- goal: input.goal,
427
- });
487
+ try {
488
+ record = await provisionMembers({
489
+ cfg,
490
+ cowork,
491
+ roomId: record.room_id,
492
+ taskId: input.taskId,
493
+ template: input.template,
494
+ binPath: getBinPath(),
495
+ brief: input.brief,
496
+ goal: input.goal,
497
+ });
498
+ }
499
+ catch (error) {
500
+ const failed = getRoomRecord(record.room_id);
501
+ if (failed)
502
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
503
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
504
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
505
+ throw error;
506
+ }
428
507
  }
429
508
  else {
430
509
  record = activateRoom(record.room_id);
@@ -446,12 +525,11 @@ export function registerTemplateCommands(parent, cOpt) {
446
525
  return;
447
526
  }
448
527
  if (!templates.length) {
449
- console.log('No templates configured.');
528
+ console.log('No templates configured. Run ours-fleet init to seed missing file-backed presets.');
450
529
  return;
451
530
  }
452
531
  for (const t of templates) {
453
- const tag = t.builtin ? ' (built-in)' : '';
454
- console.log(`${t.name}@${t.version}${tag} ${t.description}`);
532
+ console.log(`${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
455
533
  }
456
534
  }
457
535
  catch (e) {
@@ -469,6 +547,7 @@ export function registerTemplateCommands(parent, cOpt) {
469
547
  return;
470
548
  }
471
549
  console.log(`${t.name}@${t.version} ${t.description}`);
550
+ console.log(`Source: ${t.sourceFile ?? 'manifest'}`);
472
551
  if (t.contract)
473
552
  console.log(`\nContract:\n${t.contract}`);
474
553
  console.log(`\nMembers:`);
@@ -529,7 +608,6 @@ export function registerTaskCommands(parent, cOpt) {
529
608
  idempotencyKey: opts.idempotencyKey, origin: { type: 'cli' },
530
609
  list: opts.list,
531
610
  });
532
- auditTask('create', record, 'none');
533
611
  if (opts.json) {
534
612
  console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
535
613
  return;
@@ -716,11 +794,9 @@ export function registerTaskCommands(parent, cOpt) {
716
794
  .option('--json', 'JSON output')
717
795
  .action(async (id, opts) => {
718
796
  try {
719
- const previous = getTask(id).state;
720
797
  const t = await taskRoomService(opts.configuration).startTask({
721
798
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
722
799
  });
723
- auditTask('start', t, previous);
724
800
  if (opts.json) {
725
801
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
726
802
  return;
@@ -766,11 +842,12 @@ export function registerTaskCommands(parent, cOpt) {
766
842
  .option('--json', 'JSON output')
767
843
  .action((id, opts) => {
768
844
  try {
769
- const previous = getTask(id).state;
845
+ const prior = getTask(id);
846
+ const previous = prior.state;
770
847
  const t = taskRoomService().unblockTask({
771
848
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
772
849
  });
773
- auditTask('unblock', t, previous);
850
+ auditTask('unblock', t, previous, t.state, prior.blocked?.at);
774
851
  if (opts.json) {
775
852
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
776
853
  return;
@@ -820,6 +897,8 @@ export function registerTaskCommands(parent, cOpt) {
820
897
  const plan = await taskRoomService(opts.configuration).completeTask({
821
898
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
822
899
  });
900
+ if (plan.task.terminal_intent?.status === 'pending')
901
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
823
902
  const settled = plan.settlementRequired
824
903
  ? await launchTaskSettleWorker(id, opts.configuration)
825
904
  : { task: plan.task, timedOut: false };
@@ -856,6 +935,8 @@ export function registerTaskCommands(parent, cOpt) {
856
935
  const plan = await taskRoomService(opts.configuration).cancelTask({
857
936
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
858
937
  });
938
+ if (plan.task.terminal_intent?.status === 'pending')
939
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
859
940
  const settled = plan.settlementRequired
860
941
  ? await launchTaskSettleWorker(id, opts.configuration)
861
942
  : { task: plan.task, timedOut: false };
@@ -1037,12 +1118,10 @@ export function registerTaskCommands(parent, cOpt) {
1037
1118
  .option('--json', 'JSON output')
1038
1119
  .action(async (id, opts) => {
1039
1120
  try {
1040
- const previous = getTask(id).state;
1041
1121
  const result = await taskRoomService(opts.configuration).ensureTaskWork({
1042
1122
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, template: opts.template,
1043
1123
  });
1044
1124
  const t = result.task;
1045
- auditTask('work', t, previous);
1046
1125
  if (result.status === 'already_active') {
1047
1126
  if (opts.json) {
1048
1127
  console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
@@ -1089,6 +1168,8 @@ export function registerTaskCommands(parent, cOpt) {
1089
1168
  const plan = await taskRoomService(opts.configuration).finishTask({
1090
1169
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
1091
1170
  });
1171
+ if (plan.task.terminal_intent?.status === 'pending')
1172
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
1092
1173
  const settled = plan.settlementRequired
1093
1174
  ? await launchTaskSettleWorker(id, opts.configuration)
1094
1175
  : { task: plan.task, timedOut: false };
@@ -1133,7 +1214,6 @@ export function registerRoomCommands(parent, cOpt) {
1133
1214
  actor: { kind: 'local_control', surface: 'cli' }, name: opts.name,
1134
1215
  template: opts.template, goal: opts.goal, brief: opts.brief, briefFile: opts.briefFile,
1135
1216
  });
1136
- auditRoom('create', record, 'none');
1137
1217
  if (opts.json) {
1138
1218
  console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
1139
1219
  return;
@@ -1219,8 +1299,6 @@ export function registerRoomCommands(parent, cOpt) {
1219
1299
  .action(async (id, opts) => {
1220
1300
  try {
1221
1301
  const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
1222
- if (orchestration)
1223
- auditRoom('open', orchestration, orchestration.state);
1224
1302
  const url = `http://localhost:4460/room/${id}`;
1225
1303
  if (opts.json) {
1226
1304
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
@@ -1247,8 +1325,6 @@ export function registerRoomCommands(parent, cOpt) {
1247
1325
  .action(async (id, opts) => {
1248
1326
  try {
1249
1327
  const { orchestration: r, members } = await taskRoomService(opts.configuration).getRoomMembers(id);
1250
- if (r)
1251
- auditRoom('members', r, r.state);
1252
1328
  if (opts.json) {
1253
1329
  console.log(JSON.stringify({
1254
1330
  schema_version: 1,
@@ -1286,9 +1362,12 @@ export function registerRoomCommands(parent, cOpt) {
1286
1362
  await taskRoomService(opts.configuration).requestRoomDeletion({
1287
1363
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1288
1364
  });
1365
+ const closing = getRoomRecord(id);
1366
+ if (previous && closing && previous.state !== closing.state)
1367
+ auditRoom('close', closing, previous.state, closing.state);
1289
1368
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1290
- if (previous)
1291
- auditRoom('delete', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
1369
+ if (closing)
1370
+ auditRoom('delete', closing, closing.state, settled.deleted ? 'deleted' : 'closing');
1292
1371
  if (opts.json) {
1293
1372
  console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
1294
1373
  return;
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, } from './types.js';
4
- import { BUILTIN_TEMPLATES } from './templates.js';
5
4
  import { isSensitiveConfigKey } from '../sensitive-config.js';
6
5
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
7
6
  const CID_RE = /^[0-9a-fA-F]{64}$/;
@@ -117,9 +116,6 @@ export function validateRoomTemplatesConfig(raw, path) {
117
116
  if (!isPlainObject(tplRaw))
118
117
  throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
119
118
  rejectUnknown(tplRaw, [...TPK, 'name'], path, `room_templates.${name}`);
120
- const isBuiltinOverride = BUILTIN_TEMPLATES.some(b => b.name === name);
121
- if (isBuiltinOverride && !tplRaw.override_builtin)
122
- throw new RoomsTasksConfigError(path, `room_templates.${name}: overrides built-in template; set override_builtin: true and bump version`);
123
119
  const version = tplRaw.version;
124
120
  if (version === undefined || !Number.isInteger(version) || version < 1)
125
121
  throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
@@ -13,6 +13,7 @@ import { buildRoomMemberTask, sha256Text } from './member-startup.js';
13
13
  import { agentDir } from '../paths.js';
14
14
  import { readProvenance } from '../creation.js';
15
15
  import { readTempSupervisor, secureStoppedTempArchive, tempArchiveForCreationAction, tempSupervisorLiveness, } from '../temp-lifecycle.js';
16
+ import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
16
17
  export function getBinPath() {
17
18
  try {
18
19
  return realpathSync(process.argv[1]);
@@ -30,11 +31,30 @@ function canonicalJson(value) {
30
31
  .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
31
32
  return JSON.stringify(value);
32
33
  }
34
+ function selectionSummary(selection) {
35
+ if ('ref' in selection)
36
+ return `ref:${selection.ref} (reference)`;
37
+ return `inline:sha256:${createHash('sha256').update(canonicalJson(selection.inline)).digest('hex').slice(0, 16)} (inline)`;
38
+ }
39
+ function permissionSummary(definition) {
40
+ const permissions = definition.permissions;
41
+ if (!permissions)
42
+ return undefined;
43
+ return `approval=${permissions.approval},filesystem=${permissions.filesystem},unattended=${permissions.unattended}`;
44
+ }
33
45
  function launchDefinition(definition) {
34
- const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : { kind: 'inline' };
46
+ const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : {
47
+ kind: 'inline', fingerprint: `sha256:${createHash('sha256')
48
+ .update(canonicalJson(selection.inline)).digest('hex').slice(0, 16)}`,
49
+ };
35
50
  return {
36
51
  projection: {
37
52
  brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
53
+ ...(definition.permissions ? { permissions: {
54
+ approval: definition.permissions.approval,
55
+ filesystem: definition.permissions.filesystem,
56
+ unattended: definition.permissions.unattended,
57
+ } } : {}),
38
58
  operationalFields: Object.keys(definition)
39
59
  .filter(key => key !== 'brain' && key !== 'role').sort(),
40
60
  },
@@ -260,6 +280,11 @@ async function launchMember(input) {
260
280
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
261
281
  launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
262
282
  } });
283
+ recordFleetAuditPresentation({ kind: 'agent_started', id: member.name, name: member.name,
284
+ lifetime: 'temporary', brain: selectionSummary(settings.definition.brain),
285
+ role: selectionSummary(settings.definition.role), harness: 'resolved', session: 'acp',
286
+ permissions: permissionSummary(settings.definition), parent: provision.roomId,
287
+ actionId: launched.creationActionId, inherited: [] });
263
288
  }
264
289
  catch (error) {
265
290
  updateMemberStartup(provision.roomId, member.name, { launch: {
@@ -269,6 +294,8 @@ async function launchMember(input) {
269
294
  updated_at: new Date().toISOString(),
270
295
  error: error instanceof Error ? error.message : String(error),
271
296
  } });
297
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Agent', id: member.name,
298
+ state: 'failed', category: 'readiness_failed', eventId: effectiveActionId });
272
299
  throw error;
273
300
  }
274
301
  }
@@ -1,5 +1,4 @@
1
1
  import type { TemplateDefinition, TemplateSnapshot } from './types.js';
2
- export declare const BUILTIN_TEMPLATES: readonly TemplateDefinition[];
3
2
  export declare function hashTemplate(t: TemplateDefinition): string;
4
3
  export declare function snapshotTemplate(t: TemplateDefinition): TemplateSnapshot;
5
4
  export declare function resolveTemplate(name: string, customTemplates: Record<string, TemplateDefinition>): TemplateDefinition | undefined;
@@ -1,54 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- const TEAM = {
3
- name: 'team',
4
- version: 1,
5
- builtin: true,
6
- description: 'Phased task pipeline: Architect specifies, Developer implements, Tester verifies',
7
- room: { quiet_membership: false, anonymous: false },
8
- contract: [
9
- 'Architect produces a spec and posts it to the room. Work pauses for owner approval.',
10
- 'Developer implements per approved spec.',
11
- 'Tester verifies spec conformance and test coverage.',
12
- 'Completion requires tester sign-off.',
13
- ].join('\n'),
14
- members: [
15
- { slot: 'architect', role: 'Architect', count: 1, agent: { ref: 'Architect' } },
16
- { slot: 'developer', role: 'Developer', count: 1, agent: { ref: 'Developer' } },
17
- { slot: 'tester', role: 'Tester', count: 1, agent: { ref: 'Tester' } },
18
- ],
19
- };
20
- const PAIR = {
21
- name: 'pair',
22
- version: 1,
23
- builtin: true,
24
- description: 'Deliberation pair: Secretary writes code, Critic reviews — every decision deliberated together',
25
- room: { quiet_membership: false, anonymous: false },
26
- contract: [
27
- 'Secretary and Critic deliberate every decision together before acting.',
28
- 'Secretary writes code; Critic reviews and challenges.',
29
- 'No material change lands without both agreeing.',
30
- 'Completion requires joint sign-off.',
31
- ].join('\n'),
32
- members: [
33
- { slot: 'secretary', role: 'Secretary', count: 1, agent: { ref: 'Secretary' } },
34
- { slot: 'critic', role: 'Critic', count: 1, agent: { ref: 'Critic' } },
35
- ],
36
- };
37
- const SINGLE = {
38
- name: 'single',
39
- version: 1,
40
- builtin: true,
41
- description: 'Solo agent: one general-purpose agent works the task with owner oversight',
42
- room: { quiet_membership: false, anonymous: false },
43
- contract: [
44
- 'Agent works the task autonomously, consulting the owner when blocked.',
45
- 'Owner reviews and approves completion.',
46
- ].join('\n'),
47
- members: [
48
- { slot: 'agent', role: 'Agent', count: 1, agent: { ref: 'Agent' } },
49
- ],
50
- };
51
- export const BUILTIN_TEMPLATES = [TEAM, PAIR, SINGLE];
52
2
  export function hashTemplate(t) {
53
3
  const canonical = JSON.stringify({
54
4
  name: t.name, version: t.version, description: t.description,
@@ -57,38 +7,21 @@ export function hashTemplate(t) {
57
7
  return createHash('sha256').update(canonical).digest('hex');
58
8
  }
59
9
  export function snapshotTemplate(t) {
60
- return { ...t, content_hash: hashTemplate(t) };
10
+ const { sourceFile: _, ...semantic } = t;
11
+ return { ...semantic, content_hash: hashTemplate(t) };
61
12
  }
62
13
  export function resolveTemplate(name, customTemplates) {
63
14
  const atIdx = name.lastIndexOf('@');
64
15
  const baseName = atIdx > 0 ? name.slice(0, atIdx) : name;
65
16
  const versionStr = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
66
17
  const version = versionStr ? parseInt(versionStr, 10) : undefined;
67
- const custom = customTemplates[baseName];
68
- if (custom) {
69
- if (version !== undefined && custom.version !== version)
70
- return undefined;
71
- return custom;
72
- }
73
- const builtin = BUILTIN_TEMPLATES.find(b => b.name === baseName);
74
- if (builtin) {
75
- if (version !== undefined && builtin.version !== version)
76
- return undefined;
77
- return builtin;
78
- }
79
- return undefined;
18
+ const template = customTemplates[baseName];
19
+ if (!template || (version !== undefined && template.version !== version))
20
+ return undefined;
21
+ return template;
80
22
  }
81
23
  export function listTemplates(customTemplates) {
82
- const result = [];
83
- const overridden = new Set();
84
- for (const [name, t] of Object.entries(customTemplates)) {
85
- result.push({ ...t, name });
86
- if (BUILTIN_TEMPLATES.some(b => b.name === name))
87
- overridden.add(name);
88
- }
89
- for (const b of BUILTIN_TEMPLATES) {
90
- if (!overridden.has(b.name))
91
- result.push(b);
92
- }
93
- return result.sort((a, b) => a.name.localeCompare(b.name));
24
+ return Object.entries(customTemplates)
25
+ .map(([name, template]) => ({ ...template, name }))
26
+ .sort((a, b) => a.name.localeCompare(b.name));
94
27
  }
@@ -205,7 +205,8 @@ export interface TemplateDefinition {
205
205
  name: string;
206
206
  version: number;
207
207
  description: string;
208
- builtin?: boolean;
208
+ /** Authoring provenance only; excluded from snapshots and semantic hashes. */
209
+ sourceFile?: string;
209
210
  room?: TemplateRoomConfig;
210
211
  contract?: string;
211
212
  members: TemplateMemberSlot[];
package/dist/runner.js CHANGED
@@ -621,7 +621,19 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
621
621
  throw error;
622
622
  }
623
623
  control.setFleetSpawner(async (requested) => {
624
- return executeManagedSpawn(role, configPath, requested, deps.log);
624
+ const event = await executeManagedSpawn(role, configPath, requested, deps.log);
625
+ // Room-member launches are collected with their Task/Room transaction so
626
+ // causal delivery is Task → Room → Agents → Room active → Task active.
627
+ if (!requested.roomMemberStartup && role.owner_channel && ownerChannel?.notifyFleetSpawn) {
628
+ try {
629
+ await ownerChannel.notifyFleetSpawn(event);
630
+ }
631
+ catch (error) {
632
+ deps.log(`[${name}] Agent lifecycle notice delivery failed: `
633
+ + `${error?.message ?? String(error)}`);
634
+ }
635
+ }
636
+ return event;
625
637
  });
626
638
  resolvedMonitorDeps.delivery = {
627
639
  // A wake is only delivered when its turn TERMINATES successfully. A
@@ -39,7 +39,7 @@ export interface ControlRequest {
39
39
  exitCode?: number;
40
40
  effect?: 'not_started' | 'completed' | 'unknown';
41
41
  resourceIds?: Record<string, string>;
42
- presentation?: FleetAuditPresentation;
42
+ presentations?: FleetAuditPresentation[];
43
43
  };
44
44
  }
45
45
  export interface ControlResponse {
@@ -341,7 +341,7 @@ export class RoleControlServer {
341
341
  correlationId: audit.correlationId, class: audit.class, effect: audit.effect,
342
342
  ...(audit.exitCode === undefined ? {} : { exitCode: audit.exitCode }),
343
343
  ...(audit.resourceIds ? { resourceIds: audit.resourceIds } : {}),
344
- ...(audit.presentation ? { presentation: audit.presentation } : {}),
344
+ ...(audit.presentations ? { presentations: audit.presentations } : {}),
345
345
  });
346
346
  this.write(socket, { version: 1, id: request.id, ok: true, result });
347
347
  return;
@@ -12,7 +12,7 @@ import { isSensitiveConfigKey } from '../sensitive-config.js';
12
12
  export const REDACTED_ENV_VALUE = '__OURS_FLEET_SECRET_REDACTED__';
13
13
  const MAX_CONFIG_BYTES = 256 * 1024;
14
14
  const DIFF_CONTEXT_LINES = 3;
15
- const KINDS = ['agents', 'roles', 'brains'];
15
+ const KINDS = ['agents', 'roles', 'brains', 'room_templates'];
16
16
  const emptyReport = () => ({ ok: true, checks: [] });
17
17
  export class FleetConfigService {
18
18
  path;
@@ -176,7 +176,8 @@ function readSources(manifest) {
176
176
  }
177
177
  trustedDirectory(dir);
178
178
  for (const name of readdirSync(dir).sort()) {
179
- if (kind === 'agents' && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
179
+ if ((kind === 'agents' || kind === 'room_templates')
180
+ && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
180
181
  continue;
181
182
  documents.set(join(kind, name), trustedFile(join(dir, name)));
182
183
  }
@@ -222,7 +223,7 @@ function renderProposal(current, model) {
222
223
  ? render(current.documents.get(oldRel), doc) : stringify(doc));
223
224
  }
224
225
  for (const [rel, source] of current.documents)
225
- if (rel.startsWith('roles/') || rel.startsWith('brains/'))
226
+ if (rel.startsWith('roles/') || rel.startsWith('brains/') || rel.startsWith('room_templates/'))
226
227
  next.set(rel, source);
227
228
  return next;
228
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.1.0-nightly.7",
3
+ "version": "1.1.0-nightly.9",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -16,6 +16,7 @@
16
16
  "dist",
17
17
  "contrib",
18
18
  "examples",
19
+ "presets",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],
@@ -0,0 +1,3 @@
1
+ role: { ref: Agent }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Architect }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Critic }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Developer }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Secretary }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }