@ours.network/fleet 1.1.0-nightly.24 → 1.1.0-nightly.26

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 (39) hide show
  1. package/README.md +36 -5
  2. package/dist/application/task-room-service.d.ts +30 -1
  3. package/dist/application/task-room-service.js +167 -104
  4. package/dist/briefing.js +15 -4
  5. package/dist/build-info.json +4 -4
  6. package/dist/cli.js +9 -1
  7. package/dist/docs.d.ts +1 -1
  8. package/dist/docs.js +34 -4
  9. package/dist/fleet-command-audit.d.ts +11 -0
  10. package/dist/fleet-command-audit.js +47 -3
  11. package/dist/init-wizard.js +1 -0
  12. package/dist/owner-channel/channel.d.ts +4 -0
  13. package/dist/owner-channel/channel.js +90 -8
  14. package/dist/owner-channel/commands.d.ts +7 -2
  15. package/dist/owner-channel/commands.js +36 -2
  16. package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
  17. package/dist/owner-channel/lifecycle-outbox.js +87 -0
  18. package/dist/preset-migration.d.ts +1 -1
  19. package/dist/preset-migration.js +23 -4
  20. package/dist/rooms-tasks/cli.js +139 -106
  21. package/dist/rooms-tasks/external-worker.d.ts +8 -0
  22. package/dist/rooms-tasks/external-worker.js +38 -0
  23. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  24. package/dist/rooms-tasks/member-startup.js +9 -2
  25. package/dist/rooms-tasks/provision.js +17 -27
  26. package/dist/runner.js +2 -0
  27. package/dist/session/control.d.ts +1 -1
  28. package/dist/session/control.js +14 -1
  29. package/package.json +1 -1
  30. package/presets/fleet/agent_templates/Critic.yaml +1 -0
  31. package/presets/fleet/agent_templates/Developer.yaml +1 -0
  32. package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -0
  33. package/presets/fleet/roles/Critic.yaml +10 -1
  34. package/presets/fleet/roles/Developer.yaml +10 -1
  35. package/presets/fleet/roles/LocalCoordinator.yaml +14 -4
  36. package/presets/fleet/room_templates/pair.yaml +3 -0
  37. package/presets/fleet/room_templates/single.yaml +3 -1
  38. package/presets/fleet/room_templates/team.yaml +3 -0
  39. package/presets/manifest.json +1 -1
@@ -15,10 +15,11 @@ import { ACP_CANCEL_DEADLINE_EXCEEDED, CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED
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, lifecycleEventDigestBasis, renderFleetLifecycleEvent, } from '../fleet-command-audit.js';
18
+ import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, setFleetAuditLifecycleCheckpoint, } 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';
22
+ import { FleetLifecycleOutbox } from './lifecycle-outbox.js';
22
23
  import { OwnerTaskState, ownerTaskAuditId, ownerTaskDigest, } from './tasks.js';
23
24
  import { AttachmentRecoveryState, admitAttachments, cleanupAttachmentRoot, parseIncomingAttachments, parseRetrievedAttachments, prepareAttachmentDirectory, recoveredAttachment, removeRequestDirectory, safeField, validateAttachmentSelection, validateAttachmentRelaySelection, writeRecoveredAttachment, } from './attachments.js';
24
25
  import { acquireOwnerBinderLease, OWNER_BIND_HANDOFF_TIMEOUT_MS, } from './binder.js';
@@ -112,6 +113,7 @@ export class OwnerChannel {
112
113
  fleetOps;
113
114
  prepareRestart;
114
115
  commandAudits;
116
+ lifecycleOutbox;
115
117
  constructor(options) {
116
118
  this.options = options;
117
119
  this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
@@ -128,6 +130,7 @@ export class OwnerChannel {
128
130
  });
129
131
  this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
130
132
  this.commandAudits = new FleetCommandAuditStore(join(options.stateDir, '.owner-channel-command-audit.json'));
133
+ this.lifecycleOutbox = new FleetLifecycleOutbox(join(options.stateDir, '.owner-channel-lifecycle-outbox.json'));
131
134
  this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
132
135
  this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
133
136
  this.tasks = new OwnerTaskState(join(options.stateDir, '.owner-channel-tasks.json'));
@@ -153,6 +156,8 @@ export class OwnerChannel {
153
156
  options.log(`[${options.role}] owner message recovery state corrupt; message intake disabled`);
154
157
  if (!this.attachmentRecovery.integrity())
155
158
  options.log(`[${options.role}] owner attachment recovery state corrupt; attachments disabled`);
159
+ if (!this.lifecycleOutbox.integrity().ok)
160
+ options.log(`[${options.role}] owner lifecycle outbox corrupt; lifecycle delivery disabled`);
156
161
  }
157
162
  async start() {
158
163
  this.stopping = false;
@@ -200,6 +205,7 @@ export class OwnerChannel {
200
205
  this.startedOnce = true;
201
206
  this.ready = true;
202
207
  await this.flushPendingFleetCommandAudits();
208
+ await this.flushPendingFleetLifecycle();
203
209
  // Do not make role startup wait for an old owner request to finish a turn.
204
210
  // watchLoop itself drains before every establishment, including this first
205
211
  // one, so there is no drain-to-tip race.
@@ -275,6 +281,37 @@ export class OwnerChannel {
275
281
  ...(event.configuration ? { configuration: event.configuration } : {}) }), `fleet-spawn\0${event.creationActionId}`, 0);
276
282
  });
277
283
  }
284
+ notifyFleetLifecycle(presentations) {
285
+ const run = this.managementTail.then(async () => {
286
+ // Acceptance is durable before delivery. A detached worker may exit
287
+ // while the Owner channel is reconnecting; startup/recovery flushes the
288
+ // pending event through the existing exactly-once send ledger.
289
+ this.lifecycleOutbox.enqueue(presentations);
290
+ if (this.ready && !this.stopping)
291
+ await this.flushPendingFleetLifecycle();
292
+ });
293
+ this.managementTail = run.then(() => undefined, () => undefined);
294
+ return run;
295
+ }
296
+ async flushPendingFleetLifecycle() {
297
+ for (const entry of this.lifecycleOutbox.pending()) {
298
+ try {
299
+ await this.sendProactiveMessage(renderFleetLifecycleEvent(entry.presentation), `fleet-lifecycle\0${lifecycleEventDigestBasis(entry.presentation)}`, 0);
300
+ this.lifecycleOutbox.finish(entry.digest, 'delivered');
301
+ }
302
+ catch (error) {
303
+ if (error instanceof DuplicateSendError) {
304
+ this.lifecycleOutbox.finish(entry.digest, error.status === 'delivered' ? 'delivered' : 'uncertain');
305
+ this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
306
+ continue;
307
+ }
308
+ // If the send crossed the uncertain boundary, the proactive ledger
309
+ // will refuse it on the next flush. A pre-send outage stays pending.
310
+ this.logError('pending Fleet lifecycle delivery deferred', error);
311
+ return;
312
+ }
313
+ }
314
+ }
278
315
  beginFleetCommandAudit(requestId, argv) {
279
316
  const run = this.managementTail.then(async () => {
280
317
  let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
@@ -396,6 +433,7 @@ export class OwnerChannel {
396
433
  if (superseded())
397
434
  throw new Error('owner recovery epoch superseded');
398
435
  this.ready = true;
436
+ await this.flushPendingFleetLifecycle();
399
437
  this.watchTask = this.watchLoop(watchGeneration);
400
438
  }
401
439
  catch (error) {
@@ -1235,6 +1273,19 @@ export class OwnerChannel {
1235
1273
  * agent itself can execute /force-restart, /model, or any other command.
1236
1274
  */
1237
1275
  async handleCommand(sender, text, wireId) {
1276
+ const provisioningCommand = async (run) => {
1277
+ beginFleetAuditCollection();
1278
+ setFleetAuditLifecycleCheckpoint(events => this.notifyFleetLifecycle(events));
1279
+ try {
1280
+ return await run();
1281
+ }
1282
+ finally {
1283
+ setFleetAuditLifecycleCheckpoint(undefined);
1284
+ const remaining = consumeFleetAuditCollection().presentations;
1285
+ if (remaining?.length)
1286
+ await this.notifyFleetLifecycle(remaining);
1287
+ }
1288
+ };
1238
1289
  const ctx = {
1239
1290
  authenticatedCid: sender.id,
1240
1291
  role: this.options.role,
@@ -1257,13 +1308,44 @@ export class OwnerChannel {
1257
1308
  recoverRoom: roomId => this.recoverRoomFromOwner(sender, roomId, wireId),
1258
1309
  terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
1259
1310
  recoverTask: taskId => this.recoverTaskFromOwner(sender, taskId, wireId),
1260
- createTask: input => new TaskRoomApplicationService(this.options.configPath).createTask({
1261
- ...input,
1262
- actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1311
+ createTask: input => provisioningCommand(async () => {
1312
+ const service = new TaskRoomApplicationService(this.options.configPath);
1313
+ const task = await service.createTask({
1314
+ ...input,
1315
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1316
+ });
1317
+ if (!input.backlog && !input.noRoom && task.room_id) {
1318
+ const outcome = service.taskProvisioningOutcome(task.task_id);
1319
+ if (outcome.kind === 'in_progress' && !outcome.next_action)
1320
+ await this.fleetOps.provisionTask(task.task_id);
1321
+ }
1322
+ return task;
1263
1323
  }),
1264
- startTask: taskId => new TaskRoomApplicationService(this.options.configPath).startTask({
1265
- actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1324
+ startTask: taskId => provisioningCommand(async () => {
1325
+ const service = new TaskRoomApplicationService(this.options.configPath);
1326
+ const task = await service.startTask({
1327
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1328
+ });
1329
+ // A zero-duration observation repairs a ready notice after a crash or
1330
+ // duplicate start without extending the start command's existing wait.
1331
+ const outcome = await service.awaitTaskProvisioning({
1332
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1333
+ taskId: task.task_id, waitMs: 0,
1334
+ });
1335
+ if (outcome.kind === 'in_progress' && !outcome.next_action)
1336
+ await this.fleetOps.provisionTask(task.task_id);
1337
+ return outcome;
1266
1338
  }),
1339
+ awaitTask: taskId => provisioningCommand(async () => {
1340
+ const service = new TaskRoomApplicationService(this.options.configPath);
1341
+ const initial = service.taskProvisioningOutcome(taskId);
1342
+ if (initial.kind === 'in_progress')
1343
+ await this.fleetOps.provisionTask(taskId);
1344
+ return service.awaitTaskProvisioning({
1345
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1346
+ });
1347
+ }),
1348
+ taskProvisioningOutcome: taskId => new TaskRoomApplicationService(this.options.configPath).taskProvisioningOutcome(taskId),
1267
1349
  listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
1268
1350
  groupedTasks: filter => new TaskRoomApplicationService(this.options.configPath).groupedTasks(filter),
1269
1351
  listTaskLists: () => new TaskRoomApplicationService(this.options.configPath).listTaskLists(),
@@ -1294,9 +1376,9 @@ export class OwnerChannel {
1294
1376
  getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
1295
1377
  listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
1296
1378
  getTemplateQuery: name => new TaskRoomApplicationService(this.options.configPath).getTemplate(name),
1297
- createRoom: input => new TaskRoomApplicationService(this.options.configPath).createRoom({
1379
+ createRoom: input => provisioningCommand(() => new TaskRoomApplicationService(this.options.configPath).createRoom({
1298
1380
  ...input, actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1299
- }),
1381
+ })),
1300
1382
  recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
1301
1383
  readWorklogTail: maxChars => this.readWorklogTail(maxChars),
1302
1384
  reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
@@ -1,7 +1,7 @@
1
1
  import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
2
2
  import type { RoomOrchestrationRecord, TaskOutcome, TaskRecord, TaskTerminalIntent, TaskListRecord } from '../rooms-tasks/types.js';
3
3
  import { type OwnerCommentsState } from './notices.js';
4
- import type { CreateRoomRequest, CreateTaskRequest, TaskRoomApplicationService } from '../application/task-room-service.js';
4
+ import type { CreateRoomRequest, CreateTaskRequest, TaskProvisioningOutcome, TaskRoomApplicationService } from '../application/task-room-service.js';
5
5
  import type { TaskState } from '../rooms-tasks/types.js';
6
6
  /**
7
7
  * Fleet-level effects a deterministic owner command may trigger. Production
@@ -19,6 +19,9 @@ export interface OwnerFleetOps {
19
19
  settleTask(taskId: string): Promise<void>;
20
20
  /** Settle an accepted task deletion outside the caller role's supervisor lifecycle. */
21
21
  settleTaskDeletion(taskId: string): Promise<void>;
22
+ /** Continue an accepted provisioning saga until convergence. */
23
+ provisionTask(taskId: string): Promise<void>;
24
+ /** Explicit recovery may also route terminal and deletion intents. */
22
25
  recoverTask(taskId: string): Promise<void>;
23
26
  }
24
27
  /**
@@ -58,7 +61,9 @@ export interface OwnerCommandContext {
58
61
  terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
59
62
  recoverTask(taskId: string): Promise<void>;
60
63
  createTask(input: Omit<CreateTaskRequest, 'actor'>): Promise<TaskRecord>;
61
- startTask(taskId: string): Promise<TaskRecord>;
64
+ startTask(taskId: string): Promise<TaskProvisioningOutcome | TaskRecord>;
65
+ awaitTask?(taskId: string): Promise<TaskProvisioningOutcome>;
66
+ taskProvisioningOutcome?(taskId: string): TaskProvisioningOutcome;
62
67
  listTasks(filter?: {
63
68
  state?: TaskState | TaskState[];
64
69
  list?: string;
@@ -106,6 +106,24 @@ const taskAction = (title, task, fields = []) => renderMarkdownResult({
106
106
  ...fields,
107
107
  ],
108
108
  });
109
+ const taskProvisioningAction = (outcome) => {
110
+ if (outcome.kind === 'failed')
111
+ return renderMarkdownFailure({
112
+ kind: 'state', subject: `/task await ${outcome.task.task_id}`,
113
+ detail: outcome.blocker ?? 'Provisioning reached a terminal failure.',
114
+ action: outcome.next_action ?? `Run /task recover ${outcome.task.task_id}.`,
115
+ });
116
+ return taskAction(outcome.kind === 'ready' ? 'Room provisioning complete'
117
+ : outcome.next_action ? 'Room provisioning needs attention' : 'Room provisioning continues', outcome.task, [
118
+ ...(outcome.room ? [{ label: 'Room', value: `${outcome.room.room_id} — ${outcome.room.room_name}` }] : []),
119
+ ...(outcome.launch.template ? [{ label: 'Template', value: outcome.launch.template, kind: 'code' }] : []),
120
+ { label: 'Members', value: `${outcome.members.active}/${outcome.members.expected} active; ${outcome.members.launched}/${outcome.members.expected} launched` },
121
+ ...(outcome.blocker ? [{ label: 'Blocker', value: outcome.blocker }] : []),
122
+ ...(outcome.next_action ? [{ label: 'Next action', value: outcome.next_action }] : []),
123
+ ...(outcome.kind === 'in_progress'
124
+ ? [{ label: 'Await', value: `/task await ${outcome.task.task_id}`, kind: 'code' }] : []),
125
+ ]);
126
+ };
109
127
  const roomAction = (title, room, fields = []) => renderMarkdownResult({
110
128
  icon: '🏠', title,
111
129
  fields: [
@@ -319,6 +337,12 @@ export const ownerCommands = [
319
337
  title: titleParts.join(' '), origin: { type: 'owner_channel' },
320
338
  backlog, template, noRoom, brief: trailingLines, list,
321
339
  });
340
+ const provisioning = !backlog && !noRoom && t.room_id
341
+ ? ctx.taskProvisioningOutcome?.(t.task_id) : undefined;
342
+ if (provisioning) {
343
+ await ctx.reply(taskProvisioningAction(provisioning));
344
+ break;
345
+ }
322
346
  await ctx.reply(taskAction('Task created', t, [
323
347
  { label: 'Title', value: t.title },
324
348
  ...(template ? [{ label: 'Template', value: template, kind: 'code' }] : []),
@@ -419,8 +443,17 @@ export const ownerCommands = [
419
443
  case 'start': {
420
444
  if (!rest[0])
421
445
  throw new OwnerCommandUsageError('usage: /task start <id>');
422
- const t = await ctx.startTask(rest[0]);
423
- await ctx.reply(taskAction('Task started', t));
446
+ const started = await ctx.startTask(rest[0]);
447
+ await ctx.reply('kind' in started
448
+ ? taskProvisioningAction(started) : taskAction('Task started', started));
449
+ break;
450
+ }
451
+ case 'await': {
452
+ if (!rest[0])
453
+ throw new OwnerCommandUsageError('usage: /task await <id>');
454
+ if (!ctx.awaitTask)
455
+ throw new Error('task await is unavailable');
456
+ await ctx.reply(taskProvisioningAction(await ctx.awaitTask(rest[0])));
424
457
  break;
425
458
  }
426
459
  case 'block': {
@@ -731,6 +764,7 @@ export function fleetCliOps(role, configPath) {
731
764
  closeRoom: roomId => launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath),
732
765
  settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
733
766
  settleTaskDeletion: taskId => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, configPath),
767
+ provisionTask: taskId => launchFleetWorker(['task', '_provision', taskId], `task-provision-${taskId}`, configPath),
734
768
  recoverTask: taskId => launchFleetWorker(['task', '_recover', taskId], `task-recover-${taskId}`, configPath),
735
769
  };
736
770
  }
@@ -0,0 +1,24 @@
1
+ import { type FleetAuditPresentation } from '../fleet-command-audit.js';
2
+ type LifecycleDelivery = 'pending' | 'delivered' | 'uncertain';
3
+ interface LifecycleOutboxEntry {
4
+ digest: string;
5
+ presentation: FleetAuditPresentation;
6
+ delivery: LifecycleDelivery;
7
+ }
8
+ /** Durable lifecycle acceptance queue; plaintext is limited to validated concise presentations. */
9
+ export declare class FleetLifecycleOutbox {
10
+ private readonly path;
11
+ private entries;
12
+ private corruptReason?;
13
+ constructor(path: string);
14
+ integrity(): {
15
+ ok: boolean;
16
+ error?: string;
17
+ };
18
+ enqueue(presentations: FleetAuditPresentation[]): void;
19
+ pending(): readonly LifecycleOutboxEntry[];
20
+ finish(digest: string, delivery: Exclude<LifecycleDelivery, 'pending'>): void;
21
+ private assertHealthy;
22
+ private persist;
23
+ }
24
+ export {};
@@ -0,0 +1,87 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { chmodSync, existsSync, readFileSync } from 'node:fs';
3
+ import { replaceFileAtomically } from '../atomic-file.js';
4
+ import { lifecycleEventDigestBasis, validateFleetAuditPresentations, } from '../fleet-command-audit.js';
5
+ const DIGEST = /^[a-f0-9]{64}$/u;
6
+ const LIMIT = 5_000;
7
+ /** Durable lifecycle acceptance queue; plaintext is limited to validated concise presentations. */
8
+ export class FleetLifecycleOutbox {
9
+ path;
10
+ entries = [];
11
+ corruptReason;
12
+ constructor(path) {
13
+ this.path = path;
14
+ if (!existsSync(path))
15
+ return;
16
+ try {
17
+ const value = JSON.parse(readFileSync(path, 'utf8'));
18
+ if (value.version !== 1 || !Array.isArray(value.entries) || value.entries.length > LIMIT)
19
+ throw new Error('invalid lifecycle outbox');
20
+ const digests = new Set();
21
+ for (const entry of value.entries) {
22
+ validateFleetAuditPresentations([entry?.presentation]);
23
+ if (!DIGEST.test(entry.digest)
24
+ || !['pending', 'delivered', 'uncertain'].includes(entry.delivery)
25
+ || digests.has(entry.digest))
26
+ throw new Error('invalid lifecycle outbox entry');
27
+ digests.add(entry.digest);
28
+ }
29
+ this.entries = structuredClone(value.entries);
30
+ chmodSync(path, 0o600);
31
+ }
32
+ catch {
33
+ this.corruptReason = 'invalid persisted Fleet lifecycle outbox';
34
+ this.entries = [];
35
+ try {
36
+ chmodSync(path, 0o600);
37
+ }
38
+ catch { /* remain fail-closed */ }
39
+ }
40
+ }
41
+ integrity() {
42
+ return this.corruptReason ? { ok: false, error: this.corruptReason } : { ok: true };
43
+ }
44
+ enqueue(presentations) {
45
+ this.assertHealthy();
46
+ validateFleetAuditPresentations(presentations);
47
+ let changed = false;
48
+ for (const presentation of presentations) {
49
+ const digest = createHash('sha256').update(lifecycleEventDigestBasis(presentation)).digest('hex');
50
+ if (this.entries.some(entry => entry.digest === digest))
51
+ continue;
52
+ if (this.entries.length >= LIMIT) {
53
+ const settled = this.entries.findIndex(entry => entry.delivery !== 'pending');
54
+ if (settled < 0)
55
+ throw new Error(`Fleet lifecycle outbox is limited to ${LIMIT} pending events`);
56
+ this.entries.splice(settled, 1);
57
+ }
58
+ this.entries.push({ digest, presentation: structuredClone(presentation), delivery: 'pending' });
59
+ changed = true;
60
+ }
61
+ if (changed)
62
+ this.persist();
63
+ }
64
+ pending() {
65
+ this.assertHealthy();
66
+ return this.entries.filter(entry => entry.delivery === 'pending').map(entry => structuredClone(entry));
67
+ }
68
+ finish(digest, delivery) {
69
+ this.assertHealthy();
70
+ const entry = this.entries.find(candidate => candidate.digest === digest);
71
+ if (!entry)
72
+ throw new Error('unknown Fleet lifecycle outbox entry');
73
+ if (entry.delivery === delivery)
74
+ return;
75
+ if (entry.delivery !== 'pending')
76
+ throw new Error('conflicting Fleet lifecycle delivery state');
77
+ entry.delivery = delivery;
78
+ this.persist();
79
+ }
80
+ assertHealthy() {
81
+ if (this.corruptReason)
82
+ throw new Error(this.corruptReason);
83
+ }
84
+ persist() {
85
+ replaceFileAtomically(this.path, `${JSON.stringify({ version: 1, entries: this.entries })}\n`, 0o600);
86
+ }
87
+ }
@@ -31,7 +31,7 @@ interface MigrationHooks {
31
31
  beforeLockClaim?: () => void;
32
32
  beforeRoleDefaultPublish?: () => void;
33
33
  }
34
- /** Explicit adoption of exact packaged revision-3/4 role defaults; customized files are never changed. */
34
+ /** Explicit adoption of exact known packaged role defaults; customized files are never changed. */
35
35
  export declare function migratePackagedRoleDefaults(configuration: string, options?: {
36
36
  write?: boolean;
37
37
  }, hooks?: MigrationHooks): RoleDefaultMigrationResult;
@@ -51,7 +51,23 @@ const V4_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
51
51
  const V4_GENERATED_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
52
52
  'agent_templates/LocalCoordinator.yaml': 'cb7c83a835dde9d45bac42cadce2f196f561d2460321c690cdef435f7076f97f',
53
53
  });
54
- const V5_ROLE_DEFAULTS = new Set([
54
+ const V5_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
55
+ 'roles/LocalCoordinator.yaml': '52714be635060adca2bc7fe99c27c3184be6c78e3874a77e61a2fb135bac1278',
56
+ 'roles/Developer.yaml': '04278f325c366035e82d8e7f4aff72badb297eaf2fb5afae38a28a5aaf9260c9',
57
+ 'roles/Critic.yaml': 'bcb96c84534ca6fc466a8ee63f679785d347ed1cdf223efcc36ca5ed4ca7f3de',
58
+ 'agent_templates/LocalCoordinator.yaml': 'c59980ab7d94dad4d4961ba308cf62342291d8f1c57cf2faf900e80136fa2e7d',
59
+ 'agent_templates/Developer.yaml': '36f1d010a9c8591ae5ba422ef1ac26335b9f7f3382b6b8532844df754a56287e',
60
+ 'agent_templates/Critic.yaml': '7025b842e260a9c91ebc64dd3a11162a82b025b287457c6cb6f6c8964b38cdfd',
61
+ 'room_templates/single.yaml': '61051917b1f4f53a13687cc69d3f9ad98f1b02558f82b236b22aac942850395f',
62
+ 'room_templates/pair.yaml': 'a20b2145fa84e5d61266385e629892734b98fc61be9d0c7647c3778ec1bb40a8',
63
+ 'room_templates/team.yaml': 'c455f54a1f86106bfac7192299130dabfb91fa6179d2dedcc0573a384a902411',
64
+ });
65
+ const V5_GENERATED_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
66
+ 'agent_templates/LocalCoordinator.yaml': 'cb7c83a835dde9d45bac42cadce2f196f561d2460321c690cdef435f7076f97f',
67
+ 'agent_templates/Developer.yaml': 'fdd953aab72d042e62780dd273598745aae59521b39edf4876d7676bb7189c83',
68
+ 'agent_templates/Critic.yaml': '3e98a28f17876061cfadf984351aa59fdbe19629e514eff52c74546545c64a91',
69
+ });
70
+ const CURRENT_ROLE_DEFAULTS = new Set([
55
71
  'roles/Coordinator.yaml', 'roles/LocalCoordinator.yaml', 'roles/Developer.yaml', 'roles/Critic.yaml',
56
72
  'agent_templates/LocalCoordinator.yaml', 'agent_templates/Developer.yaml', 'agent_templates/Critic.yaml',
57
73
  'room_templates/single.yaml', 'room_templates/pair.yaml', 'room_templates/team.yaml',
@@ -152,7 +168,7 @@ function validateRoleDefaultMigration(stageManifest) {
152
168
  throw new Error(`Room Template '${template.name}' references missing Agent Template '${member.agent_template}'`);
153
169
  }
154
170
  }
155
- /** Explicit adoption of exact packaged revision-3/4 role defaults; customized files are never changed. */
171
+ /** Explicit adoption of exact known packaged role defaults; customized files are never changed. */
156
172
  export function migratePackagedRoleDefaults(configuration, options = {}, hooks = {}) {
157
173
  const configPath = resolve(configuration);
158
174
  const root = splitRootFor(configPath);
@@ -176,6 +192,7 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
176
192
  const known = new Set([
177
193
  ...Object.keys(V3_ROLE_DEFAULT_FINGERPRINTS),
178
194
  ...Object.keys(V4_ROLE_DEFAULT_FINGERPRINTS),
195
+ ...Object.keys(V5_ROLE_DEFAULT_FINGERPRINTS),
179
196
  ]);
180
197
  for (const relative of known) {
181
198
  const path = join(root, relative);
@@ -187,14 +204,16 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
187
204
  V3_GENERATED_ROLE_DEFAULT_FINGERPRINTS[relative],
188
205
  V4_ROLE_DEFAULT_FINGERPRINTS[relative],
189
206
  V4_GENERATED_ROLE_DEFAULT_FINGERPRINTS[relative],
207
+ V5_ROLE_DEFAULT_FINGERPRINTS[relative],
208
+ V5_GENERATED_ROLE_DEFAULT_FINGERPRINTS[relative],
190
209
  ].some(expected => expected !== undefined && actual === expected);
191
210
  if (recognized)
192
- (V5_ROLE_DEFAULTS.has(relative) ? replacements : removals).push(path);
211
+ (CURRENT_ROLE_DEFAULTS.has(relative) ? replacements : removals).push(path);
193
212
  else
194
213
  preserved.push(path);
195
214
  }
196
215
  const additions = [];
197
- for (const relative of V5_ROLE_DEFAULTS) {
216
+ for (const relative of CURRENT_ROLE_DEFAULTS) {
198
217
  const path = join(root, relative);
199
218
  if (!existsSync(path))
200
219
  additions.push(path);