@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21

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 (202) hide show
  1. package/README.md +204 -55
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +6 -25
  8. package/dist/application/role-creation-service.js +34 -72
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +58 -6
  14. package/dist/application/task-room-service.js +435 -79
  15. package/dist/briefing.js +74 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +241 -76
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +75 -3
  22. package/dist/config.js +274 -34
  23. package/dist/creation.d.ts +1 -1
  24. package/dist/creation.js +3 -2
  25. package/dist/daemon-recovery.d.ts +93 -0
  26. package/dist/daemon-recovery.js +328 -0
  27. package/dist/docs.d.ts +1 -1
  28. package/dist/docs.js +192 -66
  29. package/dist/doctor.d.ts +2 -0
  30. package/dist/doctor.js +54 -4
  31. package/dist/fleet-command-audit.d.ts +165 -0
  32. package/dist/fleet-command-audit.js +555 -0
  33. package/dist/fleet-proxy.d.ts +7 -2
  34. package/dist/fleet-proxy.js +30 -8
  35. package/dist/harness/agent-session.d.ts +5 -0
  36. package/dist/harness/claude-code-session.d.ts +2 -0
  37. package/dist/harness/claude-code-session.js +6 -1
  38. package/dist/harness/claude-code.js +2 -0
  39. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  40. package/dist/harness/codex-app-server-proxy.js +348 -14
  41. package/dist/harness/codex-session.d.ts +11 -2
  42. package/dist/harness/codex-session.js +22 -2
  43. package/dist/harness/codex.d.ts +5 -1
  44. package/dist/harness/codex.js +110 -10
  45. package/dist/harness/types.d.ts +1 -1
  46. package/dist/index.d.ts +5 -4
  47. package/dist/index.js +4 -3
  48. package/dist/init-guidance.d.ts +1 -1
  49. package/dist/init-guidance.js +4 -3
  50. package/dist/init-wizard.d.ts +98 -0
  51. package/dist/init-wizard.js +629 -0
  52. package/dist/lifecycle-summary.d.ts +134 -0
  53. package/dist/lifecycle-summary.js +179 -0
  54. package/dist/loops/config.d.ts +10 -1
  55. package/dist/loops/config.js +38 -5
  56. package/dist/model-env.d.ts +2 -2
  57. package/dist/model-env.js +1 -1
  58. package/dist/model-recovery.d.ts +1 -1
  59. package/dist/ops.js +2 -17
  60. package/dist/owner-channel/channel.d.ts +58 -0
  61. package/dist/owner-channel/channel.js +321 -42
  62. package/dist/owner-channel/commands.d.ts +6 -1
  63. package/dist/owner-channel/commands.js +34 -19
  64. package/dist/owner-channel/state.d.ts +2 -0
  65. package/dist/owner-channel/state.js +8 -2
  66. package/dist/permissions.js +1 -1
  67. package/dist/preset-bootstrap.d.ts +10 -0
  68. package/dist/preset-bootstrap.js +85 -0
  69. package/dist/preset-migration.d.ts +42 -0
  70. package/dist/preset-migration.js +414 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.js +13 -1
  84. package/dist/rooms-tasks/cli.d.ts +2 -0
  85. package/dist/rooms-tasks/cli.js +514 -75
  86. package/dist/rooms-tasks/close.d.ts +9 -1
  87. package/dist/rooms-tasks/close.js +20 -4
  88. package/dist/rooms-tasks/config.js +16 -14
  89. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  90. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  91. package/dist/rooms-tasks/deletion.d.ts +51 -0
  92. package/dist/rooms-tasks/deletion.js +217 -0
  93. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  94. package/dist/rooms-tasks/external-worker.js +14 -9
  95. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  96. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  97. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  98. package/dist/rooms-tasks/member-overrides.js +204 -0
  99. package/dist/rooms-tasks/provision.js +184 -49
  100. package/dist/rooms-tasks/room-state.d.ts +1 -0
  101. package/dist/rooms-tasks/room-state.js +12 -1
  102. package/dist/rooms-tasks/task-state.d.ts +115 -3
  103. package/dist/rooms-tasks/task-state.js +361 -11
  104. package/dist/rooms-tasks/templates.d.ts +4 -2
  105. package/dist/rooms-tasks/templates.js +42 -78
  106. package/dist/rooms-tasks/terminal.d.ts +3 -0
  107. package/dist/rooms-tasks/terminal.js +7 -3
  108. package/dist/rooms-tasks/types.d.ts +96 -17
  109. package/dist/rooms-tasks/types.js +14 -4
  110. package/dist/runner.d.ts +12 -2
  111. package/dist/runner.js +124 -43
  112. package/dist/session/acp.d.ts +9 -1
  113. package/dist/session/acp.js +35 -9
  114. package/dist/session/arbiter.d.ts +2 -1
  115. package/dist/session/arbiter.js +2 -0
  116. package/dist/session/codex-app-server-transport.d.ts +53 -0
  117. package/dist/session/codex-app-server-transport.js +184 -0
  118. package/dist/session/codex-app-server.d.ts +122 -0
  119. package/dist/session/codex-app-server.js +1031 -0
  120. package/dist/session/control.d.ts +14 -1
  121. package/dist/session/control.js +38 -0
  122. package/dist/session/conversation-types.d.ts +4 -4
  123. package/dist/session/types.d.ts +24 -6
  124. package/dist/session/types.js +28 -0
  125. package/dist/spawn.d.ts +15 -31
  126. package/dist/spawn.js +117 -237
  127. package/dist/temp-lifecycle.d.ts +1 -1
  128. package/dist/watchdog/config.d.ts +10 -7
  129. package/dist/watchdog/config.js +29 -19
  130. package/dist/watchdog/run.js +4 -13
  131. package/dist/web/fleet-config-service.d.ts +1 -0
  132. package/dist/web/fleet-config-service.js +27 -8
  133. package/dist/web/runtime.js +11 -6
  134. package/dist/web/server.js +28 -1
  135. package/dist/web/topology-promote.js +8 -1
  136. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  137. package/dist/web-app/index.html +1 -1
  138. package/package.json +3 -2
  139. package/presets/brain-catalog.json +16 -0
  140. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  141. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  142. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  143. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  144. package/presets/fleet/brains/claude-default.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  147. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  148. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  149. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  152. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  153. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  154. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  157. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  158. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  159. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  190. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  191. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  192. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  193. package/presets/fleet/roles/Coordinator.yaml +30 -0
  194. package/presets/fleet/roles/Critic.yaml +19 -0
  195. package/presets/fleet/roles/Developer.yaml +19 -0
  196. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  197. package/presets/fleet/room_templates/pair.yaml +10 -0
  198. package/presets/fleet/room_templates/single.yaml +7 -0
  199. package/presets/fleet/room_templates/team.yaml +12 -0
  200. package/presets/fleet.yaml +7 -0
  201. package/presets/manifest.json +6 -0
  202. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -1,5 +1,6 @@
1
1
  import { withFileLock } from '../atomic-file.js';
2
- import type { CoworkAdapter } from './cowork-adapter.js';
2
+ import { type TempLifecycleDeps } from '../temp-lifecycle.js';
3
+ import { type CoworkAdapter } from './cowork-adapter.js';
3
4
  import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
4
5
  export interface RoomCloseDeps {
5
6
  inspectMember?(seat: RoomMemberSeat): Promise<{
@@ -11,6 +12,13 @@ export interface RoomCloseDeps {
11
12
  removeIdentity?(seat: RoomMemberSeat): Promise<void>;
12
13
  lock?: typeof withFileLock;
13
14
  }
15
+ export declare function inspectMember(seat: RoomMemberSeat): Promise<{
16
+ launchId: string;
17
+ }>;
18
+ export declare function waitForLivenessAbsent(role: string, launchId: string, lifecycleDeps?: TempLifecycleDeps): Promise<void>;
19
+ /** Report whether any daemon identity — under any name — carries this exact CID. */
20
+ export declare function identityCidPresent(cid: string): Promise<boolean>;
21
+ export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
14
22
  /** One forward-only room close saga shared by every Fleet entry point. */
15
23
  export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
16
24
  export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
@@ -5,6 +5,7 @@ import { attachOursClient } from '@ours.network/sdk/client';
5
5
  import { withFileLock } from '../atomic-file.js';
6
6
  import { agentDir, stateRoot } from '../paths.js';
7
7
  import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
8
+ import { CoworkProtocolError } from './cowork-adapter.js';
8
9
  import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
9
10
  const CLOSE_LOCK_STALE_MS = 5 * 60_000;
10
11
  const STOP_POLLS = 50;
@@ -26,7 +27,7 @@ function exactMemberIdentity(seat) {
26
27
  throw new Error(`room member '${seat.role_name}' temp state binds identity '${identity}'; refusing mismatched retirement`);
27
28
  }
28
29
  }
29
- async function inspectMember(seat) {
30
+ export async function inspectMember(seat) {
30
31
  exactMemberIdentity(seat);
31
32
  const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
32
33
  if (!supervisor || supervisor.role !== seat.role_name) {
@@ -34,7 +35,7 @@ async function inspectMember(seat) {
34
35
  }
35
36
  return { launchId: supervisor.launchId };
36
37
  }
37
- async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {}) {
38
+ export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {}) {
38
39
  const dir = agentDir(role, true);
39
40
  const sleep = lifecycleDeps.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
40
41
  for (let attempt = 0; attempt < STOP_POLLS; attempt++) {
@@ -68,7 +69,14 @@ async function withIdentityClient(work) {
68
69
  function listedIdentity(rows, name) {
69
70
  return rows.find(row => row.name === name);
70
71
  }
71
- async function removeExactMemberIdentity(seat) {
72
+ /** Report whether any daemon identity — under any name — carries this exact CID. */
73
+ export async function identityCidPresent(cid) {
74
+ return withIdentityClient(async (client) => {
75
+ const rows = await client.listIdentities();
76
+ return rows.some(row => row.cid?.toLowerCase() === cid.toLowerCase());
77
+ });
78
+ }
79
+ export async function removeExactMemberIdentity(seat) {
72
80
  await withIdentityClient(async (client) => {
73
81
  const before = listedIdentity(await client.listIdentities(), seat.role_name);
74
82
  if (!before)
@@ -172,7 +180,15 @@ export async function closeManagedRoom(input) {
172
180
  export async function deleteLegacyClosedRooms(input) {
173
181
  const deleted = [];
174
182
  for (const room of listRoomRecords({ state: 'closed' })) {
175
- await input.cowork.deleteRoom(room.room_id);
183
+ try {
184
+ await input.cowork.deleteRoom(room.room_id);
185
+ }
186
+ catch (error) {
187
+ // The retained Fleet record is the legacy state being migrated. If the
188
+ // exact Cowork room is already absent, the desired deletion is complete.
189
+ if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
190
+ throw error;
191
+ }
176
192
  deleteRoomRecord(room.room_id);
177
193
  deleted.push(room.room_id);
178
194
  }
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
- import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, TEMPLATE_OVERRIDE_KEYS as TOK, } from './types.js';
4
- import { BUILTIN_TEMPLATES } from './templates.js';
3
+ import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, TEMPLATE_ROOM_KEYS as TRK, } from './types.js';
5
4
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
6
5
  const CID_RE = /^[0-9a-fA-F]{64}$/;
7
6
  export class RoomsTasksConfigError extends Error {
@@ -116,9 +115,6 @@ export function validateRoomTemplatesConfig(raw, path) {
116
115
  if (!isPlainObject(tplRaw))
117
116
  throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
118
117
  rejectUnknown(tplRaw, [...TPK, 'name'], path, `room_templates.${name}`);
119
- const isBuiltinOverride = BUILTIN_TEMPLATES.some(b => b.name === name);
120
- if (isBuiltinOverride && !tplRaw.override_builtin)
121
- throw new RoomsTasksConfigError(path, `room_templates.${name}: overrides built-in template; set override_builtin: true and bump version`);
122
118
  const version = tplRaw.version;
123
119
  if (version === undefined || !Number.isInteger(version) || version < 1)
124
120
  throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
@@ -136,25 +132,31 @@ export function validateRoomTemplatesConfig(raw, path) {
136
132
  throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
137
133
  if (!Number.isInteger(m.count) || m.count < 1)
138
134
  throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
139
- if (!m.role_ref || typeof m.role_ref !== 'string')
140
- throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role_ref: required string`);
141
- if (m.overrides !== undefined) {
142
- if (!isPlainObject(m.overrides))
143
- throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].overrides: must be a mapping`);
144
- rejectUnknown(m.overrides, TOK, path, `room_templates.${name}.members[${i}].overrides`);
145
- }
135
+ if (m.agent !== undefined)
136
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: persistent Agent references are no longer allowed; use agent_template: <ID>`);
137
+ if (typeof m.agent_template !== 'string' || !m.agent_template.trim())
138
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent_template: required non-blank Agent Template ID`);
146
139
  return {
147
140
  slot: m.slot,
148
141
  role: m.role,
149
142
  count: m.count,
150
- role_ref: m.role_ref,
151
- overrides: m.overrides,
143
+ agent_template: m.agent_template,
152
144
  };
153
145
  });
146
+ const slots = new Set();
147
+ for (const member of members) {
148
+ if (slots.has(member.slot))
149
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members: duplicate slot '${member.slot}'`);
150
+ slots.add(member.slot);
151
+ }
154
152
  let room;
155
153
  if (tplRaw.room !== undefined) {
156
154
  if (!isPlainObject(tplRaw.room))
157
155
  throw new RoomsTasksConfigError(path, `room_templates.${name}.room: must be a mapping`);
156
+ rejectUnknown(tplRaw.room, TRK, path, `room_templates.${name}.room`);
157
+ for (const key of TRK)
158
+ if (tplRaw.room[key] !== undefined && typeof tplRaw.room[key] !== 'boolean')
159
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.room.${key}: must be a boolean`);
158
160
  room = {
159
161
  quiet_membership: tplRaw.room.quiet_membership,
160
162
  anonymous: tplRaw.room.anonymous,
@@ -33,6 +33,7 @@ export interface CoworkRoomInfo {
33
33
  identity_cid: string;
34
34
  room_name: string;
35
35
  state: 'provisioning' | 'active' | 'closing' | 'closed';
36
+ anonymous: boolean;
36
37
  seats: CoworkSeatInfo[];
37
38
  goal?: string;
38
39
  briefing?: string;
@@ -47,6 +47,11 @@ function roomState(value, operation) {
47
47
  throw new CoworkProtocolError(operation, 'room state is invalid');
48
48
  return value;
49
49
  }
50
+ function boolean(value, operation, label) {
51
+ if (typeof value !== 'boolean')
52
+ throw new CoworkProtocolError(operation, `${label} must be a boolean`);
53
+ return value;
54
+ }
50
55
  function seatState(value, operation) {
51
56
  if (value !== 'pending' && value !== 'active' && value !== 'removed')
52
57
  throw new CoworkProtocolError(operation, 'seat state is invalid');
@@ -86,6 +91,7 @@ function projectRoom(value, operation) {
86
91
  identity_cid: text(room.identity_cid, operation, 'room.identity_cid'),
87
92
  room_name: string(room.room_name, operation, 'room.room_name'),
88
93
  state: roomState(room.state, operation),
94
+ anonymous: room.anonymous === undefined ? false : boolean(room.anonymous, operation, 'room.anonymous'),
89
95
  seats: room.seats.map((seat) => projectSeat(seat, operation)),
90
96
  role_briefings: projectedBriefings,
91
97
  ...(typeof mission?.goal === 'string' ? { goal: mission.goal } : {}),
@@ -0,0 +1,51 @@
1
+ import { type RoomCloseDeps } from './close.js';
2
+ import { type CoworkAdapter } from './cowork-adapter.js';
3
+ import { type TaskDeletionAcceptance } from './task-state.js';
4
+ import type { TaskDeletionActor, TaskRecord } from './types.js';
5
+ export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
6
+ type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'>;
7
+ export interface TaskDeletionSettleDeps {
8
+ roomClose?: RoomCloseDeps;
9
+ /** Test seam for the temp-state existence proof. */
10
+ hasTempState?(name: string): boolean;
11
+ /** Test seam for the CID-wide daemon identity scan. */
12
+ identityCidPresent?(cid: string): Promise<boolean>;
13
+ }
14
+ export interface TaskDeletionSettleResult {
15
+ task_id: string;
16
+ deleted: boolean;
17
+ /** Lifecycle state observed before unlink, for completion audit. */
18
+ previous_state?: TaskRecord['state'];
19
+ /** Title observed before unlink, for completion audit. */
20
+ title?: string;
21
+ }
22
+ /**
23
+ * Accept a permanent task deletion under the common task-operation lock so it
24
+ * serializes against terminal settlement, recovery, and the room-publication
25
+ * window. Acceptance is durable before any side effect and never requires
26
+ * Cowork availability; settlement runs separately and converges through
27
+ * retries.
28
+ */
29
+ export declare function acceptTaskDeletion(taskId: string, actor: TaskDeletionActor): Promise<TaskDeletionAcceptance>;
30
+ /** Persist a settlement failure under the same task-operation lock. */
31
+ export declare function recordTaskDeletionError(taskId: string, error: string, recoveryHint: string): Promise<TaskRecord>;
32
+ /**
33
+ * Converge an accepted deletion to physical absence in two lock-ordered
34
+ * stages. Cleanup stage (task-operation lock): per surviving room record,
35
+ * register late seats → close saga (retires members) → durable evidence
36
+ * checkpoint → tolerant remote delete → local record delete; then
37
+ * cursor-based retirement for members with no room record and tolerant remote
38
+ * close+delete of a recorded room whose local record is gone. Finalization
39
+ * stage (launch-snapshot lock → task-operation lock, matching the global
40
+ * provisioning order): re-verify quiescence, delete the owned launch snapshot
41
+ * while excluding only this deleting task from the reference scan, and unlink
42
+ * the task record LAST — every crash seam leaves the hidden intent
43
+ * recoverable. Failures are recorded on the intent; the task stays hidden and
44
+ * recoverable, never falsely settled.
45
+ */
46
+ export declare function settleTaskDeletion(input: {
47
+ taskId: string;
48
+ /** Lazy: resolved only when room work exists, so no-room tasks settle without Cowork config. */
49
+ cowork: () => DeletionCowork;
50
+ deps?: TaskDeletionSettleDeps;
51
+ }): Promise<TaskDeletionSettleResult>;
@@ -0,0 +1,217 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { withFileLock } from '../atomic-file.js';
3
+ import { agentDir } from '../paths.js';
4
+ import { secureStoppedTempArchive, stopTempSupervisor } from '../temp-lifecycle.js';
5
+ import { closeManagedRoom, identityCidPresent, inspectMember, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
6
+ import { CoworkProtocolError } from './cowork-adapter.js';
7
+ import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.js';
8
+ import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
9
+ import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
10
+ import { advanceTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
11
+ export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
12
+ import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
13
+ /**
14
+ * Accept a permanent task deletion under the common task-operation lock so it
15
+ * serializes against terminal settlement, recovery, and the room-publication
16
+ * window. Acceptance is durable before any side effect and never requires
17
+ * Cowork availability; settlement runs separately and converges through
18
+ * retries.
19
+ */
20
+ export function acceptTaskDeletion(taskId, actor) {
21
+ return withFileLock(taskOperationLockPath(taskId), () => beginTaskDeletionIntent(taskId, actor), {}, TASK_OPERATION_LOCK_STALE_MS);
22
+ }
23
+ /** Persist a settlement failure under the same task-operation lock. */
24
+ export function recordTaskDeletionError(taskId, error, recoveryHint) {
25
+ return withFileLock(taskOperationLockPath(taskId), () => setTaskDeletionError(taskId, error, recoveryHint), {}, TASK_OPERATION_LOCK_STALE_MS);
26
+ }
27
+ function errorText(error) {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+ /** Already-missing remote rooms are a settled outcome, not a failure. */
31
+ function tolerantCowork(cowork) {
32
+ const tolerate = async (work) => {
33
+ try {
34
+ await work();
35
+ }
36
+ catch (error) {
37
+ if (error instanceof CoworkProtocolError && error.code === 'not_found')
38
+ return;
39
+ throw error;
40
+ }
41
+ };
42
+ return {
43
+ closeRoom: roomId => tolerate(() => cowork.closeRoom(roomId)),
44
+ deleteRoom: roomId => tolerate(() => cowork.deleteRoom(roomId)),
45
+ };
46
+ }
47
+ function cursorSeat(cursor) {
48
+ return {
49
+ role_name: cursor.name, identity_cid: cursor.identity_cid,
50
+ slot: cursor.name, cowork_role: 'member', seat_state: 'active',
51
+ };
52
+ }
53
+ /**
54
+ * Retire one member from its durable deletion cursor when no room record
55
+ * carries the seat: the same evidence chain as close.ts, resumed at the
56
+ * recorded phase. A member with no temp state is verified absent through the
57
+ * CID-wide identity scan; a same-name stranger identity is never touched, and
58
+ * the recorded CID surviving under any name blocks settlement.
59
+ */
60
+ async function retireCursorMember(taskId, cursor, deps) {
61
+ let phase = cursor.phase;
62
+ let launchId = cursor.launch_id;
63
+ if (phase === 'identity_absent')
64
+ return;
65
+ const seat = cursorSeat(cursor);
66
+ const roomClose = deps.roomClose ?? {};
67
+ if (phase === 'pending') {
68
+ const hasState = (deps.hasTempState ?? (name => existsSync(agentDir(name, true))))(cursor.name);
69
+ if (!hasState) {
70
+ const present = await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid);
71
+ if (present) {
72
+ // Remove only an exact name+CID match; a stranger under the same name
73
+ // stays untouched and removeExactMemberIdentity refuses the mismatch.
74
+ // The recorded CID surviving under a different name throws below on
75
+ // its post-removal verification and blocks settlement.
76
+ await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
77
+ if (await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid))
78
+ throw new TaskStateError(`deletion member '${cursor.name}' identity CID survives under another name; refusing to claim retirement`);
79
+ }
80
+ advanceTaskDeletionMember(taskId, cursor.name, 'identity_absent', DELETION_MEMBER_ABSENT_VERIFIED);
81
+ return;
82
+ }
83
+ const ownership = await (roomClose.inspectMember ?? inspectMember)(seat);
84
+ advanceTaskDeletionMember(taskId, cursor.name, 'stop_requested', ownership.launchId);
85
+ phase = 'stop_requested';
86
+ launchId = ownership.launchId;
87
+ }
88
+ if (phase === 'stop_requested') {
89
+ await (roomClose.requestStop
90
+ ?? (async (role) => { await stopTempSupervisor(role); }))(cursor.name);
91
+ await (roomClose.waitForLivenessAbsent ?? waitForLivenessAbsent)(cursor.name, launchId);
92
+ advanceTaskDeletionMember(taskId, cursor.name, 'liveness_absent');
93
+ phase = 'liveness_absent';
94
+ }
95
+ if (phase === 'liveness_absent') {
96
+ const archivePath = await (roomClose.secureArchive ?? secureStoppedTempArchive)(cursor.name, launchId);
97
+ advanceTaskDeletionMember(taskId, cursor.name, 'archive_secured', undefined, archivePath);
98
+ phase = 'archive_secured';
99
+ }
100
+ if (phase === 'archive_secured') {
101
+ await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
102
+ advanceTaskDeletionMember(taskId, cursor.name, 'identity_absent');
103
+ }
104
+ }
105
+ /**
106
+ * Converge an accepted deletion to physical absence in two lock-ordered
107
+ * stages. Cleanup stage (task-operation lock): per surviving room record,
108
+ * register late seats → close saga (retires members) → durable evidence
109
+ * checkpoint → tolerant remote delete → local record delete; then
110
+ * cursor-based retirement for members with no room record and tolerant remote
111
+ * close+delete of a recorded room whose local record is gone. Finalization
112
+ * stage (launch-snapshot lock → task-operation lock, matching the global
113
+ * provisioning order): re-verify quiescence, delete the owned launch snapshot
114
+ * while excluding only this deleting task from the reference scan, and unlink
115
+ * the task record LAST — every crash seam leaves the hidden intent
116
+ * recoverable. Failures are recorded on the intent; the task stays hidden and
117
+ * recoverable, never falsely settled.
118
+ */
119
+ export async function settleTaskDeletion(input) {
120
+ const { taskId } = input;
121
+ const deps = input.deps ?? {};
122
+ const recoveryHint = `Re-run 'ours-fleet task delete ${taskId} ${taskId}' or 'ours-fleet task recover ${taskId}'.`;
123
+ const cleanup = await withFileLock(taskOperationLockPath(taskId), async () => {
124
+ let task;
125
+ try {
126
+ task = getDeletingTask(taskId);
127
+ }
128
+ catch (error) {
129
+ if (error instanceof TaskStateError && /task not found/.test(error.message))
130
+ return { kind: 'already_absent' };
131
+ throw error;
132
+ }
133
+ if (task.deletion?.status !== 'pending')
134
+ throw new TaskStateError(`task ${taskId} has no pending deletion`);
135
+ try {
136
+ // Audit evidence precedes every side effect; a receipt write failure
137
+ // aborts settlement (fail closed).
138
+ ensureTaskDeletionReceipt(taskId);
139
+ const records = listRoomRecords().filter(room => room.task_id === taskId);
140
+ const recordIds = new Set(records.map(room => room.room_id));
141
+ const recordedRoomId = task.deletion.room_id ?? task.room_id;
142
+ const needsCowork = records.length > 0 || recordedRoomId !== undefined;
143
+ const cowork = needsCowork ? tolerantCowork(input.cowork()) : undefined;
144
+ for (const record of records) {
145
+ upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
146
+ await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
147
+ const closed = getRoomRecord(record.room_id);
148
+ importTaskDeletionRetirementEvidence(taskId, closed?.member_seats ?? record.member_seats);
149
+ await cowork.deleteRoom(record.room_id);
150
+ deleteRoomRecord(record.room_id);
151
+ }
152
+ // Members whose room record is gone (crash after record deletion, or
153
+ // legacy state): resume from the durable cursors.
154
+ for (const cursor of getDeletingTask(taskId).deletion.members) {
155
+ await retireCursorMember(taskId, cursor, deps);
156
+ }
157
+ // A recorded room without a local record may still be live remotely.
158
+ if (recordedRoomId && !recordIds.has(recordedRoomId) && !getRoomRecord(recordedRoomId)) {
159
+ await cowork.closeRoom(recordedRoomId);
160
+ await cowork.deleteRoom(recordedRoomId);
161
+ }
162
+ return {
163
+ kind: 'cleaned',
164
+ snapshotHash: task.execution_plan?.snapshot.launch_snapshot_hash,
165
+ previousState: task.state,
166
+ title: task.title,
167
+ };
168
+ }
169
+ catch (error) {
170
+ setTaskDeletionError(taskId, errorText(error), recoveryHint);
171
+ throw error;
172
+ }
173
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
174
+ if (cleanup.kind === 'already_absent') {
175
+ // Heal a crash between unlink and receipt completion.
176
+ completeTaskDeletionReceipt(taskId);
177
+ return { task_id: taskId, deleted: false };
178
+ }
179
+ const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
180
+ try {
181
+ const task = getDeletingTask(taskId);
182
+ if (task.deletion?.status !== 'pending')
183
+ throw new TaskStateError(`task ${taskId} has no pending deletion`);
184
+ if (listRoomRecords().some(room => room.task_id === taskId))
185
+ throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
186
+ if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
187
+ throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
188
+ if (cleanup.snapshotHash)
189
+ releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
190
+ unlinkDeletedTask(taskId);
191
+ }
192
+ catch (error) {
193
+ setTaskDeletionError(taskId, errorText(error), recoveryHint);
194
+ throw error;
195
+ }
196
+ // After unlink the task can no longer carry errors; receipt completion is
197
+ // idempotent and the already-absent retry path heals a crash here.
198
+ completeTaskDeletionReceipt(taskId);
199
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
200
+ if (cleanup.snapshotHash) {
201
+ // Global lock order: launch-snapshot lock → task-operation lock.
202
+ const releaseSnapshotLock = acquireLaunchSnapshotLock();
203
+ try {
204
+ await finalize();
205
+ }
206
+ finally {
207
+ releaseSnapshotLock();
208
+ }
209
+ }
210
+ else {
211
+ await finalize();
212
+ }
213
+ return {
214
+ task_id: taskId, deleted: true,
215
+ previous_state: cleanup.previousState, title: cleanup.title,
216
+ };
217
+ }
@@ -1,2 +1,3 @@
1
+ export declare function fleetWorkerEnv(source?: NodeJS.ProcessEnv): Record<string, string>;
1
2
  /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
2
3
  export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
@@ -1,14 +1,23 @@
1
1
  import { execFile, spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
- /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
4
- export function launchFleetWorker(args, operation, configPath) {
5
- const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
3
+ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV } from '../fleet-proxy.js';
4
+ export function fleetWorkerEnv(source = process.env) {
6
5
  const inheritedKeys = [
7
6
  'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
8
7
  'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
9
8
  ];
10
- const inherited = inheritedKeys
11
- .flatMap(key => process.env[key] === undefined ? [] : [`${key}=${process.env[key]}`]);
9
+ const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
10
+ // Explicit defense: trusted settle/recover/delete/close workers are effects
11
+ // nested inside the outer audited command, never fresh agent attempts.
12
+ delete env[FLEET_PROXY_STATE_DIR_ENV];
13
+ delete env[FLEET_PROXY_CALLER_ENV];
14
+ return env;
15
+ }
16
+ /** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
17
+ export function launchFleetWorker(args, operation, configPath) {
18
+ const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
19
+ const env = fleetWorkerEnv();
20
+ const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
12
21
  const suffix = randomUUID().slice(0, 8);
13
22
  const safeOperation = operation.replace(/[^A-Za-z0-9_.-]/g, '-');
14
23
  return new Promise((resolve, reject) => {
@@ -29,10 +38,6 @@ export function launchFleetWorker(args, operation, configPath) {
29
38
  ], { timeout: 15_000 }, error => error ? reject(error) : resolve());
30
39
  return;
31
40
  }
32
- const env = Object.fromEntries(inherited.map(value => {
33
- const split = value.indexOf('=');
34
- return [value.slice(0, split), value.slice(split + 1)];
35
- }));
36
41
  const child = spawn(process.execPath, cli, { detached: true, stdio: 'ignore', env });
37
42
  child.once('error', reject);
38
43
  child.once('spawn', () => { child.unref(); resolve(); });
@@ -0,0 +1,19 @@
1
+ import type { AgentTemplateDefinition } from '../config.js';
2
+ /** Cross-process exclusion used from seal through durable Room publication. */
3
+ export declare function acquireLaunchSnapshotLock(options?: {
4
+ beforePublish?: (claimPath: string, lockPath: string) => void;
5
+ }): () => void;
6
+ export declare function redactLaunchDefinition(value: unknown, key?: string): unknown;
7
+ export declare function sealLaunchSnapshot(definitions: Record<string, AgentTemplateDefinition>): string;
8
+ export declare function readLaunchSnapshot(hash: string): Record<string, AgentTemplateDefinition>;
9
+ /** Delete a sealed snapshot only after no retained Task/Room record references it. */
10
+ export declare function releaseLaunchSnapshot(hash: string): void;
11
+ /**
12
+ * Deletion-finalization variant: the caller already holds the global
13
+ * launch-snapshot lock and is about to unlink the deletion-pending task
14
+ * `taskId`. The reference scan excludes exactly that task file — every other
15
+ * retained Task/Room reference still protects the snapshot. Deleting the
16
+ * snapshot BEFORE the task unlink keeps every crash seam recoverable: the
17
+ * hidden intent survives, and a retry sees the snapshot already absent.
18
+ */
19
+ export declare function releaseLaunchSnapshotForDeletingTask(hash: string, taskId: string): void;