@ours.network/fleet 0.19.0-nightly.1 → 0.19.0-nightly.11

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 (47) hide show
  1. package/README.md +30 -0
  2. package/dist/briefing.js +92 -6
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli.js +5 -0
  5. package/dist/config.d.ts +22 -0
  6. package/dist/config.js +44 -3
  7. package/dist/docs.d.ts +1 -1
  8. package/dist/docs.js +28 -0
  9. package/dist/doctor.js +125 -1
  10. package/dist/harness/claude-code.js +2 -0
  11. package/dist/harness/codex.js +2 -0
  12. package/dist/harness/types.d.ts +2 -0
  13. package/dist/owner-channel/channel.d.ts +7 -0
  14. package/dist/owner-channel/channel.js +47 -0
  15. package/dist/owner-channel/commands.d.ts +9 -0
  16. package/dist/owner-channel/commands.js +393 -0
  17. package/dist/rooms-tasks/cli.d.ts +4 -0
  18. package/dist/rooms-tasks/cli.js +1172 -0
  19. package/dist/rooms-tasks/close.d.ts +21 -0
  20. package/dist/rooms-tasks/close.js +159 -0
  21. package/dist/rooms-tasks/config.d.ts +15 -0
  22. package/dist/rooms-tasks/config.js +174 -0
  23. package/dist/rooms-tasks/cowork-adapter.d.ts +98 -0
  24. package/dist/rooms-tasks/cowork-adapter.js +385 -0
  25. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  26. package/dist/rooms-tasks/external-worker.js +40 -0
  27. package/dist/rooms-tasks/index.d.ts +10 -0
  28. package/dist/rooms-tasks/index.js +10 -0
  29. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  30. package/dist/rooms-tasks/member-startup.js +151 -0
  31. package/dist/rooms-tasks/provision.d.ts +29 -0
  32. package/dist/rooms-tasks/provision.js +514 -0
  33. package/dist/rooms-tasks/room-state.d.ts +34 -0
  34. package/dist/rooms-tasks/room-state.js +263 -0
  35. package/dist/rooms-tasks/task-state.d.ts +43 -0
  36. package/dist/rooms-tasks/task-state.js +277 -0
  37. package/dist/rooms-tasks/templates.d.ts +6 -0
  38. package/dist/rooms-tasks/templates.js +94 -0
  39. package/dist/rooms-tasks/terminal.d.ts +23 -0
  40. package/dist/rooms-tasks/terminal.js +53 -0
  41. package/dist/rooms-tasks/types.d.ts +260 -0
  42. package/dist/rooms-tasks/types.js +20 -0
  43. package/dist/spawn.d.ts +3 -1
  44. package/dist/spawn.js +1 -0
  45. package/dist/temp-lifecycle.d.ts +14 -2
  46. package/dist/temp-lifecycle.js +90 -0
  47. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import { type RoomCloseDeps } from './close.js';
2
+ import type { CoworkAdapter } from './cowork-adapter.js';
3
+ import type { TaskOutcome, TaskRecord, TaskTerminalIntent } from './types.js';
4
+ export interface TaskTerminalInput {
5
+ taskId: string;
6
+ kind: TaskTerminalIntent['kind'];
7
+ roomId?: string;
8
+ outcome?: TaskOutcome;
9
+ }
10
+ /** Persist first-wins terminal intent before any acknowledgement or close effect. */
11
+ export declare function acceptTaskTerminalIntent(input: TaskTerminalInput): Promise<TaskRecord>;
12
+ export interface SettleTaskTerminalDeps {
13
+ roomClose?: RoomCloseDeps;
14
+ afterRoomClosed?(): void | Promise<void>;
15
+ }
16
+ /** Close the recorded room, then atomically settle the persisted task intent. */
17
+ export declare function settleTaskTerminalIntent(input: {
18
+ taskId: string;
19
+ cowork: Pick<CoworkAdapter, 'closeRoom'>;
20
+ deps?: SettleTaskTerminalDeps;
21
+ }): Promise<TaskRecord>;
22
+ /** Persist failure to launch an external settle worker under the same task lock. */
23
+ export declare function recordTaskTerminalIntentError(taskId: string, error: string, recoveryHint: string): Promise<TaskRecord>;
@@ -0,0 +1,53 @@
1
+ import { join } from 'node:path';
2
+ import { withFileLock } from '../atomic-file.js';
3
+ import { stateRoot } from '../paths.js';
4
+ import { closeManagedRoom } from './close.js';
5
+ import { beginTaskTerminalIntent, finishTaskTerminalIntent, getTask, setTaskTerminalIntentError, } from './task-state.js';
6
+ const TASK_OPERATION_LOCK_STALE_MS = 10 * 60_000;
7
+ function taskOperationLockPath(taskId) {
8
+ return join(stateRoot(), 'locks', 'task-terminal', encodeURIComponent(taskId));
9
+ }
10
+ function errorText(error) {
11
+ return error instanceof Error ? error.message : String(error);
12
+ }
13
+ /** Persist first-wins terminal intent before any acknowledgement or close effect. */
14
+ export function acceptTaskTerminalIntent(input) {
15
+ return withFileLock(taskOperationLockPath(input.taskId), () => {
16
+ const accepted = beginTaskTerminalIntent(input.taskId, input);
17
+ if (!input.roomId)
18
+ return finishTaskTerminalIntent(input.taskId);
19
+ return accepted;
20
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
21
+ }
22
+ /** Close the recorded room, then atomically settle the persisted task intent. */
23
+ export function settleTaskTerminalIntent(input) {
24
+ return withFileLock(taskOperationLockPath(input.taskId), async () => {
25
+ const current = getTask(input.taskId);
26
+ const intent = current.terminal_intent;
27
+ if (!intent)
28
+ throw new Error(`task ${input.taskId} has no accepted terminal intent`);
29
+ if (intent.status === 'settled')
30
+ return current;
31
+ try {
32
+ if (intent.room_id) {
33
+ const room = await closeManagedRoom({
34
+ roomId: intent.room_id,
35
+ cowork: input.cowork,
36
+ deps: input.deps?.roomClose,
37
+ });
38
+ if (room.state !== 'closed')
39
+ throw new Error(`room ${intent.room_id} did not close`);
40
+ await input.deps?.afterRoomClosed?.();
41
+ }
42
+ return finishTaskTerminalIntent(input.taskId);
43
+ }
44
+ catch (error) {
45
+ setTaskTerminalIntentError(input.taskId, errorText(error), `Retry 'ours-fleet task recover ${input.taskId}'.`);
46
+ throw error;
47
+ }
48
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
49
+ }
50
+ /** Persist failure to launch an external settle worker under the same task lock. */
51
+ export function recordTaskTerminalIntentError(taskId, error, recoveryHint) {
52
+ return withFileLock(taskOperationLockPath(taskId), () => setTaskTerminalIntentError(taskId, error, recoveryHint), {}, TASK_OPERATION_LOCK_STALE_MS);
53
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Rooms & Tasks domain types for ours-fleet.
3
+ *
4
+ * Task and Room are Fleet orchestration records. Cowork owns room content,
5
+ * membership, and archive. Messenger Server owns owner presentation.
6
+ * Fleet stores only IDs, CIDs, orchestration state, and saga cursors.
7
+ */
8
+ export type TaskState = 'backlog' | 'provisioning' | 'active' | 'review' | 'done' | 'cancelled' | 'failed';
9
+ export declare const TASK_TERMINAL_STATES: readonly TaskState[];
10
+ export declare const TASK_CANCELLABLE_STATES: readonly TaskState[];
11
+ export interface TaskBlocked {
12
+ reason: string;
13
+ at: string;
14
+ }
15
+ export interface TaskTemplateRef {
16
+ name: string;
17
+ version: number;
18
+ content_hash: string;
19
+ }
20
+ export interface TaskMemberRole {
21
+ name: string;
22
+ identity_cid: string;
23
+ slot: string;
24
+ cowork_role: string;
25
+ }
26
+ export interface TaskOrigin {
27
+ type: 'cli' | 'owner_channel';
28
+ owner_cid?: string;
29
+ }
30
+ export interface TaskOutcome {
31
+ summary: string;
32
+ artifacts?: string[];
33
+ }
34
+ export interface TaskTerminalIntent {
35
+ kind: 'done' | 'cancelled';
36
+ status: 'pending' | 'settled';
37
+ room_id?: string;
38
+ outcome?: TaskOutcome;
39
+ accepted_at: string;
40
+ settled_at?: string;
41
+ error?: string;
42
+ error_at?: string;
43
+ recovery_hint?: string;
44
+ first_failure?: string;
45
+ first_recovery_hint?: string;
46
+ }
47
+ export interface TaskRecord {
48
+ task_id: string;
49
+ title: string;
50
+ brief?: string;
51
+ brief_file?: string;
52
+ state: TaskState;
53
+ blocked?: TaskBlocked;
54
+ template?: TaskTemplateRef;
55
+ no_room?: boolean;
56
+ room_id?: string;
57
+ room_identity_cid?: string;
58
+ member_roles: TaskMemberRole[];
59
+ origin: TaskOrigin;
60
+ idempotency_key: string;
61
+ created_at: string;
62
+ started_at?: string;
63
+ ended_at?: string;
64
+ outcome?: TaskOutcome;
65
+ terminal_intent?: TaskTerminalIntent;
66
+ }
67
+ export type RoomOrchestrationState = 'provisioning' | 'active' | 'closing' | 'closed';
68
+ export type SagaPhase = 'persist_intent' | 'create_room' | 'attach_owner' | 'create_members' | 'configure_briefings' | 'join_role_groups' | 'wait_seats' | 'launch_work' | 'wait_briefing_acks' | 'activate' | 'completed' | 'failed';
69
+ export interface SagaCursor {
70
+ phase: SagaPhase;
71
+ step_index: number;
72
+ error?: string;
73
+ recovery_hint?: string;
74
+ }
75
+ export type ProvisioningDetail = 'waiting_cowork' | 'waiting_owner_invite' | 'owner_cid_mismatch' | 'member_failed' | 'waiting_seats' | 'waiting_briefing_delivery' | 'waiting_briefing_acks' | 'briefing_delivery_failed' | 'uncertain';
76
+ export interface RoomRoleBriefingDefinition {
77
+ role: string;
78
+ text: string;
79
+ sha256: string;
80
+ version?: number;
81
+ state: 'pending' | 'configured' | 'failed';
82
+ attempts: number;
83
+ updated_at: string;
84
+ last_error?: string;
85
+ }
86
+ export interface RoomMemberLaunchState {
87
+ state: 'pending' | 'intent' | 'launched' | 'stopped' | 'failed';
88
+ attempt: number;
89
+ action_id?: string;
90
+ mission_sha256?: string;
91
+ launch_id?: string;
92
+ updated_at: string;
93
+ error?: string;
94
+ }
95
+ export interface RoomMemberBriefingState {
96
+ role: string;
97
+ state: 'pending' | 'relay_queued' | 'relay_failed' | 'acknowledged';
98
+ message_id?: string;
99
+ relay_intent_record_id?: string;
100
+ relay_result_record_id?: string;
101
+ relay_wire_id?: string;
102
+ checked_at?: string;
103
+ acknowledged_at?: string;
104
+ acknowledgement_message_id?: string;
105
+ acknowledgement_seq?: number;
106
+ rejected_ack_count: number;
107
+ last_rejected_ack_reason?: string;
108
+ last_rejected_ack_seq?: number;
109
+ last_processed_seq?: number;
110
+ }
111
+ export type RoomHistoryEvidence = {
112
+ kind: 'message';
113
+ seq: number;
114
+ record_id: string;
115
+ at: string;
116
+ message_id: string;
117
+ category: 'role_briefing' | 'chat';
118
+ author: {
119
+ identity: string;
120
+ display_name: string;
121
+ role: string;
122
+ };
123
+ text: string;
124
+ recipient_identities: string[];
125
+ briefing_role?: string;
126
+ briefing_version?: number;
127
+ } | {
128
+ kind: 'relay_intent';
129
+ seq: number;
130
+ record_id: string;
131
+ at: string;
132
+ message_id: string;
133
+ recipient_identity: string;
134
+ } | {
135
+ kind: 'relay_result';
136
+ seq: number;
137
+ record_id: string;
138
+ at: string;
139
+ intent_record_id: string;
140
+ message_id: string;
141
+ recipient_identity: string;
142
+ status: 'queued' | 'send_failed' | 'skipped_removed';
143
+ wire_id?: string;
144
+ };
145
+ export type MemberRetirementPhase = 'stop_requested' | 'liveness_absent' | 'archive_secured' | 'identity_absent';
146
+ export interface MemberRetirement {
147
+ phase: MemberRetirementPhase;
148
+ launch_id: string;
149
+ updated_at: string;
150
+ archive_path?: string;
151
+ }
152
+ export type RoomClosePhase = 'retire_members' | 'close_cowork' | 'completed';
153
+ export interface RoomCloseCursor {
154
+ phase: RoomClosePhase;
155
+ accepted_at: string;
156
+ error?: string;
157
+ error_at?: string;
158
+ recovery_hint?: string;
159
+ first_failure?: string;
160
+ first_recovery_hint?: string;
161
+ }
162
+ export interface RoomMemberSeat {
163
+ role_name: string;
164
+ identity_cid: string;
165
+ slot: string;
166
+ cowork_role: string;
167
+ seat_state: 'pending' | 'active' | 'removed';
168
+ launch?: RoomMemberLaunchState;
169
+ briefing?: RoomMemberBriefingState;
170
+ retirement?: MemberRetirement;
171
+ }
172
+ export interface RoomOrchestrationRecord {
173
+ room_id: string;
174
+ room_identity_cid?: string;
175
+ room_name: string;
176
+ goal?: string;
177
+ task_id?: string;
178
+ template_snapshot?: TemplateSnapshot;
179
+ saga: SagaCursor;
180
+ provisioning_detail?: ProvisioningDetail;
181
+ owner_seat_cid?: string;
182
+ owner_invite_fingerprint?: string;
183
+ role_briefings?: Record<string, RoomRoleBriefingDefinition>;
184
+ history_cursor?: number;
185
+ member_seats: RoomMemberSeat[];
186
+ state: RoomOrchestrationState;
187
+ created_at: string;
188
+ activated_at?: string;
189
+ closed_at?: string;
190
+ close?: RoomCloseCursor;
191
+ }
192
+ export interface TemplateMemberSlot {
193
+ slot: string;
194
+ role: string;
195
+ count: number;
196
+ role_ref: string;
197
+ overrides?: TemplateRoleOverrides;
198
+ }
199
+ export interface TemplateRoleOverrides {
200
+ harness?: string;
201
+ model?: string;
202
+ model_chain?: string[];
203
+ permissions?: Record<string, unknown>;
204
+ isolation?: Record<string, unknown>;
205
+ cwd?: string;
206
+ env?: Record<string, string>;
207
+ persona?: string;
208
+ mission?: string;
209
+ }
210
+ export interface TemplateRoomConfig {
211
+ quiet_membership?: boolean;
212
+ anonymous?: boolean;
213
+ }
214
+ export interface TemplateDefinition {
215
+ name: string;
216
+ version: number;
217
+ description: string;
218
+ builtin?: boolean;
219
+ room?: TemplateRoomConfig;
220
+ contract?: string;
221
+ members: TemplateMemberSlot[];
222
+ }
223
+ export interface TemplateSnapshot extends TemplateDefinition {
224
+ content_hash: string;
225
+ }
226
+ export interface RoomsOwnerConfig {
227
+ provider: string;
228
+ public_invite?: string;
229
+ public_invite_file?: string;
230
+ expected_cid: string;
231
+ role: string;
232
+ }
233
+ export interface RoomsCoworkConfig {
234
+ config?: string;
235
+ }
236
+ export interface RoomsDefaults {
237
+ template?: string;
238
+ attach_owner?: boolean;
239
+ close_when_task_done?: boolean;
240
+ }
241
+ export interface RoomsConfig {
242
+ cowork?: RoomsCoworkConfig;
243
+ owner: RoomsOwnerConfig;
244
+ defaults?: RoomsDefaults;
245
+ }
246
+ export interface TasksConfig {
247
+ default_room_template?: string;
248
+ create_mode?: 'start' | 'backlog';
249
+ close_room_on_done?: boolean;
250
+ retain_completed_for?: string;
251
+ }
252
+ export type RoomTemplatesConfig = Record<string, TemplateDefinition>;
253
+ export declare const ROOMS_KEYS: readonly ["cowork", "owner", "defaults"];
254
+ export declare const ROOMS_OWNER_KEYS: readonly ["provider", "public_invite", "public_invite_file", "expected_cid", "role"];
255
+ export declare const ROOMS_COWORK_KEYS: readonly ["config"];
256
+ export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
257
+ export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
258
+ export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
259
+ export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "role_ref", "overrides"];
260
+ export declare const TEMPLATE_OVERRIDE_KEYS: readonly ["harness", "model", "model_chain", "permissions", "isolation", "cwd", "env", "persona", "mission"];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Rooms & Tasks domain types for ours-fleet.
3
+ *
4
+ * Task and Room are Fleet orchestration records. Cowork owns room content,
5
+ * membership, and archive. Messenger Server owns owner presentation.
6
+ * Fleet stores only IDs, CIDs, orchestration state, and saga cursors.
7
+ */
8
+ export const TASK_TERMINAL_STATES = ['done', 'cancelled', 'failed'];
9
+ export const TASK_CANCELLABLE_STATES = ['backlog', 'provisioning', 'active', 'review'];
10
+ // ── Validation keys ─────────────────────────────────────────────────────
11
+ export const ROOMS_KEYS = ['cowork', 'owner', 'defaults'];
12
+ export const ROOMS_OWNER_KEYS = ['provider', 'public_invite', 'public_invite_file', 'expected_cid', 'role'];
13
+ export const ROOMS_COWORK_KEYS = ['config'];
14
+ export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
15
+ export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
16
+ export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
17
+ export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'role_ref', 'overrides'];
18
+ export const TEMPLATE_OVERRIDE_KEYS = [
19
+ 'harness', 'model', 'model_chain', 'permissions', 'isolation', 'cwd', 'env', 'persona', 'mission',
20
+ ];
package/dist/spawn.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { IsolationConfig } from './isolation/types.js';
2
- import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type RoleConfig, type MonitorConfig, type SessionBackendId, type UnattendedMode } from './config.js';
2
+ import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type RoleConfig, type MonitorConfig, type SessionBackendId, type UnattendedMode, type RoomStartupGate } from './config.js';
3
3
  import { type OpsDeps } from './ops.js';
4
4
  import { type CreationDeps, type CreationProvenance } from './creation.js';
5
5
  import './harness/claude-code.js';
@@ -46,6 +46,8 @@ export interface SpawnOpts {
46
46
  creationActionId?: string;
47
47
  /** Set only by a live role supervisor after a role-scoped proxy request. */
48
48
  callerRole?: string;
49
+ /** Trusted Fleet-internal gate for a Cowork room member; not a CLI/config surface. */
50
+ roomStartupGate?: RoomStartupGate;
49
51
  /** Internal provenance labels for values filled by the caller's supervisor. */
50
52
  inheritedFromCaller?: string[];
51
53
  /**
package/dist/spawn.js CHANGED
@@ -429,6 +429,7 @@ async function spawnTempInner(o, binPath, launch, tx, guarantee, onStage) {
429
429
  worklog: resolveWorklogPolicy(cfg.defaults.worklog, fromOpts.worklog),
430
430
  auth_proxy: resolveAuthProxy(cfg.defaults.auth_proxy, fromOpts.auth_proxy),
431
431
  sourceFile: '(temp)',
432
+ roomStartupGate: o.roomStartupGate,
432
433
  };
433
434
  role.env = {
434
435
  ...(cfg.defaults.env ?? {}),
@@ -46,11 +46,24 @@ export declare function markTempSupervisorActive(dir: string, pid?: number): Pro
46
46
  export declare function requestedTempStopReason(dir: string): 'operator-stop' | undefined;
47
47
  /** Move retired state out of the live roster without deleting any evidence. */
48
48
  export declare function archiveTempState(role: string, reason: TempTerminationReason, outcome: TempTerminationRecord['outcome'], detail: string, now?: Date): string | undefined;
49
- interface TempLifecycleDeps {
49
+ /** Resolve only an exact role+launch archive; names and timestamps are never proof. */
50
+ export declare function tempArchiveForLaunch(role: string, launchId: string): string | undefined;
51
+ /** Resolve one terminated archive by exact role + durable creation action. */
52
+ export declare function tempArchiveForCreationAction(role: string, creationActionId: string): {
53
+ path: string;
54
+ launchId: string;
55
+ } | undefined;
56
+ /**
57
+ * Preserve a definitively stopped launch as recovery evidence, or prove that
58
+ * its supervisor already did so. Never creates a second archive.
59
+ */
60
+ export declare function secureStoppedTempArchive(role: string, launchId: string, deps?: TempLifecycleDeps): Promise<string>;
61
+ export interface TempLifecycleDeps {
50
62
  exec?: Exec;
51
63
  now?(): number;
52
64
  kill?(pid: number, signal: NodeJS.Signals | 0): void;
53
65
  log?(line: string): void;
66
+ sleep?(ms: number): Promise<void>;
54
67
  }
55
68
  export declare function tempSupervisorLiveness(dir: string, deps?: TempLifecycleDeps): Promise<'running' | 'stopped' | 'unknown'>;
56
69
  export declare function stopTempSupervisor(role: string, deps?: TempLifecycleDeps): Promise<'stopped' | 'already-stopped'>;
@@ -59,4 +72,3 @@ export declare function stopTempSupervisor(role: string, deps?: TempLifecycleDep
59
72
  * Unknown/legacy/live entries are preserved; absence of proof is never cleanup authority.
60
73
  */
61
74
  export declare function reclaimStaleTempState(deps?: TempLifecycleDeps): Promise<string[]>;
62
- export {};
@@ -191,6 +191,96 @@ export function archiveTempState(role, reason, outcome, detail, now = new Date()
191
191
  }
192
192
  return paths.target;
193
193
  }
194
+ function isArchiveForLaunch(path, role, launchId) {
195
+ const supervisor = readTempSupervisor(path);
196
+ if (supervisor?.role !== role || supervisor.launchId !== launchId)
197
+ return false;
198
+ try {
199
+ return readFileSync(join(path, TEMP_TERMINATION_FILE), 'utf8')
200
+ .split('\n')
201
+ .filter(Boolean)
202
+ .some(line => {
203
+ const record = JSON.parse(line);
204
+ return record.version === 1 && record.role === role && record.launchId === launchId;
205
+ });
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ /** Resolve only an exact role+launch archive; names and timestamps are never proof. */
212
+ export function tempArchiveForLaunch(role, launchId) {
213
+ let entries;
214
+ try {
215
+ entries = readdirSync(archiveRoot(), { withFileTypes: true })
216
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.startsWith('.'))
217
+ .map(entry => join(archiveRoot(), entry.name));
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ const matches = entries.filter(path => isArchiveForLaunch(path, role, launchId));
223
+ if (matches.length > 1) {
224
+ throw new Error(`temporary role '${role}' launch ${launchId} has ${matches.length} recovery archives; refusing ambiguity`);
225
+ }
226
+ return matches[0];
227
+ }
228
+ /** Resolve one terminated archive by exact role + durable creation action. */
229
+ export function tempArchiveForCreationAction(role, creationActionId) {
230
+ let entries;
231
+ try {
232
+ entries = readdirSync(archiveRoot(), { withFileTypes: true })
233
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.startsWith('.'))
234
+ .map(entry => join(archiveRoot(), entry.name));
235
+ }
236
+ catch {
237
+ return undefined;
238
+ }
239
+ const matches = entries.flatMap(path => {
240
+ const supervisor = readTempSupervisor(path);
241
+ if (!supervisor || supervisor.role !== role || !isArchiveForLaunch(path, role, supervisor.launchId))
242
+ return [];
243
+ try {
244
+ const provenance = JSON.parse(readFileSync(join(path, 'creation.json'), 'utf8'));
245
+ return provenance.role === role && provenance.creationActionId === creationActionId
246
+ ? [{ path, launchId: supervisor.launchId }] : [];
247
+ }
248
+ catch {
249
+ return [];
250
+ }
251
+ });
252
+ if (matches.length > 1) {
253
+ throw new Error(`temporary role '${role}' action ${creationActionId} has ${matches.length} recovery archives; refusing ambiguity`);
254
+ }
255
+ return matches[0];
256
+ }
257
+ /**
258
+ * Preserve a definitively stopped launch as recovery evidence, or prove that
259
+ * its supervisor already did so. Never creates a second archive.
260
+ */
261
+ export async function secureStoppedTempArchive(role, launchId, deps = {}) {
262
+ const dir = join(tmpRoot(), role);
263
+ if (existsSync(dir)) {
264
+ const supervisor = readTempSupervisor(dir);
265
+ if (!supervisor || supervisor.role !== role || supervisor.launchId !== launchId) {
266
+ throw new Error(`temporary role '${role}' no longer matches recorded launch ${launchId}; refusing to archive`);
267
+ }
268
+ const live = await tempSupervisorLiveness(dir, deps);
269
+ if (live !== 'stopped') {
270
+ throw new Error(`temporary role '${role}' supervisor is ${live}; refusing to archive before proven stop`);
271
+ }
272
+ const archived = archiveTempState(role, 'operator-stop', 'retired', 'room close proved the supervisor stopped; full role evidence preserved');
273
+ if (!archived || !isArchiveForLaunch(archived, role, launchId)) {
274
+ throw new Error(`temporary role '${role}' archive could not be verified for launch ${launchId}`);
275
+ }
276
+ return archived;
277
+ }
278
+ const archived = tempArchiveForLaunch(role, launchId);
279
+ if (!archived) {
280
+ throw new Error(`temporary role '${role}' live state disappeared without an exact recovery archive for launch ${launchId}`);
281
+ }
282
+ return archived;
283
+ }
194
284
  /** Finish bounded archive renames interrupted by process or host termination. */
195
285
  function recoverInterruptedArchives(now) {
196
286
  let names;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.19.0-nightly.1",
3
+ "version": "0.19.0-nightly.11",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",