@ours.network/fleet 1.0.5 → 1.1.0-nightly.10

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 (159) hide show
  1. package/README.md +122 -133
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.d.ts +0 -1
  5. package/dist/application/capabilities.js +3 -13
  6. package/dist/application/fleet-query-service.d.ts +0 -3
  7. package/dist/application/fleet-query-service.js +14 -25
  8. package/dist/application/role-creation-service.d.ts +5 -24
  9. package/dist/application/role-creation-service.js +27 -67
  10. package/dist/application/role-removal-service.js +6 -3
  11. package/dist/application/role-repository.d.ts +1 -2
  12. package/dist/application/role-repository.js +7 -7
  13. package/dist/application/session-control.d.ts +0 -12
  14. package/dist/application/session-control.js +0 -42
  15. package/dist/application/session-mutations.d.ts +5 -5
  16. package/dist/application/task-room-service.d.ts +1 -1
  17. package/dist/application/task-room-service.js +154 -16
  18. package/dist/application/types.d.ts +1 -8
  19. package/dist/briefing.js +12 -11
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +143 -63
  22. package/dist/config-yaml.d.ts +1 -1
  23. package/dist/config.d.ts +66 -6
  24. package/dist/config.js +398 -43
  25. package/dist/creation.d.ts +1 -1
  26. package/dist/creation.js +2 -2
  27. package/dist/daemon-recovery.d.ts +93 -0
  28. package/dist/daemon-recovery.js +328 -0
  29. package/dist/docs.d.ts +1 -1
  30. package/dist/docs.js +103 -127
  31. package/dist/doctor.js +18 -11
  32. package/dist/fleet-command-audit.d.ts +160 -0
  33. package/dist/fleet-command-audit.js +433 -0
  34. package/dist/fleet-proxy.d.ts +3 -1
  35. package/dist/fleet-proxy.js +30 -8
  36. package/dist/generated-agent-source.d.ts +9 -0
  37. package/dist/generated-agent-source.js +53 -0
  38. package/dist/harness/acp-session-transport.d.ts +8 -0
  39. package/dist/harness/acp-session-transport.js +3 -0
  40. package/dist/harness/agent-session.d.ts +51 -0
  41. package/dist/harness/agent-session.js +1 -0
  42. package/dist/harness/claude-code-session.d.ts +24 -0
  43. package/dist/harness/claude-code-session.js +36 -0
  44. package/dist/harness/claude-code.d.ts +2 -1
  45. package/dist/harness/claude-code.js +41 -81
  46. package/dist/harness/codex-session.d.ts +24 -0
  47. package/dist/harness/codex-session.js +36 -0
  48. package/dist/harness/codex.d.ts +2 -1
  49. package/dist/harness/codex.js +55 -45
  50. package/dist/harness/registry.js +4 -0
  51. package/dist/harness/types.d.ts +6 -38
  52. package/dist/index.d.ts +4 -6
  53. package/dist/index.js +2 -4
  54. package/dist/init-guidance.d.ts +1 -0
  55. package/dist/init-guidance.js +6 -0
  56. package/dist/isolation/resources.d.ts +3 -7
  57. package/dist/isolation/resources.js +3 -7
  58. package/dist/model-env.d.ts +4 -6
  59. package/dist/model-env.js +5 -8
  60. package/dist/monitor.d.ts +1 -24
  61. package/dist/monitor.js +5 -140
  62. package/dist/ops.js +4 -2
  63. package/dist/owner-channel/channel.d.ts +58 -2
  64. package/dist/owner-channel/channel.js +269 -29
  65. package/dist/owner-channel/commands.d.ts +2 -0
  66. package/dist/owner-channel/commands.js +31 -13
  67. package/dist/owner-channel/state.d.ts +2 -0
  68. package/dist/owner-channel/state.js +8 -2
  69. package/dist/preset-bootstrap.d.ts +10 -0
  70. package/dist/preset-bootstrap.js +85 -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.d.ts +3 -2
  84. package/dist/resolved-plan.js +21 -5
  85. package/dist/rooms-tasks/cli.js +225 -40
  86. package/dist/rooms-tasks/config.js +24 -13
  87. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  88. package/dist/rooms-tasks/external-worker.js +14 -9
  89. package/dist/rooms-tasks/provision.js +98 -36
  90. package/dist/rooms-tasks/templates.d.ts +0 -1
  91. package/dist/rooms-tasks/templates.js +9 -76
  92. package/dist/rooms-tasks/types.d.ts +10 -16
  93. package/dist/rooms-tasks/types.js +1 -4
  94. package/dist/runner.d.ts +13 -16
  95. package/dist/runner.js +110 -122
  96. package/dist/sensitive-config.d.ts +2 -0
  97. package/dist/sensitive-config.js +4 -0
  98. package/dist/session/acp.d.ts +7 -2
  99. package/dist/session/acp.js +29 -6
  100. package/dist/session/activity.d.ts +2 -3
  101. package/dist/session/activity.js +2 -3
  102. package/dist/session/arbiter.d.ts +4 -4
  103. package/dist/session/control.d.ts +17 -4
  104. package/dist/session/control.js +35 -0
  105. package/dist/session/types.d.ts +11 -5
  106. package/dist/spawn.d.ts +8 -33
  107. package/dist/spawn.js +53 -213
  108. package/dist/supervisor/none.d.ts +1 -1
  109. package/dist/supervisor/none.js +2 -2
  110. package/dist/temp-lifecycle.d.ts +1 -1
  111. package/dist/tmux.d.ts +1 -14
  112. package/dist/tmux.js +1 -51
  113. package/dist/watchdog/config.d.ts +10 -7
  114. package/dist/watchdog/config.js +30 -18
  115. package/dist/watchdog/run.js +12 -36
  116. package/dist/web/auth.d.ts +1 -1
  117. package/dist/web/fleet-config-service.d.ts +11 -8
  118. package/dist/web/fleet-config-service.js +384 -183
  119. package/dist/web/runtime.js +4 -15
  120. package/dist/web/server.d.ts +1 -3
  121. package/dist/web/server.js +3 -14
  122. package/dist/web/topology-promote.js +26 -19
  123. package/dist/web/topology.js +2 -2
  124. package/dist/web/yaml-document-edit.js +2 -0
  125. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  126. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  127. package/dist/web-app/index.html +2 -2
  128. package/examples/fleet/agents/Alice.yaml +11 -0
  129. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  130. package/examples/fleet/brains/claude-default.yaml +5 -0
  131. package/examples/fleet/roles/coordinator.yaml +5 -0
  132. package/examples/fleet/roles/developer.yaml +3 -0
  133. package/examples/fleet.yaml +74 -0
  134. package/package.json +5 -8
  135. package/presets/fleet/agents/Agent.yaml +3 -0
  136. package/presets/fleet/agents/Architect.yaml +3 -0
  137. package/presets/fleet/agents/Critic.yaml +3 -0
  138. package/presets/fleet/agents/Developer.yaml +3 -0
  139. package/presets/fleet/agents/Secretary.yaml +3 -0
  140. package/presets/fleet/agents/Tester.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/roles/Agent.yaml +6 -0
  143. package/presets/fleet/roles/Architect.yaml +6 -0
  144. package/presets/fleet/roles/Critic.yaml +6 -0
  145. package/presets/fleet/roles/Developer.yaml +6 -0
  146. package/presets/fleet/roles/Secretary.yaml +6 -0
  147. package/presets/fleet/roles/Tester.yaml +6 -0
  148. package/presets/fleet/room_templates/pair.yaml +11 -0
  149. package/presets/fleet/room_templates/single.yaml +8 -0
  150. package/presets/fleet/room_templates/team.yaml +12 -0
  151. package/presets/fleet.yaml +7 -0
  152. package/presets/manifest.json +5 -0
  153. package/dist/session/tmux.d.ts +0 -28
  154. package/dist/session/tmux.js +0 -80
  155. package/dist/web/terminal/bridge.d.ts +0 -27
  156. package/dist/web/terminal/bridge.js +0 -317
  157. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  158. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  159. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { loadConfig, ConfigError } from '../config.js';
3
4
  import { provisionMembers, getBinPath } from './provision.js';
4
5
  import { launchFleetWorker } from './external-worker.js';
@@ -9,6 +10,7 @@ import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
9
10
  import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
10
11
  import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
11
12
  import { TaskListError } from './task-lists.js';
13
+ import { recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
12
14
  class TaskRoomPublicError extends Error {
13
15
  code;
14
16
  fields;
@@ -41,7 +43,7 @@ function taskRoomPublicFailure(error) {
41
43
  case 'template_not_found': return {
42
44
  legacy: `template not found: ${f.template ?? 'requested-template'}`, kind: 'not_found',
43
45
  detail: `The requested template ${f.template ?? ''}`.trim() + ' was not found.',
44
- action: 'Run ours-fleet template list and retry with an available template.',
46
+ action: 'Run ours-fleet init to seed missing file-backed presets, then run ours-fleet template list.',
45
47
  };
46
48
  case 'template_mismatch': return {
47
49
  legacy: `template ${f.requested ?? 'requested-template'} does not match room ${f.room ?? 'requested-room'}'s provisioned template ${f.provisioned ?? 'recorded-template'}`,
@@ -89,8 +91,29 @@ function die(e) {
89
91
  const msg = e instanceof TaskRoomPublicError
90
92
  ? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
91
93
  process.stderr.write(`error: ${msg}\n`);
94
+ recordFleetAuditFailure(classifyTaskRoomFailure(e));
92
95
  process.exit(1);
93
96
  }
97
+ function classifyTaskRoomFailure(e) {
98
+ if (e instanceof TaskRoomApplicationError)
99
+ e = taskRoomPublicError(e.code, e.fields);
100
+ if (e instanceof TaskRoomPublicError || e instanceof TaskListError || e instanceof ConfigError)
101
+ return { class: 'validation', effect: 'not_started' };
102
+ if (e instanceof TaskStateError) {
103
+ const known = /^task not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
104
+ || e.message === 'invalid task ID: expected the canonical 17-character lowercase ID'
105
+ || isKnownTaskStateMessage(e.message);
106
+ return known ? { class: 'validation', effect: 'not_started' }
107
+ : { class: 'runtime', effect: 'unknown' };
108
+ }
109
+ if (e instanceof RoomStateError) {
110
+ const known = /^room not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
111
+ || isKnownRoomStateMessage(e.message);
112
+ return known ? { class: 'validation', effect: 'not_started' }
113
+ : { class: 'runtime', effect: 'unknown' };
114
+ }
115
+ return { class: 'runtime', effect: 'unknown' };
116
+ }
94
117
  const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
95
118
  const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
96
119
  function isKnownTaskStateMessage(message) {
@@ -127,6 +150,7 @@ function dieTaskRoom(e) {
127
150
  action: failure.action,
128
151
  });
129
152
  process.stderr.write(`${output}\n`);
153
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
130
154
  process.exit(1);
131
155
  }
132
156
  if (e instanceof TaskListError) {
@@ -142,6 +166,7 @@ function dieTaskRoom(e) {
142
166
  : 'Use a valid NFC-normalized name of at most 64 Unicode code points.',
143
167
  });
144
168
  process.stderr.write(`${output}\n`);
169
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
145
170
  process.exit(1);
146
171
  }
147
172
  const taskMissing = e instanceof TaskStateError
@@ -167,6 +192,7 @@ function dieTaskRoom(e) {
167
192
  : 'Retry once; if it repeats, run ours-fleet doctor and inspect the role logs.',
168
193
  });
169
194
  process.stderr.write(`${output}\n`);
195
+ recordFleetAuditFailure(classifyTaskRoomFailure(e));
170
196
  process.exit(1);
171
197
  }
172
198
  const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
@@ -175,23 +201,107 @@ const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
175
201
  + ` — List ${markdownCode(t.list_name ?? 'default')}`
176
202
  + (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
177
203
  });
178
- const taskActionMarkdown = (title, task, fields = []) => renderMarkdownResult({
179
- icon: '📋', title,
180
- fields: [
181
- { label: 'ID', value: task.task_id, kind: 'code' },
182
- { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
183
- { label: 'List', value: task.list_name ?? 'default', kind: 'code' },
184
- ...fields,
185
- ],
186
- });
187
- const roomActionMarkdown = (title, room, fields = []) => renderMarkdownResult({
188
- icon: '🏠', title,
189
- fields: [
190
- { label: 'ID', value: room.room_id, kind: 'code' },
191
- { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
192
- ...fields,
193
- ],
194
- });
204
+ const taskActionMarkdown = (title, task, fields = []) => {
205
+ recordFleetAuditResource('task', task.task_id);
206
+ if (task.room_id)
207
+ recordFleetAuditResource('room', task.room_id);
208
+ return renderMarkdownResult({
209
+ icon: '📋', title,
210
+ fields: [
211
+ { label: 'ID', value: task.task_id, kind: 'code' },
212
+ { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
213
+ { label: 'List', value: task.list_name ?? 'default', kind: 'code' },
214
+ ...fields,
215
+ ],
216
+ });
217
+ };
218
+ function safeSelectionSummary(definition, kind) {
219
+ const selected = definition?.[kind];
220
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
221
+ return 'unresolved';
222
+ const selection = selected;
223
+ if (selection.kind === 'ref' && typeof selection.id === 'string')
224
+ return `ref:${selection.id} (reference)`;
225
+ if (selection.kind === 'inline' && typeof selection.fingerprint === 'string')
226
+ return `inline:${selection.fingerprint} (inline)`;
227
+ if (typeof selection.ref === 'string')
228
+ return `ref:${selection.ref}`;
229
+ if (!Object.hasOwn(selection, 'inline'))
230
+ return 'unresolved';
231
+ const canonical = (value) => Array.isArray(value)
232
+ ? `[${value.map(canonical).join(',')}]`
233
+ : value && typeof value === 'object'
234
+ ? `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
235
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(',')}}`
236
+ : JSON.stringify(value);
237
+ const fingerprint = createHash('sha256').update(canonical(selection.inline)).digest('hex').slice(0, 16);
238
+ return `inline:sha256:${fingerprint}`;
239
+ }
240
+ function safePermissionsSummary(definition) {
241
+ const permissions = definition?.permissions;
242
+ if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions))
243
+ return undefined;
244
+ const policy = permissions;
245
+ const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof policy[key] === 'string' ? [`${key}=${policy[key]}`] : []);
246
+ return fields.length ? fields.join(',') : undefined;
247
+ }
248
+ function auditTask(operation, task, previousState, newState = task.state, revision) {
249
+ const materialSameState = operation === 'block' || operation === 'unblock' || operation === 'settling';
250
+ if (previousState === newState && !materialSameState)
251
+ return;
252
+ recordFleetAuditResource('task', task.task_id);
253
+ if (task.room_id)
254
+ recordFleetAuditResource('room', task.room_id);
255
+ const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
256
+ const definitions = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.agent_definition]) ?? []);
257
+ const semanticOperation = operation === 'recover'
258
+ ? newState === 'active' ? 'work' : newState === 'done' ? 'done'
259
+ : newState === 'cancelled' ? 'cancel' : undefined
260
+ : operation;
261
+ if (!semanticOperation)
262
+ return;
263
+ recordFleetAuditPresentation({ kind: 'task', operation: semanticOperation, id: task.task_id, title: task.title,
264
+ previousState, newState,
265
+ revision: revision ?? task.blocked?.at ?? task.ended_at ?? task.started_at ?? task.created_at,
266
+ list: task.list_name ?? 'default',
267
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
268
+ roomId: task.room_id, agents: [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => ({ name: member.name,
269
+ brain: safeSelectionSummary(definitions.get(member.name), 'brain'),
270
+ role: safeSelectionSummary(definitions.get(member.name), 'role') === 'unresolved'
271
+ ? member.cowork_role : safeSelectionSummary(definitions.get(member.name), 'role'),
272
+ permissions: safePermissionsSummary(definitions.get(member.name)) })) });
273
+ }
274
+ function auditRoom(operation, room, previousState, newState = room.state) {
275
+ if (previousState === newState)
276
+ return;
277
+ recordFleetAuditResource('room', room.room_id);
278
+ const semanticOperation = operation === 'recover'
279
+ ? newState === 'active' ? 'activate' : newState === 'deleted' ? 'delete'
280
+ : newState === 'closing' ? 'close' : undefined
281
+ : operation;
282
+ if (!semanticOperation)
283
+ return;
284
+ recordFleetAuditPresentation({ kind: 'room', operation: semanticOperation, id: room.room_id, previousState, newState,
285
+ revision: room.closed_at ?? room.activated_at ?? room.close?.accepted_at ?? room.created_at,
286
+ name: room.room_name, taskId: room.task_id,
287
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
288
+ participants: [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(member => ({ name: member.role_name, id: member.identity_cid,
289
+ brain: safeSelectionSummary(member.launch?.agent_definition, 'brain'),
290
+ role: safeSelectionSummary(member.launch?.agent_definition, 'role') === 'unresolved'
291
+ ? member.cowork_role : safeSelectionSummary(member.launch?.agent_definition, 'role'),
292
+ permissions: safePermissionsSummary(member.launch?.agent_definition) })) });
293
+ }
294
+ const roomActionMarkdown = (title, room, fields = []) => {
295
+ recordFleetAuditResource('room', room.room_id);
296
+ return renderMarkdownResult({
297
+ icon: '🏠', title,
298
+ fields: [
299
+ { label: 'ID', value: room.room_id, kind: 'code' },
300
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
301
+ ...fields,
302
+ ],
303
+ });
304
+ };
195
305
  const PUBLIC_SETTLE_WAIT_MS = 60_000;
196
306
  const PUBLIC_SETTLE_POLL_MS = 100;
197
307
  const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
@@ -208,6 +318,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
208
318
  actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
209
319
  recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
210
320
  });
321
+ const task = getTask(taskId);
322
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
323
+ state: task.state, category: 'settlement_failed',
324
+ eventId: task.terminal_intent?.error_at ?? task.terminal_intent?.accepted_at ?? task.created_at });
211
325
  throw error;
212
326
  }
213
327
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -220,7 +334,11 @@ async function launchTaskSettleWorker(taskId, configPath) {
220
334
  }
221
335
  await sleep(PUBLIC_SETTLE_POLL_MS);
222
336
  }
223
- return { task: getTask(taskId), timedOut: true };
337
+ const task = getTask(taskId);
338
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
339
+ state: task.state, category: 'settlement_pending',
340
+ eventId: task.terminal_intent?.accepted_at ?? task.created_at });
341
+ return { task, timedOut: true };
224
342
  }
225
343
  async function launchRoomDeleteWorker(roomId, configPath) {
226
344
  const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
@@ -232,6 +350,11 @@ async function launchRoomDeleteWorker(roomId, configPath) {
232
350
  actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
233
351
  recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
234
352
  });
353
+ const room = getRoomRecord(roomId);
354
+ if (room)
355
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
356
+ state: room.state, category: 'cleanup_failed',
357
+ eventId: room.close?.error_at ?? room.close?.accepted_at ?? room.created_at });
235
358
  throw error;
236
359
  }
237
360
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -244,7 +367,12 @@ async function launchRoomDeleteWorker(roomId, configPath) {
244
367
  }
245
368
  await sleep(PUBLIC_SETTLE_POLL_MS);
246
369
  }
247
- return { deleted: getRoomRecord(roomId) === undefined, timedOut: true };
370
+ const remaining = getRoomRecord(roomId);
371
+ if (remaining)
372
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
373
+ state: remaining.state, category: 'cleanup_pending',
374
+ eventId: remaining.close?.accepted_at ?? remaining.created_at });
375
+ return { deleted: remaining === undefined, timedOut: true };
248
376
  }
249
377
  function loadCfg(opts) {
250
378
  return loadConfig(opts.configuration);
@@ -346,21 +474,36 @@ async function provisionRoom(cfg, input) {
346
474
  setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
347
475
  ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
348
476
  : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
477
+ const failed = getRoomRecord(record.room_id);
478
+ if (failed)
479
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
480
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
481
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
349
482
  throw error;
350
483
  }
351
484
  }
352
485
  record = advanceSaga(record.room_id, 'create_members', 3);
353
486
  if (input.template && input.template.members.length > 0) {
354
- record = await provisionMembers({
355
- cfg,
356
- cowork,
357
- roomId: record.room_id,
358
- taskId: input.taskId,
359
- template: input.template,
360
- binPath: getBinPath(),
361
- brief: input.brief,
362
- goal: input.goal,
363
- });
487
+ try {
488
+ record = await provisionMembers({
489
+ cfg,
490
+ cowork,
491
+ roomId: record.room_id,
492
+ taskId: input.taskId,
493
+ template: input.template,
494
+ binPath: getBinPath(),
495
+ brief: input.brief,
496
+ goal: input.goal,
497
+ });
498
+ }
499
+ catch (error) {
500
+ const failed = getRoomRecord(record.room_id);
501
+ if (failed)
502
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
503
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
504
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
505
+ throw error;
506
+ }
364
507
  }
365
508
  else {
366
509
  record = activateRoom(record.room_id);
@@ -382,12 +525,11 @@ export function registerTemplateCommands(parent, cOpt) {
382
525
  return;
383
526
  }
384
527
  if (!templates.length) {
385
- console.log('No templates configured.');
528
+ console.log('No templates configured. Run ours-fleet init to seed missing file-backed presets.');
386
529
  return;
387
530
  }
388
531
  for (const t of templates) {
389
- const tag = t.builtin ? ' (built-in)' : '';
390
- console.log(`${t.name}@${t.version}${tag} ${t.description}`);
532
+ console.log(`${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
391
533
  }
392
534
  }
393
535
  catch (e) {
@@ -405,11 +547,13 @@ export function registerTemplateCommands(parent, cOpt) {
405
547
  return;
406
548
  }
407
549
  console.log(`${t.name}@${t.version} ${t.description}`);
550
+ console.log(`Source: ${t.sourceFile ?? 'manifest'}`);
408
551
  if (t.contract)
409
552
  console.log(`\nContract:\n${t.contract}`);
410
553
  console.log(`\nMembers:`);
411
554
  for (const m of t.members)
412
- console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
555
+ console.log(` ${m.slot}: ${m.count}× ${m.role} (`
556
+ + `${'ref' in m.agent ? `agent ref: ${m.agent.ref}` : 'inline Agent'})`);
413
557
  console.log(`\nContent hash: ${t.content_hash}`);
414
558
  }
415
559
  catch (e) {
@@ -487,7 +631,7 @@ export function registerTaskCommands(parent, cOpt) {
487
631
  .option('--list <name>', 'filter by task list')
488
632
  .option('--group-by-list', 'group deterministic results by list (JSON)')
489
633
  .option('--json', 'JSON output')
490
- .action((opts) => {
634
+ .action(async (opts) => {
491
635
  try {
492
636
  let stateFilter;
493
637
  if (opts.state && opts.state !== 'all') {
@@ -513,9 +657,10 @@ export function registerTaskCommands(parent, cOpt) {
513
657
  taskCmd.command('lists')
514
658
  .description('list named task lists')
515
659
  .option('--json', 'JSON output')
516
- .action((opts) => {
660
+ .action(async (opts) => {
517
661
  try {
518
- const lists = taskRoomService().listTaskLists();
662
+ const service = taskRoomService(opts.configuration);
663
+ const lists = service.listTaskLists();
519
664
  if (opts.json) {
520
665
  console.log(JSON.stringify({ schema_version: 1, lists }, null, 2));
521
666
  return;
@@ -606,9 +751,10 @@ export function registerTaskCommands(parent, cOpt) {
606
751
  taskCmd.command('show <id>')
607
752
  .description('show task details')
608
753
  .option('--json', 'JSON output')
609
- .action((id, opts) => {
754
+ .action(async (id, opts) => {
610
755
  try {
611
- const { task: t, orchestration: room } = taskRoomService().getTask(id);
756
+ const service = taskRoomService();
757
+ const { task: t, orchestration: room } = service.getTask(id);
612
758
  if (opts.json) {
613
759
  console.log(JSON.stringify({
614
760
  schema_version: 1, task: t, orchestration: room ?? null,
@@ -676,9 +822,11 @@ export function registerTaskCommands(parent, cOpt) {
676
822
  .option('--json', 'JSON output')
677
823
  .action((id, opts) => {
678
824
  try {
825
+ const previous = getTask(id).state;
679
826
  const t = taskRoomService().blockTask({
680
827
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
681
828
  });
829
+ auditTask('block', t, previous);
682
830
  if (opts.json) {
683
831
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
684
832
  return;
@@ -696,9 +844,12 @@ export function registerTaskCommands(parent, cOpt) {
696
844
  .option('--json', 'JSON output')
697
845
  .action((id, opts) => {
698
846
  try {
847
+ const prior = getTask(id);
848
+ const previous = prior.state;
699
849
  const t = taskRoomService().unblockTask({
700
850
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
701
851
  });
852
+ auditTask('unblock', t, previous, t.state, prior.blocked?.at);
702
853
  if (opts.json) {
703
854
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
704
855
  return;
@@ -716,9 +867,11 @@ export function registerTaskCommands(parent, cOpt) {
716
867
  .option('--json', 'JSON output')
717
868
  .action((id, opts) => {
718
869
  try {
870
+ const previous = getTask(id).state;
719
871
  const t = taskRoomService().reviewTask({
720
872
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
721
873
  });
874
+ auditTask('review', t, previous);
722
875
  if (opts.json) {
723
876
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
724
877
  return;
@@ -738,6 +891,7 @@ export function registerTaskCommands(parent, cOpt) {
738
891
  .option('--json', 'JSON output')
739
892
  .action(async (id, opts) => {
740
893
  try {
894
+ const previous = getTask(id).state;
741
895
  let summary = opts.summary;
742
896
  if (opts.summaryFile)
743
897
  summary = readFileSync(opts.summaryFile, 'utf8');
@@ -745,10 +899,13 @@ export function registerTaskCommands(parent, cOpt) {
745
899
  const plan = await taskRoomService(opts.configuration).completeTask({
746
900
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
747
901
  });
902
+ if (plan.task.terminal_intent?.status === 'pending')
903
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
748
904
  const settled = plan.settlementRequired
749
905
  ? await launchTaskSettleWorker(id, opts.configuration)
750
906
  : { task: plan.task, timedOut: false };
751
907
  const t = settled.task;
908
+ auditTask('done', t, previous);
752
909
  if (opts.json) {
753
910
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
754
911
  return;
@@ -776,13 +933,17 @@ export function registerTaskCommands(parent, cOpt) {
776
933
  try {
777
934
  if (id !== confirmId)
778
935
  throw taskRoomPublicError('task_confirmation_mismatch');
936
+ const previous = getTask(id).state;
779
937
  const plan = await taskRoomService(opts.configuration).cancelTask({
780
938
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
781
939
  });
940
+ if (plan.task.terminal_intent?.status === 'pending')
941
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
782
942
  const settled = plan.settlementRequired
783
943
  ? await launchTaskSettleWorker(id, opts.configuration)
784
944
  : { task: plan.task, timedOut: false };
785
945
  const t = settled.task;
946
+ auditTask('cancel', t, previous);
786
947
  if (opts.json) {
787
948
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
788
949
  return;
@@ -810,9 +971,16 @@ export function registerTaskCommands(parent, cOpt) {
810
971
  try {
811
972
  if (id !== confirmId)
812
973
  throw taskRoomPublicError('task_confirmation_mismatch');
974
+ let prior;
975
+ try {
976
+ prior = getTask(id);
977
+ }
978
+ catch { /* idempotent already-absent delete */ }
813
979
  const deleted = taskRoomService().deleteTask({
814
980
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
815
981
  });
982
+ if (prior)
983
+ auditTask('delete', prior, prior.state, 'deleted');
816
984
  if (opts.json) {
817
985
  console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
818
986
  return;
@@ -833,6 +1001,7 @@ export function registerTaskCommands(parent, cOpt) {
833
1001
  .option('--json', 'JSON output')
834
1002
  .action(async (id, opts) => {
835
1003
  try {
1004
+ const previous = getTask(id).state;
836
1005
  const app = taskRoomService(opts.configuration);
837
1006
  const actor = { kind: 'local_control', surface: 'cli' };
838
1007
  const begin = await app.beginTaskRecovery({ actor, taskId: id });
@@ -843,6 +1012,7 @@ export function registerTaskCommands(parent, cOpt) {
843
1012
  })()
844
1013
  : begin.result;
845
1014
  const t = recovered.task;
1015
+ auditTask('recover', t, previous);
846
1016
  const room = recovered.room;
847
1017
  const recoveryActions = recovered.issues.map(issue => {
848
1018
  if (issue.code === 'terminal_pending')
@@ -992,6 +1162,7 @@ export function registerTaskCommands(parent, cOpt) {
992
1162
  .option('--json', 'JSON output')
993
1163
  .action(async (id, opts) => {
994
1164
  try {
1165
+ const previous = getTask(id).state;
995
1166
  let summary = opts.summary;
996
1167
  if (opts.summaryFile)
997
1168
  summary = readFileSync(opts.summaryFile, 'utf8');
@@ -999,10 +1170,13 @@ export function registerTaskCommands(parent, cOpt) {
999
1170
  const plan = await taskRoomService(opts.configuration).finishTask({
1000
1171
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
1001
1172
  });
1173
+ if (plan.task.terminal_intent?.status === 'pending')
1174
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
1002
1175
  const settled = plan.settlementRequired
1003
1176
  ? await launchTaskSettleWorker(id, opts.configuration)
1004
1177
  : { task: plan.task, timedOut: false };
1005
1178
  const t = settled.task;
1179
+ auditTask('finish', t, previous);
1006
1180
  if (opts.json) {
1007
1181
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
1008
1182
  return;
@@ -1126,7 +1300,7 @@ export function registerRoomCommands(parent, cOpt) {
1126
1300
  .option('--json', 'JSON output')
1127
1301
  .action(async (id, opts) => {
1128
1302
  try {
1129
- const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
1303
+ const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
1130
1304
  const url = `http://localhost:4460/room/${id}`;
1131
1305
  if (opts.json) {
1132
1306
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
@@ -1186,10 +1360,16 @@ export function registerRoomCommands(parent, cOpt) {
1186
1360
  try {
1187
1361
  if (id !== confirmId)
1188
1362
  throw taskRoomPublicError('room_confirmation_mismatch');
1363
+ const previous = getRoomRecord(id);
1189
1364
  await taskRoomService(opts.configuration).requestRoomDeletion({
1190
1365
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1191
1366
  });
1367
+ const closing = getRoomRecord(id);
1368
+ if (previous && closing && previous.state !== closing.state)
1369
+ auditRoom('close', closing, previous.state, closing.state);
1192
1370
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1371
+ if (closing)
1372
+ auditRoom('delete', closing, closing.state, settled.deleted ? 'deleted' : 'closing');
1193
1373
  if (opts.json) {
1194
1374
  console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
1195
1375
  return;
@@ -1226,11 +1406,14 @@ export function registerRoomCommands(parent, cOpt) {
1226
1406
  .option('--json', 'JSON output')
1227
1407
  .action(async (id, opts) => {
1228
1408
  try {
1409
+ const previous = getRoomRecord(id);
1229
1410
  const recovered = await taskRoomService(opts.configuration).recoverRoom({
1230
1411
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1231
1412
  });
1232
1413
  if (recovered.kind === 'deletion_worker_required') {
1233
1414
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1415
+ if (previous)
1416
+ auditRoom('recover', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
1234
1417
  if (opts.json) {
1235
1418
  console.log(JSON.stringify({
1236
1419
  schema_version: 1, room: null, orchestration: null,
@@ -1251,6 +1434,8 @@ export function registerRoomCommands(parent, cOpt) {
1251
1434
  return;
1252
1435
  }
1253
1436
  const { room: cowork, orchestration: r, issues: actions } = recovered;
1437
+ if (r)
1438
+ auditRoom('recover', r, previous?.state ?? 'unresolved', r.state);
1254
1439
  if (opts.json) {
1255
1440
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
1256
1441
  return;
@@ -1,7 +1,7 @@
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, } from './types.js';
4
+ import { isSensitiveConfigKey } from '../sensitive-config.js';
5
5
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
6
6
  const CID_RE = /^[0-9a-fA-F]{64}$/;
7
7
  export class RoomsTasksConfigError extends Error {
@@ -116,9 +116,6 @@ export function validateRoomTemplatesConfig(raw, path) {
116
116
  if (!isPlainObject(tplRaw))
117
117
  throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
118
118
  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
119
  const version = tplRaw.version;
123
120
  if (version === undefined || !Number.isInteger(version) || version < 1)
124
121
  throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
@@ -136,19 +133,33 @@ export function validateRoomTemplatesConfig(raw, path) {
136
133
  throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
137
134
  if (!Number.isInteger(m.count) || m.count < 1)
138
135
  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`);
136
+ if (!isPlainObject(m.agent))
137
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: required mapping`);
138
+ const agentKeys = Object.keys(m.agent);
139
+ const isRef = agentKeys.length === 1 && typeof m.agent.ref === 'string';
140
+ const isDefinition = agentKeys.includes('role') && agentKeys.includes('brain');
141
+ if (!isRef && !isDefinition)
142
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: must be {ref} or canonical {role, brain, ...}`);
143
+ const containsSensitive = (value) => Array.isArray(value)
144
+ ? value.some(containsSensitive)
145
+ : Boolean(value && typeof value === 'object' && Object.entries(value)
146
+ .some(([key, child]) => isSensitiveConfigKey(key) || containsSensitive(child)));
147
+ if (isDefinition && containsSensitive(m.agent))
148
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: inline sensitive configuration cannot be persisted; use references`);
149
+ if (isDefinition) {
150
+ const definition = m.agent;
151
+ const brain = isPlainObject(definition.brain) && isPlainObject(definition.brain.inline)
152
+ ? definition.brain.inline : undefined;
153
+ if (definition.env !== undefined || definition.owner_channel !== undefined
154
+ || definition.auth_proxy !== undefined || brain?.harness_options !== undefined
155
+ || brain?.session_options !== undefined)
156
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: secret-capable inline fields cannot be persisted; use references`);
145
157
  }
146
158
  return {
147
159
  slot: m.slot,
148
160
  role: m.role,
149
161
  count: m.count,
150
- role_ref: m.role_ref,
151
- overrides: m.overrides,
162
+ agent: structuredClone(m.agent),
152
163
  };
153
164
  });
154
165
  let room;
@@ -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(); });