@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.
- package/README.md +23 -4
- package/dist/application/role-creation-service.js +1 -1
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/application/task-room-service.js +147 -14
- package/dist/briefing.js +6 -6
- package/dist/build-info.json +4 -4
- package/dist/cli.js +8 -11
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +49 -2
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +39 -18
- package/dist/fleet-command-audit.d.ts +37 -8
- package/dist/fleet-command-audit.js +120 -62
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/owner-channel/channel.d.ts +2 -2
- package/dist/owner-channel/channel.js +31 -38
- package/dist/owner-channel/commands.js +3 -9
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/rooms-tasks/cli.js +121 -42
- package/dist/rooms-tasks/config.js +0 -4
- package/dist/rooms-tasks/provision.js +28 -1
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +2 -1
- package/dist/runner.js +13 -1
- package/dist/session/control.d.ts +1 -1
- package/dist/session/control.js +1 -1
- package/dist/web/fleet-config-service.js +4 -3
- package/package.json +2 -1
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +5 -0
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -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
|
|
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
|
|
219
|
-
const
|
|
220
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
236
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1291
|
-
auditRoom('delete',
|
|
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 } : {
|
|
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
|
-
|
|
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
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
presentations?: FleetAuditPresentation[];
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
export interface ControlResponse {
|
package/dist/session/control.js
CHANGED
|
@@ -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.
|
|
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'
|
|
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.
|
|
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
|
],
|