@ours.network/fleet 1.2.0-nightly.12 → 1.2.0-nightly.14

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 (108) hide show
  1. package/README.md +2 -0
  2. package/dist/application/model-catalog.d.ts +1 -1
  3. package/dist/application/model-catalog.js +5 -2
  4. package/dist/application/task-room-service.d.ts +3 -0
  5. package/dist/application/task-room-service.js +1 -1
  6. package/dist/atomic-file.d.ts +5 -0
  7. package/dist/atomic-file.js +62 -1
  8. package/dist/build-info.json +4 -4
  9. package/dist/erased-resources.d.ts +9 -0
  10. package/dist/erased-resources.js +87 -0
  11. package/dist/fleet-command-audit.d.ts +1 -0
  12. package/dist/fleet-command-audit.js +9 -5
  13. package/dist/init-wizard.js +10 -3
  14. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  15. package/dist/rooms-tasks/close.d.ts +2 -2
  16. package/dist/rooms-tasks/close.js +128 -22
  17. package/dist/rooms-tasks/deletion.d.ts +1 -1
  18. package/dist/rooms-tasks/deletion.js +41 -9
  19. package/dist/rooms-tasks/erasure.d.ts +6 -0
  20. package/dist/rooms-tasks/erasure.js +169 -0
  21. package/dist/rooms-tasks/room-state.d.ts +3 -1
  22. package/dist/rooms-tasks/room-state.js +37 -7
  23. package/dist/rooms-tasks/task-state.d.ts +10 -4
  24. package/dist/rooms-tasks/task-state.js +54 -12
  25. package/dist/rooms-tasks/types.d.ts +2 -0
  26. package/dist/temp-lifecycle.d.ts +5 -0
  27. package/dist/temp-lifecycle.js +28 -12
  28. package/docs/task-workspaces.md +34 -2
  29. package/package.json +1 -1
  30. package/presets/README.md +66 -0
  31. package/presets/brain-catalog.json +7 -2
  32. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  33. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  34. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  35. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  36. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  37. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  38. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  39. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  40. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  41. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  42. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  43. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  44. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  45. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  46. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  47. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  48. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  49. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  50. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  51. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  52. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  53. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  54. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  55. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  56. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  57. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  58. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  59. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  60. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  61. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  62. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  63. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  64. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  65. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  66. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  67. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  68. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  69. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  70. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  71. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  72. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  73. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  74. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  75. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  76. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  77. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  78. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  79. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  80. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  81. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  82. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  83. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  84. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  85. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  86. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  87. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  88. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  89. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  90. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  91. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  92. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  93. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  94. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  95. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  96. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  97. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  98. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  99. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  100. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  101. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  102. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  103. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  104. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  105. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  106. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  107. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  108. package/presets/manifest.json +2 -2
@@ -7,14 +7,23 @@ import { getTask } from './task-state.js';
7
7
  import { storedRoomLaunchPolicy } from './types.js';
8
8
  import { releaseLaunchSnapshot } from './launch-snapshot.js';
9
9
  export const roomsDir = () => join(stateRoot(), 'rooms');
10
- function roomPath(id) { return join(roomsDir(), `${id}.json`); }
10
+ function roomPath(id) {
11
+ if (!/^[a-zA-Z0-9_-]{1,120}$/.test(id))
12
+ throw new RoomStateError('Invalid room ID');
13
+ return join(roomsDir(), `${id}.json`);
14
+ }
11
15
  export class RoomStateError extends Error {
12
16
  }
13
17
  function readRoom(id) {
14
18
  const p = roomPath(id);
15
- if (!existsSync(p))
16
- throw new RoomStateError(`room not found: ${id}`);
17
- return JSON.parse(readFileSync(p, 'utf8'));
19
+ try {
20
+ return JSON.parse(readFileSync(p, 'utf8'));
21
+ }
22
+ catch (error) {
23
+ if (error.code === 'ENOENT')
24
+ throw new RoomStateError(`room not found: ${id}`);
25
+ throw error;
26
+ }
18
27
  }
19
28
  function writeRoom(record) {
20
29
  mkdirSync(roomsDir(), { recursive: true });
@@ -54,8 +63,10 @@ export function getRoomRecord(id) {
54
63
  try {
55
64
  return readRoom(id);
56
65
  }
57
- catch {
58
- return undefined;
66
+ catch (error) {
67
+ if (error instanceof RoomStateError && error.message === `room not found: ${id}`)
68
+ return undefined;
69
+ throw error;
59
70
  }
60
71
  }
61
72
  /** Remove a terminal orchestration record from the live room inventory. */
@@ -211,7 +222,7 @@ export function beginRoomClose(id) {
211
222
  const RETIREMENT_ORDER = [
212
223
  'stop_requested', 'liveness_absent', 'archive_secured', 'identity_absent',
213
224
  ];
214
- export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath) {
225
+ export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath, absenceVerified = false) {
215
226
  const r = readRoom(id);
216
227
  if (r.state !== 'closing')
217
228
  throw new RoomStateError(`room ${id} is not closing`);
@@ -228,6 +239,7 @@ export function advanceMemberRetirement(id, roleName, phase, launchId, archivePa
228
239
  throw new RoomStateError(`room ${id} member ${roleName} retirement cannot move backward to ${phase}`);
229
240
  }
230
241
  seat.retirement = {
242
+ ...(absenceVerified || previous?.absence_verified ? { absence_verified: true } : {}),
231
243
  phase,
232
244
  launch_id: launchId,
233
245
  updated_at: new Date().toISOString(),
@@ -280,3 +292,21 @@ export function setRoomCloseError(id, error, recoveryHint) {
280
292
  writeRoom(r);
281
293
  return r;
282
294
  }
295
+ /** Settlement-only binding recovery; never publishes a seat or changes a launch. */
296
+ export function recordRetirementMemberCids(id, recovered) {
297
+ const room = readRoom(id);
298
+ if (room.state !== 'closing')
299
+ throw new RoomStateError('Room is not closing');
300
+ for (const proof of recovered) {
301
+ const seat = room.member_seats.find(value => value.role_name === proof.role_name);
302
+ if (!seat || seat.launch?.action_id !== proof.launch?.action_id || seat.invite_id !== proof.invite_id)
303
+ throw new RoomStateError('Member changed during identity recovery');
304
+ if (!proof.identity_cid)
305
+ continue;
306
+ if (seat.identity_cid && seat.identity_cid.toLowerCase() !== proof.identity_cid.toLowerCase())
307
+ throw new RoomStateError('Member identity changed during recovery');
308
+ seat.identity_cid = proof.identity_cid;
309
+ }
310
+ writeRoom(room);
311
+ return room;
312
+ }
@@ -62,9 +62,8 @@ export declare function getDeletingTask(id: string): TaskRecord;
62
62
  export declare function taskDeletionState(id: string): 'none' | 'pending' | 'absent';
63
63
  /**
64
64
  * Durable, surface-independent audit evidence for a permanent deletion. The
65
- * receipt outlives the task record: written with the acceptance intent, and
66
- * completed before settlement is reported. Metadata only — never brief or
67
- * room content.
65
+ * receipt supports in-flight recovery only: written with acceptance intent,
66
+ * then erased before settlement is reported. It may contain a bounded title.
68
67
  */
69
68
  export interface TaskDeletionReceipt {
70
69
  schema_version: 1;
@@ -88,7 +87,7 @@ export declare function readTaskDeletionReceipt(id: string): TaskDeletionReceipt
88
87
  * settlement rather than deleting resources without audit evidence.
89
88
  */
90
89
  export declare function ensureTaskDeletionReceipt(id: string): void;
91
- /** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
90
+ /** Erase the in-flight receipt; also heals a crash after task unlink. */
92
91
  export declare function completeTaskDeletionReceipt(id: string): void;
93
92
  export type TaskDeletionAcceptance = {
94
93
  status: 'accepted' | 'pending';
@@ -129,6 +128,7 @@ interface SeatEvidence {
129
128
  action_id?: string;
130
129
  };
131
130
  retirement?: {
131
+ absence_verified?: boolean;
132
132
  phase: TaskDeletionMemberPhase;
133
133
  launch_id: string;
134
134
  archive_path?: string;
@@ -166,4 +166,10 @@ export declare function beginTaskWorkspaceCleanup(id: string): void;
166
166
  * records are the idempotent already-settled outcome.
167
167
  */
168
168
  export declare function unlinkDeletedTask(id: string): boolean;
169
+ /** Called only after current temp and exact identity absence have been checked. */
170
+ export declare function settleAbsentTaskDeletionMember(id: string, name: string): void;
171
+ /** Room-only erasure keeps the parent Task but removes its dead room/member links. */
172
+ export declare function detachDeletedRoom(taskId: string, roomId: string): void;
173
+ /** A completed room may consume its archives while other task members still retire. */
174
+ export declare function beginTaskArchiveCleanup(id: string): void;
169
175
  export {};
@@ -535,14 +535,10 @@ export function ensureTaskDeletionReceipt(id) {
535
535
  writeDeletionReceiptForIntent(stored);
536
536
  });
537
537
  }
538
- /** Record settlement on the receipt; idempotent, tolerant of legacy absence. */
538
+ /** Erase the in-flight receipt; also heals a crash after task unlink. */
539
539
  export function completeTaskDeletionReceipt(id) {
540
- const receipt = readTaskDeletionReceipt(id);
541
- if (!receipt || receipt.settled_at)
542
- return;
543
- receipt.settled_at = new Date().toISOString();
544
- receipt.result = 'deleted';
545
- replaceFileAtomically(deletionReceiptPath(id), JSON.stringify(receipt, null, 2) + '\n');
540
+ assertCanonicalTaskId(id);
541
+ rmSync(deletionReceiptPath(id), { force: true });
546
542
  }
547
543
  /**
548
544
  * Persist the first-wins durable deletion intent. Accepts every lifecycle
@@ -561,8 +557,10 @@ export function beginTaskDeletionIntent(id, actor) {
561
557
  stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
562
558
  }
563
559
  catch (error) {
564
- if (isNotFoundError(error))
560
+ if (isNotFoundError(error)) {
561
+ completeTaskDeletionReceipt(id);
565
562
  return { status: 'already_absent' };
563
+ }
566
564
  throw error;
567
565
  }
568
566
  if (stored.deletion?.status === 'pending') {
@@ -712,12 +710,17 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
712
710
  const evidence = seat.retirement;
713
711
  if (evidence?.phase !== 'identity_absent')
714
712
  throw new TaskStateError(`task ${id} deletion cannot checkpoint member '${seat.role_name}' before completed retirement`);
715
- const neverLaunched = evidence.launch_id === DELETION_MEMBER_NEVER_LAUNCHED;
713
+ const neverLaunched = evidence.launch_id === DELETION_MEMBER_NEVER_LAUNCHED
714
+ || evidence.launch_id === DELETION_MEMBER_ABSENT_VERIFIED || evidence.absence_verified === true;
716
715
  if (!neverLaunched && evidence.archive_path === undefined)
717
716
  throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' retirement evidence lacks its archive`);
718
717
  if (!seat.identity_cid) {
719
- if (neverLaunched)
720
- continue; // provably never held a managed identity
718
+ if (neverLaunched) {
719
+ stored.deletion.absent_members ??= [];
720
+ if (!stored.deletion.absent_members.includes(seat.role_name))
721
+ stored.deletion.absent_members.push(seat.role_name);
722
+ continue;
723
+ }
721
724
  const proof = proofs.find(value => value.name === seat.role_name);
722
725
  if (proof && proof.launch_id === evidence.launch_id && proof.archive_path === evidence.archive_path
723
726
  && proof.launch_id === seat.launch?.launch_id && proof.action_id === seat.launch?.action_id) {
@@ -761,7 +764,7 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
761
764
  export function beginTaskWorkspaceCleanup(id) {
762
765
  withTaskLock(id, () => {
763
766
  const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
764
- if (stored.deletion?.status !== 'pending' || !stored.workspace
767
+ if (stored.deletion?.status !== 'pending'
765
768
  || stored.deletion.members.some(member => member.phase !== 'identity_absent'))
766
769
  throw new TaskStateError('Task is not ready for workspace cleanup');
767
770
  stored.deletion.workspace_cleanup_started_at ??= new Date().toISOString();
@@ -798,3 +801,42 @@ export function unlinkDeletedTask(id) {
798
801
  return true;
799
802
  });
800
803
  }
804
+ /** Called only after current temp and exact identity absence have been checked. */
805
+ export function settleAbsentTaskDeletionMember(id, name) {
806
+ withTaskLock(id, () => {
807
+ const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
808
+ if (stored.deletion?.status !== 'pending')
809
+ throw new TaskStateError('Task is not deleting');
810
+ const member = stored.deletion.members.find(value => value.name === name);
811
+ if (!member)
812
+ throw new TaskStateError('Unknown deletion member');
813
+ member.phase = 'identity_absent';
814
+ member.launch_id ??= DELETION_MEMBER_ABSENT_VERIFIED;
815
+ member.updated_at = new Date().toISOString();
816
+ writeTask(stored);
817
+ });
818
+ }
819
+ /** Room-only erasure keeps the parent Task but removes its dead room/member links. */
820
+ export function detachDeletedRoom(taskId, roomId) {
821
+ withTaskLock(taskId, () => {
822
+ if (!existsSync(taskPath(taskId)))
823
+ return;
824
+ const stored = JSON.parse(readFileSync(taskPath(taskId), 'utf8'));
825
+ if (stored.room_id !== roomId || stored.deletion?.status === 'pending')
826
+ return;
827
+ delete stored.room_id;
828
+ delete stored.room_identity_cid;
829
+ stored.member_roles = [];
830
+ writeTask(stored);
831
+ });
832
+ }
833
+ /** A completed room may consume its archives while other task members still retire. */
834
+ export function beginTaskArchiveCleanup(id) {
835
+ withTaskLock(id, () => {
836
+ const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
837
+ if (stored.deletion?.status !== 'pending')
838
+ throw new TaskStateError('Task is not deleting');
839
+ stored.deletion.workspace_cleanup_started_at ??= new Date().toISOString();
840
+ writeTask(stored);
841
+ });
842
+ }
@@ -88,6 +88,7 @@ export interface TaskDeletionIntent {
88
88
  /** Snapshot of managed members at acceptance; the missing-room retirement evidence. */
89
89
  members: TaskDeletionMemberCursor[];
90
90
  archived_absences?: ArchivedMemberAbsence[];
91
+ absent_members?: string[];
91
92
  /** Archive proofs were verified before owned workspace cleanup may consume them. */
92
93
  workspace_cleanup_started_at?: string;
93
94
  error?: string;
@@ -212,6 +213,7 @@ export type RoomHistoryEvidence = {
212
213
  };
213
214
  export type MemberRetirementPhase = 'stop_requested' | 'liveness_absent' | 'archive_secured' | 'identity_absent';
214
215
  export interface MemberRetirement {
216
+ absence_verified?: boolean;
215
217
  phase: MemberRetirementPhase;
216
218
  launch_id: string;
217
219
  updated_at: string;
@@ -44,6 +44,11 @@ export declare function makeTempSupervisorLauncher(options?: {
44
44
  }): SupervisorLauncher;
45
45
  export declare function markTempSupervisorActive(dir: string, pid?: number): Promise<void>;
46
46
  export declare function requestedTempStopReason(dir: string): 'operator-stop' | undefined;
47
+ /** Explicit erasure removes only exact retired role/launch events. */
48
+ export declare function eraseTerminationEvents(launches: ReadonlyArray<{
49
+ role: string;
50
+ launchId: string;
51
+ }>): void;
47
52
  /** Move retired state out of the live roster without deleting any evidence. */
48
53
  export declare function archiveTempState(role: string, reason: TempTerminationReason, outcome: TempTerminationRecord['outcome'], detail: string, now?: Date): string | undefined;
49
54
  /** Resolve only an exact role+launch archive; names and timestamps are never proof. */
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { basename, join } from 'node:path';
4
- import { replaceFileAtomically, withFileLock } from './atomic-file.js';
4
+ import { replaceFileAtomically, withFileLock, withSynchronousFileLock } from './atomic-file.js';
5
5
  import { realExec } from './exec.js';
6
6
  import { stateRoot, tmpRoot } from './paths.js';
7
7
  export const TEMP_SUPERVISOR_FILE = '.temp-supervisor.json';
@@ -133,18 +133,34 @@ function appendTermination(dir, record) {
133
133
  replaceFileAtomically(join(dir, TEMP_GLOBAL_TERMINATION_MARKER), line);
134
134
  }
135
135
  function appendGlobalTermination(line, checkExisting = false) {
136
- mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
137
- const path = join(archiveRoot(), 'terminations.jsonl');
138
- // Recovery may revisit a .retiring directory after a crash between the
139
- // global append and final rename. Avoid duplicating that exact event.
140
- if (checkExisting) {
141
- try {
142
- if (readFileSync(path, 'utf8').split('\n').includes(line.trimEnd()))
143
- return;
136
+ withSynchronousFileLock(join(stateRoot(), 'locks', 'termination-journal'), () => {
137
+ mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
138
+ const path = join(archiveRoot(), 'terminations.jsonl');
139
+ // Recovery may revisit a .retiring directory after a crash between the
140
+ // global append and final rename. Avoid duplicating that exact event.
141
+ if (checkExisting) {
142
+ try {
143
+ if (readFileSync(path, 'utf8').split('\n').includes(line.trimEnd()))
144
+ return;
145
+ }
146
+ catch { /* the journal does not exist yet */ }
144
147
  }
145
- catch { /* the journal does not exist yet */ }
146
- }
147
- appendFileSync(path, line, { mode: 0o600 });
148
+ appendFileSync(path, line, { mode: 0o600 });
149
+ });
150
+ }
151
+ /** Explicit erasure removes only exact retired role/launch events. */
152
+ export function eraseTerminationEvents(launches) {
153
+ withSynchronousFileLock(join(stateRoot(), 'locks', 'termination-journal'), () => {
154
+ const path = join(archiveRoot(), 'terminations.jsonl');
155
+ if (!existsSync(path))
156
+ return;
157
+ const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
158
+ const kept = lines.filter(line => {
159
+ const row = JSON.parse(line);
160
+ return !launches.some(launch => launch.role === row.role && launch.launchId === row.launchId);
161
+ });
162
+ replaceFileAtomically(path, kept.length ? kept.join('\n') + '\n' : '');
163
+ });
148
164
  }
149
165
  /** Pick a sibling path without overwriting evidence from an earlier attempt. */
150
166
  function collisionSafeArchivePaths(targetBase, retiringBase) {
@@ -52,8 +52,21 @@ matching archived launches, including failed and replaced attempts, using the
52
52
  workspace descriptor plus role and termination/launch evidence. It preflights
53
53
  these archives and atomically transfers them to `.fleet-retired-agents` in the
54
54
  workspace before removal. Unrelated archives are not adopted by a name prefix.
55
- Fleet audit receipts, creation/termination journals and shared infrastructure
56
- logs are audit metadata and intentionally outlive Task artifact deletion.
55
+ Explicit deletion also erases legacy archives with exact launch/action or room
56
+ provenance, matching private supervisor journals and admission/launch descriptors,
57
+ and the exact retired-launch rows in the global termination journal. It never
58
+ infers ownership from an Agent's shared `cwd`. Artifact removal uses a durable
59
+ manifest, ownership fingerprints and per-entry rename/removal checkpoints, so
60
+ an interrupted deletion can resume without deleting a replacement at the old path.
61
+ Task deletion acceptance receipts exist only during cleanup and are erased after
62
+ settlement (including retry after a crash between Task unlink and receipt cleanup).
63
+ Shared Fleet command audit and lifecycle delivery ledgers have exactly attributed
64
+ target labels, configuration and command arguments removed; unrelated entries are
65
+ preserved. Delivery/deduplication metadata and a content-free erased-resource ID
66
+ registry remain so a running writer cannot resurrect erased content. Infrastructure
67
+ logs without structured ownership, backups, and copies already delivered to other
68
+ identities are external retention boundaries; this is not a claim to erase every
69
+ external copy.
57
70
 
58
71
  Provisioning serializes Task operation then Room close locks, including bounded
59
72
  seat waits, so concurrent retries cannot publish competing members and deletion
@@ -96,3 +109,22 @@ remote result; this change deduplicates locally recorded rooms and does not clai
96
109
  to solve that remote transaction window. Inspect remote room ownership before
97
110
  retrying ambiguous remote creation. No services or installed Fleet code are
98
111
  changed by building or testing this feature.
112
+
113
+
114
+ ## Broken provisioning and deletion recovery
115
+
116
+ Deletion accepts every lifecycle state. Missing temporary directories, identities,
117
+ remote rooms and already-erased artifacts are settled outcomes. Retry the same
118
+ explicit delete after an interruption. A missing local member CID is recovered
119
+ only from a matching authenticated seat in the pinned room (exact invite and
120
+ role, uniquely matched), or a supervisor journal tied to the selected daemon,
121
+ temporary instance and recorded creation action. These sources must agree.
122
+ The recovered CID is persisted before retirement consumes the evidence.
123
+
124
+ A contradictory CID, replacement launch, ambiguous seat, unreadable/corrupt
125
+ record, or unreachable authority is not evidence of absence. Cleanup retains its
126
+ cursor and reports the conflict instead of deleting an unrelated resource or
127
+ claiming complete erasure. An identity with no recoverable authenticated binding
128
+ requires restoring its actual ownership evidence; a name-only force removal is
129
+ not supported. Room deletion removes the linked Task's dead room/member links and preserves its workspace; use explicit
130
+ Task deletion when the request includes the associated Task and its artifacts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.12",
3
+ "version": "1.2.0-nightly.14",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -0,0 +1,66 @@
1
+ # Predefined Brains
2
+
3
+ `brain-catalog.json` is the source for generated `fleet/brains/*.yaml` files.
4
+ Run `node scripts/generate-brain-presets.mjs` after changing it, and run the same
5
+ command with `--check` to verify generated files. An empty `efforts` array generates
6
+ one `-default` preset with no `effort` field. Bootstrap adds missing presets and
7
+ preserves existing user files. Existing model presets remain available.
8
+
9
+ ## Current model additions (verified 2026-09-24)
10
+
11
+ | Harness | Exact model ID | Preset efforts |
12
+ | --- | --- | --- |
13
+ | Codex | `gpt-6-sol` | low, medium, high, xhigh, max, ultra |
14
+ | Codex | `gpt-6-luna` | low, medium, high, xhigh, max |
15
+ | Claude Code | `claude-fable-5-1` | low, medium, high, xhigh, max |
16
+ | Claude Code | `claude-opus-5-5` | low, medium, high, xhigh, max |
17
+ | Claude Code (retained) | `claude-sonnet-5` | low, medium, high, xhigh, max |
18
+ | Claude Code | `claude-haiku-4-5-20251001` | default, with no effort setting |
19
+
20
+ For example, select `codex-gpt-6-sol-ultra`, `claude-opus-5-5-max`, or
21
+ `claude-haiku-4-5-20251001-default` as a Brain reference. The init wizard offers
22
+ models supporting its Quick/Balanced/Thorough (low/medium/high) choices; use the
23
+ named Brain directly for Haiku, which does not support effort.
24
+
25
+ ### Runtime requirements and evidence
26
+
27
+ Codex CLI **0.155.0** freshly queried through `app-server` `initialize` then
28
+ `model/list` advertised the GPT rows above. A previously populated
29
+ `~/.codex/models_cache.json` did not contain these models; refresh runtime discovery
30
+ before concluding a model is unavailable. `ours-fleet doctor` compares presets to
31
+ the local cache. The catalog is not an account entitlement guarantee.
32
+
33
+ The [Sol](https://developers.openai.com/api/docs/models/gpt-6-sol) and
34
+ [Luna](https://developers.openai.com/api/docs/models/gpt-6-luna) API model pages
35
+ also support `none`, but the verified Codex runtime does not advertise it.
36
+ Consequently there are no `none` presets. Sol's `ultra` is an advertised Codex
37
+ harness mode, not an additional API reasoning level; Luna does not advertise it.
38
+
39
+ Claude Code **2.1.281** was verified using the Agent SDK `supportedModels()`
40
+ control request, with `pathToClaudeCodeExecutable` pointing to the installed
41
+ Claude CLI, without sending an inference prompt. It reports Fable 5.1, Opus 5.5,
42
+ Sonnet 5 and Haiku 4.5 with the capabilities above. These are verified versions,
43
+ not asserted minimum versions. Provider references:
44
+ [model IDs](https://platform.claude.com/docs/en/models/overview) and
45
+ [Claude Code effort support](https://code.claude.com/docs/en/model-config#adjust-effort-level).
46
+
47
+ **Check the executable used by ACP before selecting the new Claude presets.**
48
+ The inspected `claude-agent-acp` **0.63.0** installation bundles Agent SDK
49
+ **0.3.220**, whose default executable reports older models. To use the verified
50
+ installed Claude CLI, set `CLAUDE_CODE_EXECUTABLE` to its absolute path in the
51
+ launch environment (for example `/usr/bin/claude` on the verification host).
52
+ Otherwise use a bundled SDK whose runtime advertises these models and efforts.
53
+ The presets do not change your executable or restart agents. An older bundled
54
+ runtime alone is not evidence of support for the new models.
55
+
56
+ No-prompt ACP startup and `session/set_config_option` probes also verified all
57
+ new GPT and Claude effort combinations, including Sol `ultra` and Claude `max`;
58
+ Haiku startup advertises no effort option. The probes used Codex ACP 1.10.0
59
+ with the installed Codex executable and Claude ACP 0.63.0 with the native override.
60
+
61
+ ACP builds effort options from the selected model's `supportedEffortLevels` and
62
+ applies the selection through the SDK's session flag settings. This supports
63
+ `max` without writing it to persistent `effortLevel` settings. Fleet verifies
64
+ that ACP applied the requested effort and fails on rejection. `ultracode` is a
65
+ separate Claude orchestration setting and is not a model effort preset. Account
66
+ and organization restrictions still apply at launch; no fallback model is added.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "catalog_revision": 2,
3
+ "catalog_revision": 3,
4
4
  "models": [
5
5
  { "harness": "codex", "session": "acp", "model": "gpt-6-astra", "efforts": ["low", "medium", "high", "xhigh", "max", "ultra"] },
6
6
  { "harness": "codex", "session": "acp", "model": "gpt-5.6-sol", "efforts": ["low", "medium", "high", "xhigh", "max", "ultra"] },
@@ -12,6 +12,11 @@
12
12
  { "harness": "codex", "session": "acp", "model": "gpt-5.3-codex-spark", "efforts": ["low", "medium", "high", "xhigh"] },
13
13
  { "harness": "claude-code", "session": "acp", "model": "claude-fable-5", "efforts": ["low", "medium", "high", "xhigh", "max"] },
14
14
  { "harness": "claude-code", "session": "acp", "model": "claude-opus-5", "efforts": ["low", "medium", "high", "xhigh", "max"] },
15
- { "harness": "claude-code", "session": "acp", "model": "claude-sonnet-5", "efforts": ["low", "medium", "high", "xhigh", "max"] }
15
+ { "harness": "claude-code", "session": "acp", "model": "claude-sonnet-5", "efforts": ["low", "medium", "high", "xhigh", "max"] },
16
+ { "harness": "codex", "session": "acp", "model": "gpt-6-sol", "efforts": ["low", "medium", "high", "xhigh", "max", "ultra"] },
17
+ { "harness": "codex", "session": "acp", "model": "gpt-6-luna", "efforts": ["low", "medium", "high", "xhigh", "max"] },
18
+ { "harness": "claude-code", "session": "acp", "model": "claude-fable-5-1", "efforts": ["low", "medium", "high", "xhigh", "max"] },
19
+ { "harness": "claude-code", "session": "acp", "model": "claude-opus-5-5", "efforts": ["low", "medium", "high", "xhigh", "max"] },
20
+ { "harness": "claude-code", "session": "acp", "model": "claude-haiku-4-5-20251001", "efforts": [] }
16
21
  ]
17
22
  }
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-fable-5-1
5
+ effort: high
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-fable-5-1
5
+ effort: low
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-fable-5-1
5
+ effort: max
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-fable-5-1
5
+ effort: medium
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-fable-5-1
5
+ effort: xhigh
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-fable-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-fable-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-fable-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-fable-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-fable-5
@@ -0,0 +1,4 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-haiku-4-5-20251001
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-opus-5-5
5
+ effort: high
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-opus-5-5
5
+ effort: low
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-opus-5-5
5
+ effort: max
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-opus-5-5
5
+ effort: medium
@@ -0,0 +1,5 @@
1
+ # Generated from presets/brain-catalog.json revision 3.
2
+ harness: claude-code
3
+ session: acp
4
+ model: claude-opus-5-5
5
+ effort: xhigh
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-opus-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-opus-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-opus-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-opus-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-opus-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-sonnet-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-sonnet-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-sonnet-5
@@ -1,4 +1,4 @@
1
- # Generated from presets/brain-catalog.json revision 2.
1
+ # Generated from presets/brain-catalog.json revision 3.
2
2
  harness: claude-code
3
3
  session: acp
4
4
  model: claude-sonnet-5