@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
@@ -13,15 +13,35 @@ export class FleetCliExit extends Error {
13
13
  this.effect = effect;
14
14
  }
15
15
  }
16
+ /** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
17
+ export const FLEET_LIFECYCLE_EVENT_KINDS = Object.freeze([
18
+ 'agent_started', 'task', 'room', 'lifecycle_failure',
19
+ ]);
20
+ /** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
21
+ export function lifecycleEventDigestBasis(value) {
22
+ const resourceKind = value.kind === 'lifecycle_failure' ? value.resource : value.kind;
23
+ return `${resourceKind}\0${value.id}\0${value.eventId}`;
24
+ }
16
25
  let collection;
17
- export function beginFleetAuditCollection() { collection = { resourceIds: {} }; }
26
+ export function beginFleetAuditCollection() { collection = { resourceIds: {}, presentations: [] }; }
18
27
  export function recordFleetAuditResource(kind, id) {
19
28
  if (id && collection)
20
29
  collection.resourceIds[kind] = id;
21
30
  }
22
31
  export function recordFleetAuditPresentation(value) {
23
- if (collection)
24
- collection.presentation = structuredClone(value);
32
+ if (collection && FLEET_LIFECYCLE_EVENT_KINDS.includes(value.kind)) {
33
+ const basis = value.eventId ?? (value.kind === 'agent_started' ? value.actionId
34
+ : value.kind === 'task' || value.kind === 'room'
35
+ ? `${value.operation}:${value.revision ?? `${value.previousState ?? 'none'}:${value.newState}`}`
36
+ : `${value.category}:${value.state}`);
37
+ collection.presentations.push(structuredClone({ ...value, eventId: basis }));
38
+ if (value.kind === 'task')
39
+ collection.resourceIds.task = value.id;
40
+ if (value.kind === 'room')
41
+ collection.resourceIds.room = value.id;
42
+ if (value.kind === 'agent_started')
43
+ collection.resourceIds.agent = value.id;
44
+ }
25
45
  }
26
46
  export function recordFleetAuditFailure(failure) { if (collection)
27
47
  collection.failure = failure; }
@@ -30,7 +50,7 @@ export function consumeFleetAuditCollection() {
30
50
  collection = undefined;
31
51
  return {
32
52
  ...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
33
- ...(current?.presentation ? { presentation: current.presentation } : {}),
53
+ ...(current?.presentations.length ? { presentations: current.presentations } : {}),
34
54
  ...(current?.failure ? { failure: current.failure } : {}),
35
55
  };
36
56
  }
@@ -43,12 +63,11 @@ const AGENT_SURFACES = {
43
63
  room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close', 'recover']),
44
64
  };
45
65
  export const fleetProxyCommandInventory = Object.freeze(Object.fromEntries(Object.entries(AGENT_SURFACES).map(([surface, commands]) => [surface, [...commands]])));
46
- const DENIED = new Set([
47
- 'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
48
- 'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
49
- ]);
50
66
  export const fleetProxyTopLevelInventory = Object.freeze({
51
- safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), denied: [...DENIED],
67
+ safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), public: [
68
+ 'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
69
+ 'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
70
+ ], denied: [],
52
71
  hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
53
72
  });
54
73
  const globalValueOptions = new Set(['-c', '--configuration']);
@@ -92,13 +111,14 @@ export function classifyFleetArgv(argv) {
92
111
  if (sub.startsWith('_'))
93
112
  return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
94
113
  if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
95
- return { command: `${command} ${sub}`, route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
114
+ return { command: `${command} ${sub}`, route: `supervisor-proxy/${command}`, decision: 'allow' };
96
115
  return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
97
116
  route: `supervisor-proxy/${command}`, decision: 'allow' };
98
117
  }
99
- if (DENIED.has(command))
100
- return { command, route: 'supervisor-proxy/permission-denied', decision: 'deny' };
101
- return { command, route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
118
+ // The proxy is attribution and routing, not a product permission boundary.
119
+ // Commander validates every public command. Hidden process entry points above
120
+ // remain guarded because recursively invoking them breaks the supervisor protocol.
121
+ return { command, route: 'supervisor-proxy/public', decision: 'allow' };
102
122
  }
103
123
  const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
104
124
  const sensitiveValueFlags = new Set([
@@ -251,7 +271,7 @@ export class FleetCommandAuditStore {
251
271
  const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
252
272
  ...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
253
273
  ...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
254
- ...(attempt.outcome.presentation ? { presentation: attempt.outcome.presentation } : {}) };
274
+ ...(attempt.outcome.presentations ? { presentations: attempt.outcome.presentations } : {}) };
255
275
  if (JSON.stringify(prior) !== JSON.stringify(outcome))
256
276
  throw new Error('conflicting fleet command outcome for existing correlation');
257
277
  }
@@ -293,7 +313,7 @@ export function validateFleetAuditBegin(value) {
293
313
  }
294
314
  export function validateFleetAuditFinish(value) {
295
315
  const input = value;
296
- const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentation'];
316
+ const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentations'];
297
317
  const resources = input?.resourceIds;
298
318
  if (!input || Object.keys(input).some(key => !allowed.includes(key))
299
319
  || typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
@@ -302,7 +322,8 @@ export function validateFleetAuditFinish(value) {
302
322
  || (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
303
323
  || Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
304
324
  || typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
305
- || (input.presentation !== undefined && !validPresentation(input.presentation)))
325
+ || (input.presentations !== undefined && (!Array.isArray(input.presentations)
326
+ || input.presentations.length > 128 || !input.presentations.every(validPresentation))))
306
327
  throw new Error('invalid fleet audit finish fields');
307
328
  }
308
329
  function validPresentation(value) {
@@ -312,64 +333,101 @@ function validPresentation(value) {
312
333
  const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
313
334
  const text = (v) => typeof v === 'string' && v.length <= 256
314
335
  && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
336
+ const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
337
+ const transition = (allowed) => typeof p.operation === 'string' && typeof p.newState === 'string'
338
+ && allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
315
339
  if (p.kind === 'agent_started')
316
- return safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
340
+ return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
341
+ 'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited'])
342
+ && safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
317
343
  && text(p.brain) && text(p.role) && safe(p.harness) && p.session === 'acp' && safe(p.parent) && safe(p.actionId)
318
344
  && (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
319
345
  && Array.isArray(p.inherited) && p.inherited.every(text);
320
346
  if (p.kind === 'task')
321
- return safe(p.operation) && safe(p.id) && safe(p.newState)
347
+ return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
348
+ 'newState', 'template', 'roomId', 'revision', 'list', 'agents'])
349
+ && ['create', 'start', 'work', 'block', 'unblock', 'review', 'done',
350
+ 'cancel', 'finish', 'delete', 'settling'].includes(String(p.operation)) && safe(p.eventId) && safe(p.id) && safe(p.newState)
351
+ && transition({ create: ['none→backlog', 'none→provisioning', 'none→active'],
352
+ start: ['backlog→provisioning'], work: ['provisioning→active'],
353
+ block: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
354
+ unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
355
+ review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
356
+ cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
357
+ delete: ['done→deleted'], settling: ['active→active', 'review→review', 'backlog→backlog',
358
+ 'provisioning→provisioning'] })
322
359
  && (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
323
360
  && (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
361
+ && (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
324
362
  && Array.isArray(p.agents) && p.agents.length <= 64
325
- && p.agents.every(a => a && typeof a === 'object' && safe(a.name)
363
+ && p.agents.every(a => a && typeof a === 'object'
364
+ && exact(a, ['name', 'brain', 'role', 'permissions'])
365
+ && safe(a.name)
326
366
  && text(a.role)
327
- && (a.brain === undefined || text(a.brain)));
367
+ && (a.brain === undefined || text(a.brain))
368
+ && (a.permissions === undefined || text(a.permissions)));
328
369
  if (p.kind === 'room')
329
- return safe(p.operation) && safe(p.id) && safe(p.newState)
370
+ return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
371
+ 'revision', 'name', 'template', 'taskId', 'participants'])
372
+ && ['create', 'activate', 'close', 'delete'].includes(String(p.operation))
373
+ && safe(p.eventId) && safe(p.id) && safe(p.newState)
374
+ && transition({ create: ['none→provisioning'], activate: ['provisioning→active'],
375
+ close: ['active→closing', 'provisioning→closing'],
376
+ delete: ['closing→deleted'] })
330
377
  && (p.previousState === undefined || safe(p.previousState))
331
- && (p.template === undefined || text(p.template))
378
+ && (p.name === undefined || text(p.name)) && (p.template === undefined || text(p.template))
379
+ && (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
332
380
  && Array.isArray(p.participants) && p.participants.length <= 64
333
- && p.participants.every(a => a && typeof a === 'object' && safe(a.name)
334
- && text(a.role));
381
+ && p.participants.every(a => a && typeof a === 'object'
382
+ && exact(a, ['name', 'id', 'brain', 'role', 'permissions'])
383
+ && safe(a.name)
384
+ && text(a.role)
385
+ && (a.id === undefined || safe(a.id))
386
+ && (a.brain === undefined || text(a.brain))
387
+ && (a.permissions === undefined || text(a.permissions)));
388
+ if (p.kind === 'lifecycle_failure')
389
+ return exact(p, ['kind', 'eventId', 'resource', 'id', 'state', 'category'])
390
+ && ['Agent', 'Task', 'Room'].includes(String(p.resource))
391
+ && safe(p.eventId) && safe(p.id) && safe(p.state) && ['provision_failed', 'provision_pending', 'readiness_failed', 'settlement_failed',
392
+ 'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
335
393
  return false;
336
394
  }
337
- export function renderFleetAuditInvocation(attempt) {
338
- return ['🧾 Fleet command invoked', `Correlation: ${attempt.correlationId}`, `Agent: ${attempt.caller}`,
339
- `Route: ${attempt.classification.route}`, `Fleet action: ${attempt.classification.command}`,
340
- `Decision: ${attempt.classification.decision}`, `Invoked: ${attempt.invokedAt}`,
341
- `Raw argv (redacted): ${JSON.stringify(attempt.argv)}`].join('\n');
342
- }
343
- export function renderFleetAuditOutcome(attempt) {
344
- if (!attempt.outcome)
345
- throw new Error('fleet command audit outcome is absent');
346
- return ['🧾 Fleet command outcome', `Correlation: ${attempt.correlationId}`, `Agent: ${attempt.caller}`,
347
- `Route: ${attempt.classification.route}`, `Fleet action: ${attempt.classification.command}`,
348
- `Result: ${attempt.outcome.class}`, `Effect: ${attempt.outcome.effect}`,
349
- ...(attempt.outcome.exitCode === undefined ? [] : [`Exit: ${attempt.outcome.exitCode}`]),
350
- ...Object.entries(attempt.outcome.resourceIds ?? {}).sort(([a], [b]) => a.localeCompare(b))
351
- .map(([kind, id]) => `${kind[0]?.toUpperCase()}${kind.slice(1)} ID: ${id}`),
352
- `Completed: ${attempt.outcome.completedAt}`, `Raw argv (redacted): ${JSON.stringify(attempt.argv)}`,
353
- ...renderPresentation(attempt.outcome.presentation)].join('\n');
354
- }
355
- function renderPresentation(value) {
356
- if (!value)
357
- return [];
358
- if (value.kind === 'agent_started')
359
- return ['Structured result: Agent started', `Agent: ${value.name}`,
360
- `Brain: ${value.brain}`, `Role: ${value.role}`,
361
- `Runtime: ${value.harness}/${value.session}${value.model ? ` model=${value.model}` : ''}`,
362
- ...(value.permissions ? [`Permissions: ${value.permissions}`] : []), `Parent: ${value.parent}`,
363
- `Agent action ID: ${value.actionId}`,
364
- `Inheritance: ${value.inherited.length ? value.inherited.join(', ') : 'none'}`];
365
- if (value.kind === 'task')
366
- return ['Structured result: Task', `Operation: ${value.operation}`,
367
- `Task: ${value.title ? `${value.title} (` : ''}${value.id}${value.title ? ')' : ''}`,
368
- `Status: ${value.previousState ?? 'unresolved'} -> ${value.newState}`,
369
- ...(value.template ? [`Template: ${value.template}`] : []), ...(value.roomId ? [`Room: ${value.roomId}`] : []),
370
- `Responsible Agents: ${value.agents.length ? value.agents.map(a => `${a.name} [Brain ${a.brain ?? 'unresolved'}; Role ${a.role}]`).join('; ') : 'none'}`];
371
- return ['Structured result: Room', `Operation: ${value.operation}`, `Room: ${value.id}`,
372
- `Status: ${value.previousState ?? 'unresolved'} -> ${value.newState}`,
373
- ...(value.template ? [`Template: ${value.template}`] : []),
374
- `Participants: ${value.participants.length ? value.participants.map(a => `${a.name} [Role ${a.role}]`).join('; ') : 'none'}`];
395
+ /** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
396
+ export function renderFleetLifecycleEvent(value) {
397
+ if (value.kind === 'lifecycle_failure') {
398
+ const actions = {
399
+ provision_failed: 'Inspect Fleet service logs, correct configuration, then recover the resource.',
400
+ provision_pending: 'Run Task or Room recover after checking member readiness.',
401
+ readiness_failed: 'Inspect the Agent log, correct its configuration, then retry creation.',
402
+ settlement_failed: 'Inspect Fleet service logs, then run Task recover.',
403
+ settlement_pending: 'Run Task recover to continue settlement.',
404
+ cleanup_failed: 'Inspect Fleet service logs, then run Room recover.',
405
+ cleanup_pending: 'Run Room recover to continue cleanup.',
406
+ };
407
+ const lifecycle = value.category.endsWith('_pending') ? 'lifecycle pending' : 'lifecycle failure';
408
+ return `⚠️ ${value.resource} ${value.id} ${lifecycle} (${value.category}); `
409
+ + `state ${value.state}. Action: ${actions[value.category]}`;
410
+ }
411
+ if (value.kind === 'agent_started') {
412
+ const permission = value.permissions ? `; permissions ${value.permissions}` : '';
413
+ return `🧑‍💻 ${value.parent} spawned ${value.lifetime} Agent ${value.name} (${value.id}) — ready. `
414
+ + `Brain ${value.brain ?? 'unresolved'}; Role ${value.role ?? 'unresolved'}${permission}.`;
415
+ }
416
+ if (value.kind === 'task') {
417
+ const title = value.title ? ` “${value.title}”` : '';
418
+ const context = [value.list ? `List ${value.list}` : undefined,
419
+ value.template ? `template ${value.template}` : undefined,
420
+ value.roomId ? `Room ${value.roomId}` : undefined].filter(Boolean).join('; ');
421
+ const agents = value.agents.length ? ` Agents: ${value.agents.map(agent => `${agent.name} [Brain ${agent.brain ?? 'unresolved'}; Role ${agent.role}`
422
+ + `${agent.permissions ? `; permissions ${agent.permissions}` : ''}]`).join('; ')}.` : '';
423
+ return `📋 Task${title} (${value.id}) ${value.operation}: `
424
+ + `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${agents}`;
425
+ }
426
+ const context = [value.template ? `template ${value.template}` : undefined,
427
+ value.taskId ? `Task ${value.taskId}` : undefined].filter(Boolean).join('; ');
428
+ const participants = value.participants.length ? ` Participants: ${value.participants.map(participant => `${participant.name}${participant.id ? ` (${participant.id})` : ''} [Role ${participant.role}`
429
+ + `${participant.brain ? `; Brain ${participant.brain}` : ''}`
430
+ + `${participant.permissions ? `; permissions ${participant.permissions}` : ''}]`).join('; ')}.` : '';
431
+ return `🏠 Room${value.name ? ` “${value.name}”` : ''} (${value.id}) ${value.operation}: `
432
+ + `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${participants}`;
375
433
  }
@@ -1 +1 @@
1
- export declare const INIT_COMPLETION_GUIDANCE = "\nNext:\n cp \"$(npm root -g)/@ours.network/fleet/examples/fleet.yaml\" ~/fleet.yaml\n cp -R \"$(npm root -g)/@ours.network/fleet/examples/fleet\" ~/fleet\n edit ~/fleet.yaml and ~/fleet/{agents,roles,brains}/*.yaml, then: ours-fleet up";
1
+ export declare const INIT_COMPLETION_GUIDANCE = "\nNext:\n ours-fleet config\n ours-fleet template list\n edit the seeded manifest and fleet/{agents,roles,brains,room_templates}/*.yaml\n configure rooms.owner, then create a task with --template single, pair, or team";
@@ -1,5 +1,6 @@
1
1
  export const INIT_COMPLETION_GUIDANCE = `
2
2
  Next:
3
- cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
4
- cp -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
5
- edit ~/fleet.yaml and ~/fleet/{agents,roles,brains}/*.yaml, then: ours-fleet up`;
3
+ ours-fleet config
4
+ ours-fleet template list
5
+ edit the seeded manifest and fleet/{agents,roles,brains,room_templates}/*.yaml
6
+ configure rooms.owner, then create a task with --template single, pair, or team`;
@@ -53,7 +53,7 @@ export interface OwnerChannelHandle {
53
53
  exitCode?: number;
54
54
  effect: 'not_started' | 'completed' | 'unknown';
55
55
  resourceIds?: Record<string, string>;
56
- presentation?: FleetAuditPresentation;
56
+ presentations?: FleetAuditPresentation[];
57
57
  }): Promise<FleetAuditAttempt>;
58
58
  }
59
59
  export declare const OWNER_RECOVERY_QUEUE_CAPACITY = 32;
@@ -223,7 +223,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
223
223
  exitCode?: number;
224
224
  effect: 'not_started' | 'completed' | 'unknown';
225
225
  resourceIds?: Record<string, string>;
226
- presentation?: FleetAuditPresentation;
226
+ presentations?: FleetAuditPresentation[];
227
227
  }): Promise<FleetAuditAttempt>;
228
228
  recover(epoch: string): Promise<void>;
229
229
  private recoveryStage;
@@ -15,7 +15,7 @@ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/t
15
15
  import { VERSION } from '../version.js';
16
16
  import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
17
17
  import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
18
- import { FleetCommandAuditStore, renderFleetAuditInvocation, renderFleetAuditOutcome, } from '../fleet-command-audit.js';
18
+ import { FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, } from '../fleet-command-audit.js';
19
19
  import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
20
20
  import { ownerNotices, } from './notices.js';
21
21
  import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
@@ -263,18 +263,14 @@ export class OwnerChannel {
263
263
  return this.queueManagement(async () => {
264
264
  if (!this.ready || this.stopping)
265
265
  throw new Error('owner-channel MCP client is unavailable');
266
- const model = event.model ? `, model ${event.model}` : '';
267
- const monitorPolicy = event.monitor.interrupt === true
268
- ? ' with interruption'
269
- : event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
270
- const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
271
266
  const permission = event.permissionMode
272
- ? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
273
- : '';
274
- const inherited = event.inherited.length
275
- ? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
276
- await this.sendProactiveMessage(`🧑‍💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
277
- + `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
267
+ ? `${event.permissionMode.fleetMode}/${event.permissionMode.nativeMode}` : undefined;
268
+ await this.sendProactiveMessage(renderFleetLifecycleEvent({ kind: 'agent_started', eventId: event.creationActionId,
269
+ id: event.role, name: event.role,
270
+ lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
271
+ harness: event.harness, session: event.session, model: event.model,
272
+ permissions: permission, parent: event.caller, actionId: event.creationActionId,
273
+ inherited: event.inherited }), `fleet-spawn\0${event.creationActionId}`, 0);
278
274
  });
279
275
  }
280
276
  beginFleetCommandAudit(requestId, argv) {
@@ -283,20 +279,9 @@ export class OwnerChannel {
283
279
  if (attempt.invocation === 'delivered')
284
280
  return attempt;
285
281
  if (attempt.invocation === 'uncertain')
286
- throw new Error('fleet command invocation delivery is uncertain; execution is denied');
287
- if (!this.ready || this.stopping) {
288
- this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
289
- this.options.log(`[${this.options.role}] fleet command invocation ${attempt.correlationId} `
290
- + 'delivery unavailable; execution denied');
291
- throw new Error('owner-channel MCP client is unavailable; invocation recorded uncertain');
292
- }
293
- try {
294
- await this.sendProactiveMessage(renderFleetAuditInvocation(attempt), `fleet-command-invocation\0${attempt.correlationId}`, 0);
295
- }
296
- catch (error) {
297
- this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
298
- throw error;
299
- }
282
+ return attempt;
283
+ this.options.log(`[${this.options.role}] fleet proxy command ${attempt.correlationId} `
284
+ + `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
300
285
  attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
301
286
  return attempt;
302
287
  });
@@ -309,26 +294,34 @@ export class OwnerChannel {
309
294
  class: input.class, effect: input.effect,
310
295
  ...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
311
296
  ...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
312
- ...(input.presentation ? { presentation: input.presentation } : {}),
297
+ ...(input.presentations ? { presentations: input.presentations } : {}),
313
298
  });
314
299
  if (attempt.outcome?.delivery === 'delivered')
315
300
  return attempt;
316
301
  if (attempt.outcome?.delivery === 'uncertain')
317
- throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome.effect}`);
318
- if (!this.ready || this.stopping) {
319
- this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
320
- this.options.log(`[${this.options.role}] fleet command outcome ${attempt.correlationId} `
321
- + `delivery unavailable after execution; effect status=${attempt.outcome?.effect}`);
322
- throw new Error(`owner-channel unavailable after execution; effect status=${attempt.outcome?.effect}`);
323
- }
302
+ return attempt;
303
+ let uncertain = false;
324
304
  try {
325
- await this.sendProactiveMessage(renderFleetAuditOutcome(attempt), `fleet-command-outcome\0${attempt.correlationId}`, 0);
305
+ for (const event of attempt.outcome?.presentations ?? []) {
306
+ try {
307
+ await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
308
+ }
309
+ catch (error) {
310
+ if (error instanceof DuplicateSendError) {
311
+ if (error.status !== 'delivered')
312
+ uncertain = true;
313
+ this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
314
+ continue;
315
+ }
316
+ throw error;
317
+ }
318
+ }
326
319
  }
327
320
  catch (error) {
328
- this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
329
- throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome?.effect}`, { cause: error });
321
+ uncertain = true;
322
+ this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
330
323
  }
331
- attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
324
+ attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
332
325
  return attempt;
333
326
  });
334
327
  this.managementTail = run.then(() => undefined, () => undefined);
@@ -598,10 +598,7 @@ export const ownerCommands = [
598
598
  const templates = ctx.listTemplateQueries();
599
599
  if (!templates.length)
600
600
  return ctx.reply('📐 No templates.');
601
- const lines = templates.map(t => {
602
- const tag = t.builtin ? ' (built-in)' : '';
603
- return `${t.name}@${t.version}${tag} ${t.description}`;
604
- });
601
+ const lines = templates.map(t => `${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
605
602
  await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
606
603
  }),
607
604
  },
@@ -627,7 +624,7 @@ export const ownerCommands = [
627
624
  const lines = [
628
625
  `📐 Template: ${t.name}@${t.version}`,
629
626
  `Description: ${t.description}`,
630
- ...(t.builtin ? ['Source: built-in'] : []),
627
+ `Source: ${t.sourceFile ?? 'manifest'}`,
631
628
  ...(t.contract ? [`Contract: ${t.contract}`] : []),
632
629
  'Members:',
633
630
  ...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} → `
@@ -646,10 +643,7 @@ export const ownerCommands = [
646
643
  const templates = ctx.listTemplateQueries();
647
644
  if (!templates.length)
648
645
  return ctx.reply('📐 No templates.');
649
- const lines = templates.map(t => {
650
- const tag = t.builtin ? ' (built-in)' : '';
651
- return `${t.name}@${t.version}${tag} ${t.description}`;
652
- });
646
+ const lines = templates.map(t => `${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
653
647
  await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
654
648
  break;
655
649
  }
@@ -1,5 +1,7 @@
1
1
  /** A send whose (dedupe-scoped) digest was already recorded: it must not repeat. */
2
2
  export declare class DuplicateSendError extends Error {
3
+ readonly status: OwnerProactiveSend['status'];
4
+ constructor(status: OwnerProactiveSend['status']);
3
5
  }
4
6
  /** Durable bounded dedupe containing wire IDs only — never message or reply plaintext. */
5
7
  export declare class OwnerChannelState {
@@ -5,6 +5,11 @@ import { replaceFileAtomically } from '../atomic-file.js';
5
5
  import { canonicalCid } from '../config.js';
6
6
  /** A send whose (dedupe-scoped) digest was already recorded: it must not repeat. */
7
7
  export class DuplicateSendError extends Error {
8
+ status;
9
+ constructor(status) {
10
+ super('duplicate proactive owner message refused');
11
+ this.status = status;
12
+ }
8
13
  }
9
14
  /** Durable bounded dedupe containing wire IDs only — never message or reply plaintext. */
10
15
  export class OwnerChannelState {
@@ -184,8 +189,9 @@ export class OwnerConversationState {
184
189
  // 'all' scope serves wire-keyed idempotency: a crash replay must not
185
190
  // deliver the same wire to a second owner after the route moved.
186
191
  const scope = dedupe === 'all' ? this.sends : recent;
187
- if (scope.some(send => send.digest === digest))
188
- throw new DuplicateSendError('duplicate proactive owner message refused');
192
+ const duplicate = scope.find(send => send.digest === digest);
193
+ if (duplicate)
194
+ throw new DuplicateSendError(duplicate.status);
189
195
  const last = recent.at(-1);
190
196
  if (last && now - last.at < minIntervalMs)
191
197
  throw new Error(`proactive owner messages are rate-limited to one every ${minIntervalMs}ms`);
@@ -0,0 +1,10 @@
1
+ export interface BootstrapResult {
2
+ revision: number;
3
+ sourceRoot: string;
4
+ configPath: string;
5
+ created: string[];
6
+ preserved: string[];
7
+ }
8
+ export declare const packagedPresetRoot: () => string;
9
+ /** Seed packaged defaults without replacing a single existing byte. */
10
+ export declare function bootstrapPresets(configuration?: string): BootstrapResult;
@@ -0,0 +1,85 @@
1
+ import { constants, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, chmodSync, } from 'node:fs';
2
+ import { dirname, join, relative, resolve, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { defaultConfigPath } from './paths.js';
5
+ import { splitRootFor } from './config.js';
6
+ export const packagedPresetRoot = () => fileURLToPath(new URL('../presets', import.meta.url));
7
+ function assertSafeExisting(path, kind) {
8
+ const stat = lstatSync(path);
9
+ if (stat.isSymbolicLink())
10
+ throw new Error(`preset bootstrap refuses symlink target: ${path}`);
11
+ if (kind === 'file' ? !stat.isFile() : !stat.isDirectory())
12
+ throw new Error(`preset bootstrap expected ${kind}: ${path}`);
13
+ const uid = process.getuid?.();
14
+ if (uid !== undefined && stat.uid !== uid)
15
+ throw new Error(`preset bootstrap refuses target owned by uid ${stat.uid}: ${path}`);
16
+ if ((stat.mode & 0o022) !== 0)
17
+ throw new Error(`preset bootstrap refuses group/world-writable target: ${path}`);
18
+ }
19
+ function ensureDirectory(path) {
20
+ const parent = dirname(path);
21
+ if (parent !== path && !existsSync(parent))
22
+ ensureDirectory(parent);
23
+ if (existsSync(path)) {
24
+ assertSafeExisting(path, 'directory');
25
+ return;
26
+ }
27
+ mkdirSync(path, { mode: 0o700 });
28
+ }
29
+ function seedFile(source, target, result) {
30
+ ensureDirectory(dirname(target));
31
+ if (existsSync(target)) {
32
+ assertSafeExisting(target, 'file');
33
+ result.preserved.push(target);
34
+ return;
35
+ }
36
+ try {
37
+ copyFileSync(source, target, constants.COPYFILE_EXCL);
38
+ chmodSync(target, 0o600);
39
+ result.created.push(target);
40
+ }
41
+ catch (error) {
42
+ if (error.code !== 'EEXIST')
43
+ throw error;
44
+ assertSafeExisting(target, 'file');
45
+ result.preserved.push(target);
46
+ }
47
+ }
48
+ function presetFiles(root) {
49
+ const files = [];
50
+ const visit = (directory) => {
51
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
52
+ const path = join(directory, entry.name);
53
+ if (entry.isDirectory())
54
+ visit(path);
55
+ else if (entry.isFile())
56
+ files.push(path);
57
+ }
58
+ };
59
+ visit(root);
60
+ return files.sort();
61
+ }
62
+ /** Seed packaged defaults without replacing a single existing byte. */
63
+ export function bootstrapPresets(configuration = defaultConfigPath()) {
64
+ const sourceRoot = packagedPresetRoot();
65
+ const manifest = JSON.parse(readFileSync(join(sourceRoot, 'manifest.json'), 'utf8'));
66
+ if (manifest.schema_version !== 1 || !Number.isInteger(manifest.preset_revision))
67
+ throw new Error(`invalid packaged preset manifest: ${join(sourceRoot, 'manifest.json')}`);
68
+ const configPath = resolve(configuration);
69
+ const result = {
70
+ revision: manifest.preset_revision, sourceRoot, configPath, created: [], preserved: [],
71
+ };
72
+ ensureDirectory(dirname(configPath));
73
+ seedFile(join(sourceRoot, 'fleet.yaml'), configPath, result);
74
+ const sourceSplit = join(sourceRoot, 'fleet');
75
+ const targetSplit = splitRootFor(configPath);
76
+ if (existsSync(targetSplit))
77
+ assertSafeExisting(targetSplit, 'directory');
78
+ for (const source of presetFiles(sourceSplit)) {
79
+ const rel = relative(sourceSplit, source);
80
+ if (rel.startsWith('..') || rel.includes(`..${sep}`))
81
+ throw new Error(`invalid packaged preset path: ${source}`);
82
+ seedFile(source, join(targetSplit, rel), result);
83
+ }
84
+ return result;
85
+ }