@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,14 +1,52 @@
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';
5
- import { resolveTemplate, snapshotTemplate, } from './templates.js';
6
- import { getTask, activateTask, TaskStateError, } from './task-state.js';
6
+ import { resolveTemplate, sealTemplateSnapshot, snapshotTemplate, } from './templates.js';
7
+ import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from './launch-snapshot.js';
8
+ import { parseGroupedMemberArgs, readMembersFile } from './member-overrides.js';
9
+ const MEMBER_ARG_FLAGS = new Set([
10
+ '--member', '--agent-template', '--brain', '--role', '--approval', '--filesystem', '--unattended',
11
+ '--cwd', '--model', '--effort', '--loops-file', '--no-loops',
12
+ ]);
13
+ export function cliMemberOverrides(membersFile, argv = process.argv.slice(2)) {
14
+ const grouped = [];
15
+ for (let i = 0; i < argv.length; i++)
16
+ if (MEMBER_ARG_FLAGS.has(argv[i])) {
17
+ grouped.push(argv[i]);
18
+ if (argv[i] !== '--no-loops') {
19
+ grouped.push(argv[i + 1]);
20
+ i += 1;
21
+ }
22
+ }
23
+ if (membersFile && grouped.length)
24
+ throw new Error('--members-file cannot be combined with grouped --member options');
25
+ if (membersFile)
26
+ return readMembersFile(membersFile);
27
+ return grouped.length ? parseGroupedMemberArgs(grouped) : undefined;
28
+ }
29
+ function commandArgv(command) {
30
+ let root = command;
31
+ while (root.parent)
32
+ root = root.parent;
33
+ return root.rawArgs ?? process.argv.slice(2);
34
+ }
35
+ function cliAnonymousOverride(argv) {
36
+ const enabled = argv.includes('--anonymous');
37
+ const disabled = argv.includes('--no-anonymous');
38
+ if (enabled && disabled)
39
+ throw new Error('--anonymous and --no-anonymous are mutually exclusive');
40
+ return enabled ? true : disabled ? false : undefined;
41
+ }
42
+ import { getTask, getDeletingTask, activateTask, TaskStateError, } from './task-state.js';
7
43
  import { createRoomRecord, getRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
8
44
  import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
9
45
  import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
10
46
  import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
11
47
  import { TaskListError } from './task-lists.js';
48
+ import { recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
49
+ import { renderAgentConfiguration } from '../lifecycle-summary.js';
12
50
  class TaskRoomPublicError extends Error {
13
51
  code;
14
52
  fields;
@@ -41,7 +79,7 @@ function taskRoomPublicFailure(error) {
41
79
  case 'template_not_found': return {
42
80
  legacy: `template not found: ${f.template ?? 'requested-template'}`, kind: 'not_found',
43
81
  detail: `The requested template ${f.template ?? ''}`.trim() + ' was not found.',
44
- action: 'Run ours-fleet template list and retry with an available template.',
82
+ action: 'Run interactive ours-fleet init to review and create a complete default setup, then run ours-fleet template list.',
45
83
  };
46
84
  case 'template_mismatch': return {
47
85
  legacy: `template ${f.requested ?? 'requested-template'} does not match room ${f.room ?? 'requested-room'}'s provisioned template ${f.provisioned ?? 'recorded-template'}`,
@@ -89,8 +127,29 @@ function die(e) {
89
127
  const msg = e instanceof TaskRoomPublicError
90
128
  ? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
91
129
  process.stderr.write(`error: ${msg}\n`);
130
+ recordFleetAuditFailure(classifyTaskRoomFailure(e));
92
131
  process.exit(1);
93
132
  }
133
+ function classifyTaskRoomFailure(e) {
134
+ if (e instanceof TaskRoomApplicationError)
135
+ e = taskRoomPublicError(e.code, e.fields);
136
+ if (e instanceof TaskRoomPublicError || e instanceof TaskListError || e instanceof ConfigError)
137
+ return { class: 'validation', effect: 'not_started' };
138
+ if (e instanceof TaskStateError) {
139
+ const known = /^task not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
140
+ || e.message === 'invalid task ID: expected the canonical 17-character lowercase ID'
141
+ || isKnownTaskStateMessage(e.message);
142
+ return known ? { class: 'validation', effect: 'not_started' }
143
+ : { class: 'runtime', effect: 'unknown' };
144
+ }
145
+ if (e instanceof RoomStateError) {
146
+ const known = /^room not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
147
+ || isKnownRoomStateMessage(e.message);
148
+ return known ? { class: 'validation', effect: 'not_started' }
149
+ : { class: 'runtime', effect: 'unknown' };
150
+ }
151
+ return { class: 'runtime', effect: 'unknown' };
152
+ }
94
153
  const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
95
154
  const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
96
155
  function isKnownTaskStateMessage(message) {
@@ -104,7 +163,7 @@ function isKnownTaskStateMessage(message) {
104
163
  new RegExp(`^task ${SAFE_ID_WORD} is not tied to room ${SAFE_ID_WORD}$`, 'u'),
105
164
  new RegExp(`^task ${SAFE_ID_WORD} has no terminal intent$`, 'u'),
106
165
  new RegExp(`^task ${SAFE_ID_WORD} reached terminal state '${TASK_STATE_WORD}' outside its intent$`, 'u'),
107
- new RegExp(`^cannot delete a '${TASK_STATE_WORD}' task; only 'done' tasks can be deleted$`, 'u'),
166
+ new RegExp(`^task ${SAFE_ID_WORD} is pending deletion; run 'ours-fleet task delete ${SAFE_ID_WORD} ${SAFE_ID_WORD}' to retry cleanup$`, 'u'),
108
167
  ].some(pattern => pattern.test(message));
109
168
  }
110
169
  function isKnownRoomStateMessage(message) {
@@ -127,6 +186,7 @@ function dieTaskRoom(e) {
127
186
  action: failure.action,
128
187
  });
129
188
  process.stderr.write(`${output}\n`);
189
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
130
190
  process.exit(1);
131
191
  }
132
192
  if (e instanceof TaskListError) {
@@ -142,6 +202,7 @@ function dieTaskRoom(e) {
142
202
  : 'Use a valid NFC-normalized name of at most 64 Unicode code points.',
143
203
  });
144
204
  process.stderr.write(`${output}\n`);
205
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
145
206
  process.exit(1);
146
207
  }
147
208
  const taskMissing = e instanceof TaskStateError
@@ -167,6 +228,7 @@ function dieTaskRoom(e) {
167
228
  : 'Retry once; if it repeats, run ours-fleet doctor and inspect the role logs.',
168
229
  });
169
230
  process.stderr.write(`${output}\n`);
231
+ recordFleetAuditFailure(classifyTaskRoomFailure(e));
170
232
  process.exit(1);
171
233
  }
172
234
  const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
@@ -175,23 +237,118 @@ const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
175
237
  + ` — List ${markdownCode(t.list_name ?? 'default')}`
176
238
  + (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
177
239
  });
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
- });
240
+ const taskActionMarkdown = (title, task, fields = []) => {
241
+ recordFleetAuditResource('task', task.task_id);
242
+ if (task.room_id)
243
+ recordFleetAuditResource('room', task.room_id);
244
+ return renderMarkdownResult({
245
+ icon: '📋', title,
246
+ fields: [
247
+ { label: 'ID', value: task.task_id, kind: 'code' },
248
+ { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
249
+ { label: 'List', value: task.list_name ?? 'default', kind: 'code' },
250
+ ...fields,
251
+ ],
252
+ });
253
+ };
254
+ /**
255
+ * Shared launch-configuration line for CLI member listings. Pre-upgrade seats
256
+ * without a captured presentation keep the minimal listing rather than
257
+ * claiming resolved facts. Output is already escaped (markdownItems boundary).
258
+ */
259
+ function seatConfigurationSuffix(presentation) {
260
+ return presentation ? ` — ${renderAgentConfiguration(presentation)}` : '';
261
+ }
262
+ function safeSelectionSummary(definition, kind) {
263
+ const selected = definition?.[kind];
264
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
265
+ return 'unresolved';
266
+ const selection = selected;
267
+ if (selection.kind === 'ref' && typeof selection.id === 'string')
268
+ return `ref:${selection.id} (reference)`;
269
+ if (selection.kind === 'inline' && typeof selection.fingerprint === 'string')
270
+ return `inline:${selection.fingerprint} (inline)`;
271
+ if (typeof selection.ref === 'string')
272
+ return `ref:${selection.ref}`;
273
+ if (!Object.hasOwn(selection, 'inline'))
274
+ return 'unresolved';
275
+ const canonical = (value) => Array.isArray(value)
276
+ ? `[${value.map(canonical).join(',')}]`
277
+ : value && typeof value === 'object'
278
+ ? `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
279
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(',')}}`
280
+ : JSON.stringify(value);
281
+ const fingerprint = createHash('sha256').update(canonical(selection.inline)).digest('hex').slice(0, 16);
282
+ return `inline:sha256:${fingerprint}`;
283
+ }
284
+ function safePermissionsSummary(definition) {
285
+ const permissions = definition?.permissions;
286
+ if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions))
287
+ return undefined;
288
+ const policy = permissions;
289
+ const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof policy[key] === 'string' ? [`${key}=${policy[key]}`] : []);
290
+ return fields.length ? fields.join(',') : undefined;
291
+ }
292
+ function auditTask(operation, task, previousState, newState = task.state, revision) {
293
+ const materialSameState = operation === 'block' || operation === 'unblock' || operation === 'settling';
294
+ if (previousState === newState && !materialSameState)
295
+ return;
296
+ recordFleetAuditResource('task', task.task_id);
297
+ if (task.room_id)
298
+ recordFleetAuditResource('room', task.room_id);
299
+ const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
300
+ const definitions = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.agent_definition]) ?? []);
301
+ const presentations = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.presentation]) ?? []);
302
+ const semanticOperation = operation === 'recover'
303
+ ? newState === 'active' ? 'work' : newState === 'done' ? 'done'
304
+ : newState === 'cancelled' ? 'cancel' : undefined
305
+ : operation;
306
+ if (!semanticOperation)
307
+ return;
308
+ recordFleetAuditPresentation({ kind: 'task', operation: semanticOperation, id: task.task_id, title: task.title,
309
+ previousState, newState,
310
+ revision: revision ?? task.blocked?.at ?? task.ended_at ?? task.started_at ?? task.created_at,
311
+ list: task.list_name ?? 'default',
312
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
313
+ roomId: task.room_id, agents: [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => ({ name: member.name,
314
+ brain: safeSelectionSummary(definitions.get(member.name), 'brain'),
315
+ role: safeSelectionSummary(definitions.get(member.name), 'role') === 'unresolved'
316
+ ? member.cowork_role : safeSelectionSummary(definitions.get(member.name), 'role'),
317
+ permissions: safePermissionsSummary(definitions.get(member.name)),
318
+ configuration: presentations.get(member.name) })) });
319
+ }
320
+ function auditRoom(operation, room, previousState, newState = room.state) {
321
+ if (previousState === newState)
322
+ return;
323
+ recordFleetAuditResource('room', room.room_id);
324
+ const semanticOperation = operation === 'recover'
325
+ ? newState === 'active' ? 'activate' : newState === 'deleted' ? 'delete'
326
+ : newState === 'closing' ? 'close' : undefined
327
+ : operation;
328
+ if (!semanticOperation)
329
+ return;
330
+ recordFleetAuditPresentation({ kind: 'room', operation: semanticOperation, id: room.room_id, previousState, newState,
331
+ revision: room.closed_at ?? room.activated_at ?? room.close?.accepted_at ?? room.created_at,
332
+ name: room.room_name, taskId: room.task_id,
333
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
334
+ participants: [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(member => ({ name: member.role_name, id: member.identity_cid,
335
+ brain: safeSelectionSummary(member.launch?.agent_definition, 'brain'),
336
+ role: safeSelectionSummary(member.launch?.agent_definition, 'role') === 'unresolved'
337
+ ? member.cowork_role : safeSelectionSummary(member.launch?.agent_definition, 'role'),
338
+ permissions: safePermissionsSummary(member.launch?.agent_definition),
339
+ configuration: member.launch?.presentation })) });
340
+ }
341
+ const roomActionMarkdown = (title, room, fields = []) => {
342
+ recordFleetAuditResource('room', room.room_id);
343
+ return renderMarkdownResult({
344
+ icon: '🏠', title,
345
+ fields: [
346
+ { label: 'ID', value: room.room_id, kind: 'code' },
347
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
348
+ ...fields,
349
+ ],
350
+ });
351
+ };
195
352
  const PUBLIC_SETTLE_WAIT_MS = 60_000;
196
353
  const PUBLIC_SETTLE_POLL_MS = 100;
197
354
  const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
@@ -208,6 +365,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
208
365
  actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
209
366
  recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
210
367
  });
368
+ const task = getTask(taskId);
369
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
370
+ state: task.state, category: 'settlement_failed',
371
+ eventId: task.terminal_intent?.error_at ?? task.terminal_intent?.accepted_at ?? task.created_at });
211
372
  throw error;
212
373
  }
213
374
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -220,7 +381,54 @@ async function launchTaskSettleWorker(taskId, configPath) {
220
381
  }
221
382
  await sleep(PUBLIC_SETTLE_POLL_MS);
222
383
  }
223
- return { task: getTask(taskId), timedOut: true };
384
+ const task = getTask(taskId);
385
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
386
+ state: task.state, category: 'settlement_pending',
387
+ eventId: task.terminal_intent?.accepted_at ?? task.created_at });
388
+ return { task, timedOut: true };
389
+ }
390
+ /** Launch the deletion settle worker and wait boundedly for physical absence. */
391
+ async function launchTaskDeleteWorker(taskId, configPath) {
392
+ const readDeletion = () => {
393
+ try {
394
+ const task = getDeletingTask(taskId);
395
+ return { present: true, errorAt: task.deletion?.error_at, error: task.deletion?.error };
396
+ }
397
+ catch (error) {
398
+ // Only proven physical absence counts as deleted; anything else propagates.
399
+ if (error instanceof TaskStateError && /task not found/.test(error.message))
400
+ return { present: false };
401
+ throw error;
402
+ }
403
+ };
404
+ const before = readDeletion();
405
+ if (!before.present)
406
+ return { deleted: true, timedOut: false };
407
+ try {
408
+ await launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, configPath);
409
+ }
410
+ catch (error) {
411
+ await taskRoomService(configPath).recordDeletionError({
412
+ actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
413
+ recoveryHint: `External delete worker failed to start. Retry task delete ${taskId} ${taskId}.`,
414
+ }).catch(() => { });
415
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
416
+ state: 'deleting', category: 'settlement_failed', eventId: new Date().toISOString() });
417
+ throw error;
418
+ }
419
+ const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
420
+ while (Date.now() < deadline) {
421
+ const current = readDeletion();
422
+ if (!current.present)
423
+ return { deleted: true, timedOut: false };
424
+ if (current.errorAt !== before.errorAt && current.error) {
425
+ throw new TaskStateError(current.error);
426
+ }
427
+ await sleep(PUBLIC_SETTLE_POLL_MS);
428
+ }
429
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
430
+ state: 'deleting', category: 'settlement_pending', eventId: new Date().toISOString() });
431
+ return { deleted: false, timedOut: true };
224
432
  }
225
433
  async function launchRoomDeleteWorker(roomId, configPath) {
226
434
  const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
@@ -232,6 +440,11 @@ async function launchRoomDeleteWorker(roomId, configPath) {
232
440
  actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
233
441
  recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
234
442
  });
443
+ const room = getRoomRecord(roomId);
444
+ if (room)
445
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
446
+ state: room.state, category: 'cleanup_failed',
447
+ eventId: room.close?.error_at ?? room.close?.accepted_at ?? room.created_at });
235
448
  throw error;
236
449
  }
237
450
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -244,7 +457,12 @@ async function launchRoomDeleteWorker(roomId, configPath) {
244
457
  }
245
458
  await sleep(PUBLIC_SETTLE_POLL_MS);
246
459
  }
247
- return { deleted: getRoomRecord(roomId) === undefined, timedOut: true };
460
+ const remaining = getRoomRecord(roomId);
461
+ if (remaining)
462
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
463
+ state: remaining.state, category: 'cleanup_pending',
464
+ eventId: remaining.close?.accepted_at ?? remaining.created_at });
465
+ return { deleted: remaining === undefined, timedOut: true };
248
466
  }
249
467
  function loadCfg(opts) {
250
468
  return loadConfig(opts.configuration);
@@ -271,7 +489,7 @@ function resolveRoomTemplate(cfg, name) {
271
489
  const template = resolveTemplate(name, allTemplates(cfg));
272
490
  if (!template)
273
491
  throw new Error(`template not found: ${name}`);
274
- return snapshotTemplate(template);
492
+ return snapshotTemplate(template, cfg.agentTemplates);
275
493
  }
276
494
  function durableTaskRoomTemplate(task, room) {
277
495
  const snapshot = room.template_snapshot;
@@ -315,22 +533,36 @@ async function provisionRoom(cfg, input) {
315
533
  throw new ConfigError('rooms.owner: public_invite or public_invite_file is required when attach_owner is enabled');
316
534
  const cowork = coworkFor(cfg);
317
535
  const goal = input.goal?.trim() || input.name;
318
- const briefing = input.brief?.trim() || input.template?.contract?.trim() || goal;
319
- const created = await cowork.createRoom({
320
- room_name: input.name,
321
- goal,
322
- briefing,
323
- quiet_membership: input.template?.room?.quiet_membership,
324
- anonymous: input.template?.room?.anonymous,
325
- });
326
- let record = createRoomRecord({
327
- room_id: created.room_id,
328
- room_name: input.name,
329
- room_identity_cid: created.identity_cid,
330
- task_id: input.taskId,
331
- template_snapshot: input.template,
332
- });
333
- input.onCreated?.(record);
536
+ const unlockSnapshot = input.template ? acquireLaunchSnapshotLock() : undefined;
537
+ let launchTemplate;
538
+ let record;
539
+ try {
540
+ launchTemplate = input.template
541
+ ? sealTemplateSnapshot(input.template, cfg.agentTemplates ?? {}) : undefined;
542
+ const briefing = input.brief?.trim() || launchTemplate?.contract?.trim() || goal;
543
+ const created = await cowork.createRoom({
544
+ room_name: input.name,
545
+ goal,
546
+ briefing,
547
+ quiet_membership: launchTemplate?.room?.quiet_membership,
548
+ anonymous: launchTemplate?.room?.anonymous,
549
+ });
550
+ record = createRoomRecord({
551
+ room_id: created.room_id,
552
+ room_name: input.name,
553
+ room_identity_cid: created.identity_cid,
554
+ task_id: input.taskId,
555
+ template_snapshot: launchTemplate,
556
+ });
557
+ input.onCreated?.(record);
558
+ }
559
+ catch (error) {
560
+ unlockSnapshot?.();
561
+ if (launchTemplate?.launch_snapshot_hash)
562
+ releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
563
+ throw error;
564
+ }
565
+ unlockSnapshot?.();
334
566
  record = advanceSaga(record.room_id, 'create_room', 1);
335
567
  if (attachOwner) {
336
568
  try {
@@ -346,21 +578,36 @@ async function provisionRoom(cfg, input) {
346
578
  setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
347
579
  ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
348
580
  : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
581
+ const failed = getRoomRecord(record.room_id);
582
+ if (failed)
583
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
584
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
585
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
349
586
  throw error;
350
587
  }
351
588
  }
352
589
  record = advanceSaga(record.room_id, 'create_members', 3);
353
- 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
- });
590
+ if (launchTemplate && launchTemplate.members.length > 0) {
591
+ try {
592
+ record = await provisionMembers({
593
+ cfg,
594
+ cowork,
595
+ roomId: record.room_id,
596
+ taskId: input.taskId,
597
+ template: launchTemplate,
598
+ binPath: getBinPath(),
599
+ brief: input.brief,
600
+ goal: input.goal,
601
+ });
602
+ }
603
+ catch (error) {
604
+ const failed = getRoomRecord(record.room_id);
605
+ if (failed)
606
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
607
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
608
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
609
+ throw error;
610
+ }
364
611
  }
365
612
  else {
366
613
  record = activateRoom(record.room_id);
@@ -382,12 +629,11 @@ export function registerTemplateCommands(parent, cOpt) {
382
629
  return;
383
630
  }
384
631
  if (!templates.length) {
385
- console.log('No templates configured.');
632
+ console.log('No templates configured. Run interactive ours-fleet init to review and create a complete default setup.');
386
633
  return;
387
634
  }
388
635
  for (const t of templates) {
389
- const tag = t.builtin ? ' (built-in)' : '';
390
- console.log(`${t.name}@${t.version}${tag} ${t.description}`);
636
+ console.log(`${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
391
637
  }
392
638
  }
393
639
  catch (e) {
@@ -405,11 +651,13 @@ export function registerTemplateCommands(parent, cOpt) {
405
651
  return;
406
652
  }
407
653
  console.log(`${t.name}@${t.version} ${t.description}`);
654
+ console.log(`Source: ${t.sourceFile ?? 'manifest'}`);
408
655
  if (t.contract)
409
656
  console.log(`\nContract:\n${t.contract}`);
410
657
  console.log(`\nMembers:`);
411
658
  for (const m of t.members)
412
- console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
659
+ console.log(` ${m.slot}: ${m.count}× ${m.role} (`
660
+ + `agent template: ${m.agent_template})`);
413
661
  console.log(`\nContent hash: ${t.content_hash}`);
414
662
  }
415
663
  catch (e) {
@@ -452,17 +700,35 @@ export function registerTaskCommands(parent, cOpt) {
452
700
  .option('--brief-file <path>', 'task brief from file')
453
701
  .option('--backlog', 'create in backlog (do not start immediately)')
454
702
  .option('--no-room', 'create task without a room')
703
+ .option('--anonymous', 'create an anonymous Cowork room')
704
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
455
705
  .option('--idempotency-key <key>', 'idempotency key')
456
706
  .option('--list <name>', 'task list (default: default)')
707
+ .option('--members-file <path>', 'typed YAML member overrides')
708
+ .option('--member <slot>', 'begin a typed member override block')
709
+ .option('--agent-template <id>', 'Agent Template for the current member block')
710
+ .option('--brain <id>', 'Brain preset for the current member block')
711
+ .option('--role <id>', 'Role preset for the current member block')
712
+ .option('--approval <mode>', 'approval for the current member block')
713
+ .option('--filesystem <mode>', 'filesystem for the current member block')
714
+ .option('--unattended <mode>', 'unattended policy for the current member block')
715
+ .option('--cwd <path>', 'working directory for the current member block')
716
+ .option('--model <id>', 'model for the current member block')
717
+ .option('--effort <level>', 'reasoning effort for the current member block')
718
+ .option('--loops-file <path>', 'owner-only loops: YAML for the current temporary member')
719
+ .option('--no-loops', 'disable loops for the current temporary member')
457
720
  .option('--json', 'JSON output')
458
- .action(async (opts) => {
721
+ .action(async (opts, command) => {
459
722
  try {
723
+ const members = cliMemberOverrides(opts.membersFile, commandArgv(command));
460
724
  const record = await taskRoomService(opts.configuration).createTask({
461
725
  actor: { kind: 'local_control', surface: 'cli' }, title: opts.title,
462
726
  brief: opts.brief, briefFile: opts.briefFile, template: opts.template,
463
727
  backlog: opts.backlog, noRoom: opts.room === false,
728
+ anonymous: cliAnonymousOverride(commandArgv(command)),
464
729
  idempotencyKey: opts.idempotencyKey, origin: { type: 'cli' },
465
730
  list: opts.list,
731
+ members,
466
732
  });
467
733
  if (opts.json) {
468
734
  console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
@@ -487,7 +753,7 @@ export function registerTaskCommands(parent, cOpt) {
487
753
  .option('--list <name>', 'filter by task list')
488
754
  .option('--group-by-list', 'group deterministic results by list (JSON)')
489
755
  .option('--json', 'JSON output')
490
- .action((opts) => {
756
+ .action(async (opts) => {
491
757
  try {
492
758
  let stateFilter;
493
759
  if (opts.state && opts.state !== 'all') {
@@ -513,9 +779,10 @@ export function registerTaskCommands(parent, cOpt) {
513
779
  taskCmd.command('lists')
514
780
  .description('list named task lists')
515
781
  .option('--json', 'JSON output')
516
- .action((opts) => {
782
+ .action(async (opts) => {
517
783
  try {
518
- const lists = taskRoomService().listTaskLists();
784
+ const service = taskRoomService(opts.configuration);
785
+ const lists = service.listTaskLists();
519
786
  if (opts.json) {
520
787
  console.log(JSON.stringify({ schema_version: 1, lists }, null, 2));
521
788
  return;
@@ -606,9 +873,10 @@ export function registerTaskCommands(parent, cOpt) {
606
873
  taskCmd.command('show <id>')
607
874
  .description('show task details')
608
875
  .option('--json', 'JSON output')
609
- .action((id, opts) => {
876
+ .action(async (id, opts) => {
610
877
  try {
611
- const { task: t, orchestration: room } = taskRoomService().getTask(id);
878
+ const service = taskRoomService();
879
+ const { task: t, orchestration: room } = service.getTask(id);
612
880
  if (opts.json) {
613
881
  console.log(JSON.stringify({
614
882
  schema_version: 1, task: t, orchestration: room ?? null,
@@ -633,7 +901,9 @@ export function registerTaskCommands(parent, cOpt) {
633
901
  ],
634
902
  sections: [
635
903
  ...(t.member_roles.length ? [{
636
- heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`),
904
+ heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`
905
+ + seatConfigurationSuffix(room?.member_seats
906
+ .find(seat => seat.role_name === m.name)?.launch?.presentation)),
637
907
  }] : []),
638
908
  ...(room ? roomStartupSections(room) : []),
639
909
  ],
@@ -646,12 +916,30 @@ export function registerTaskCommands(parent, cOpt) {
646
916
  }
647
917
  });
648
918
  cOpt(taskCmd.command('start <id>'))
649
- .description('start a backlog task')
919
+ .description('idempotently select a plan, provision, and start a task')
920
+ .option('--template <name>', 'room template')
921
+ .option('--anonymous', 'create an anonymous Cowork room')
922
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
923
+ .option('--members-file <path>', 'typed YAML member overrides')
924
+ .option('--member <slot>', 'begin a typed member override block')
925
+ .option('--agent-template <id>', 'Agent Template for current member')
926
+ .option('--brain <id>', 'Brain preset for current member')
927
+ .option('--role <id>', 'Role preset for current member')
928
+ .option('--approval <mode>', 'approval for current member')
929
+ .option('--filesystem <mode>', 'filesystem for current member')
930
+ .option('--unattended <mode>', 'unattended policy for current member')
931
+ .option('--cwd <path>', 'working directory for current member')
932
+ .option('--model <id>', 'model for current member')
933
+ .option('--effort <level>', 'reasoning effort for current member')
934
+ .option('--loops-file <path>', 'owner-only loops: YAML for current member')
935
+ .option('--no-loops', 'disable loops for current member')
650
936
  .option('--json', 'JSON output')
651
- .action(async (id, opts) => {
937
+ .action(async (id, opts, command) => {
652
938
  try {
653
939
  const t = await taskRoomService(opts.configuration).startTask({
654
940
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
941
+ template: opts.template, members: cliMemberOverrides(opts.membersFile, commandArgv(command)),
942
+ anonymous: cliAnonymousOverride(commandArgv(command)),
655
943
  });
656
944
  if (opts.json) {
657
945
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
@@ -676,9 +964,11 @@ export function registerTaskCommands(parent, cOpt) {
676
964
  .option('--json', 'JSON output')
677
965
  .action((id, opts) => {
678
966
  try {
967
+ const previous = getTask(id).state;
679
968
  const t = taskRoomService().blockTask({
680
969
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
681
970
  });
971
+ auditTask('block', t, previous);
682
972
  if (opts.json) {
683
973
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
684
974
  return;
@@ -696,9 +986,12 @@ export function registerTaskCommands(parent, cOpt) {
696
986
  .option('--json', 'JSON output')
697
987
  .action((id, opts) => {
698
988
  try {
989
+ const prior = getTask(id);
990
+ const previous = prior.state;
699
991
  const t = taskRoomService().unblockTask({
700
992
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
701
993
  });
994
+ auditTask('unblock', t, previous, t.state, prior.blocked?.at);
702
995
  if (opts.json) {
703
996
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
704
997
  return;
@@ -716,9 +1009,11 @@ export function registerTaskCommands(parent, cOpt) {
716
1009
  .option('--json', 'JSON output')
717
1010
  .action((id, opts) => {
718
1011
  try {
1012
+ const previous = getTask(id).state;
719
1013
  const t = taskRoomService().reviewTask({
720
1014
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
721
1015
  });
1016
+ auditTask('review', t, previous);
722
1017
  if (opts.json) {
723
1018
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
724
1019
  return;
@@ -738,6 +1033,7 @@ export function registerTaskCommands(parent, cOpt) {
738
1033
  .option('--json', 'JSON output')
739
1034
  .action(async (id, opts) => {
740
1035
  try {
1036
+ const previous = getTask(id).state;
741
1037
  let summary = opts.summary;
742
1038
  if (opts.summaryFile)
743
1039
  summary = readFileSync(opts.summaryFile, 'utf8');
@@ -745,10 +1041,13 @@ export function registerTaskCommands(parent, cOpt) {
745
1041
  const plan = await taskRoomService(opts.configuration).completeTask({
746
1042
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
747
1043
  });
1044
+ if (plan.task.terminal_intent?.status === 'pending')
1045
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
748
1046
  const settled = plan.settlementRequired
749
1047
  ? await launchTaskSettleWorker(id, opts.configuration)
750
1048
  : { task: plan.task, timedOut: false };
751
1049
  const t = settled.task;
1050
+ auditTask('done', t, previous);
752
1051
  if (opts.json) {
753
1052
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
754
1053
  return;
@@ -776,13 +1075,17 @@ export function registerTaskCommands(parent, cOpt) {
776
1075
  try {
777
1076
  if (id !== confirmId)
778
1077
  throw taskRoomPublicError('task_confirmation_mismatch');
1078
+ const previous = getTask(id).state;
779
1079
  const plan = await taskRoomService(opts.configuration).cancelTask({
780
1080
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
781
1081
  });
1082
+ if (plan.task.terminal_intent?.status === 'pending')
1083
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
782
1084
  const settled = plan.settlementRequired
783
1085
  ? await launchTaskSettleWorker(id, opts.configuration)
784
1086
  : { task: plan.task, timedOut: false };
785
1087
  const t = settled.task;
1088
+ auditTask('cancel', t, previous);
786
1089
  if (opts.json) {
787
1090
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
788
1091
  return;
@@ -804,21 +1107,46 @@ export function registerTaskCommands(parent, cOpt) {
804
1107
  }
805
1108
  });
806
1109
  cOpt(taskCmd.command('delete <id> <confirm-id>'))
807
- .description('delete a done task from the backlog (requires ID twice for confirmation)')
1110
+ .description('permanently delete a task in any lifecycle state (requires ID twice for confirmation)')
808
1111
  .option('--json', 'JSON output')
809
- .action((id, confirmId, opts) => {
1112
+ .action(async (id, confirmId, opts) => {
810
1113
  try {
811
1114
  if (id !== confirmId)
812
1115
  throw taskRoomPublicError('task_confirmation_mismatch');
813
- const deleted = taskRoomService().deleteTask({
1116
+ const accepted = await taskRoomService(opts.configuration).requestTaskDeletion({
814
1117
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
815
1118
  });
1119
+ if (accepted.status === 'already_absent') {
1120
+ if (opts.json) {
1121
+ console.log(JSON.stringify({
1122
+ schema_version: 1, task_id: id, deleted: false, already_absent: true,
1123
+ }, null, 2));
1124
+ return;
1125
+ }
1126
+ console.log(renderMarkdownResult({
1127
+ icon: '🗑️', title: 'Task already absent',
1128
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1129
+ }));
1130
+ return;
1131
+ }
1132
+ const settled = await launchTaskDeleteWorker(id, opts.configuration);
816
1133
  if (opts.json) {
817
- console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
1134
+ console.log(JSON.stringify({
1135
+ schema_version: 1, task_id: id, accepted: true,
1136
+ deleted: settled.deleted, pending: settled.timedOut,
1137
+ }, null, 2));
1138
+ return;
1139
+ }
1140
+ if (!settled.deleted) {
1141
+ console.log(renderMarkdownFailure({
1142
+ kind: 'pending', subject: `task delete ${id} ${id}`,
1143
+ detail: 'The deletion was accepted and cleanup is still settling.',
1144
+ action: `Re-run ours-fleet task delete ${id} ${id} or ours-fleet task recover ${id}.`,
1145
+ }));
818
1146
  return;
819
1147
  }
820
1148
  console.log(renderMarkdownResult({
821
- icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
1149
+ icon: '🗑️', title: 'Task deleted',
822
1150
  fields: [{ label: 'ID', value: id, kind: 'code' }],
823
1151
  }));
824
1152
  }
@@ -833,9 +1161,29 @@ export function registerTaskCommands(parent, cOpt) {
833
1161
  .option('--json', 'JSON output')
834
1162
  .action(async (id, opts) => {
835
1163
  try {
1164
+ const previous = getTask(id).state;
836
1165
  const app = taskRoomService(opts.configuration);
837
1166
  const actor = { kind: 'local_control', surface: 'cli' };
838
1167
  const begin = await app.beginTaskRecovery({ actor, taskId: id });
1168
+ if (begin.kind === 'deletion_worker_required') {
1169
+ const settled = await launchTaskDeleteWorker(id, opts.configuration);
1170
+ const payload = { schema_version: 1, task_id: id, deleted: settled.deleted };
1171
+ if (opts.json) {
1172
+ console.log(JSON.stringify(payload, null, 2));
1173
+ return;
1174
+ }
1175
+ console.log(settled.deleted
1176
+ ? renderMarkdownResult({
1177
+ icon: '🗑️', title: 'Task deletion completed by recovery',
1178
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1179
+ })
1180
+ : renderMarkdownFailure({
1181
+ kind: 'pending', subject: `task recover ${id}`,
1182
+ detail: 'The task is pending deletion and cleanup is still settling.',
1183
+ action: `Run ours-fleet task delete ${id} ${id} to retry.`,
1184
+ }));
1185
+ return;
1186
+ }
839
1187
  const recovered = begin.kind === 'terminal_worker_required'
840
1188
  ? await (async () => {
841
1189
  const settled = await launchTaskSettleWorker(id, opts.configuration);
@@ -843,6 +1191,7 @@ export function registerTaskCommands(parent, cOpt) {
843
1191
  })()
844
1192
  : begin.result;
845
1193
  const t = recovered.task;
1194
+ auditTask('recover', t, previous);
846
1195
  const room = recovered.room;
847
1196
  const recoveryActions = recovered.issues.map(issue => {
848
1197
  if (issue.code === 'terminal_pending')
@@ -913,6 +1262,35 @@ export function registerTaskCommands(parent, cOpt) {
913
1262
  dieTaskRoom(e);
914
1263
  }
915
1264
  });
1265
+ cOpt(taskCmd.command('_settle_delete <id>', { hidden: true }))
1266
+ .description('internal: settle a previously accepted task deletion')
1267
+ .option('--json', 'JSON output')
1268
+ .action(async (id, opts) => {
1269
+ try {
1270
+ const app = taskRoomService(opts.configuration);
1271
+ const result = await app.settleTaskDeletion({
1272
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
1273
+ });
1274
+ if (opts.json) {
1275
+ console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
1276
+ return;
1277
+ }
1278
+ console.log(renderMarkdownResult({
1279
+ icon: '🗑️', title: result.deleted ? 'Task deletion settled' : 'Task already absent',
1280
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1281
+ }));
1282
+ }
1283
+ catch (e) {
1284
+ await taskRoomService(opts.configuration).recordDeletionError({
1285
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
1286
+ error: errorText(e),
1287
+ recoveryHint: `External delete worker failed. Retry task delete ${id} ${id}.`,
1288
+ }).catch(() => { });
1289
+ if (opts.json)
1290
+ die(e);
1291
+ dieTaskRoom(e);
1292
+ }
1293
+ });
916
1294
  cOpt(taskCmd.command('_recover <id>', { hidden: true }))
917
1295
  .description('internal: settle and continue task recovery')
918
1296
  .option('--json', 'JSON output')
@@ -921,6 +1299,11 @@ export function registerTaskCommands(parent, cOpt) {
921
1299
  const actor = { kind: 'internal_worker', surface: 'cli' };
922
1300
  try {
923
1301
  const begin = await app.beginTaskRecovery({ actor, taskId: id });
1302
+ if (begin.kind === 'deletion_worker_required') {
1303
+ const settled = await app.settleTaskDeletion({ actor, taskId: id });
1304
+ console.log(JSON.stringify({ schema_version: 1, deletion: settled }, null, 2));
1305
+ return;
1306
+ }
924
1307
  const result = begin.kind === 'terminal_worker_required'
925
1308
  ? await (async () => {
926
1309
  try {
@@ -945,15 +1328,35 @@ export function registerTaskCommands(parent, cOpt) {
945
1328
  }
946
1329
  });
947
1330
  cOpt(taskCmd.command('work <id>'))
948
- .description('ensure a task has a room and agents running')
1331
+ .description('deprecated alias for task start')
949
1332
  .option('--template <name>', 'room template')
1333
+ .option('--anonymous', 'create an anonymous Cowork room')
1334
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
1335
+ .option('--members-file <path>', 'typed YAML member overrides')
1336
+ .option('--member <slot>', 'begin a typed member override block')
1337
+ .option('--agent-template <id>', 'Agent Template for current member')
1338
+ .option('--brain <id>', 'Brain preset for current member')
1339
+ .option('--role <id>', 'Role preset for current member')
1340
+ .option('--approval <mode>', 'approval for current member')
1341
+ .option('--filesystem <mode>', 'filesystem for current member')
1342
+ .option('--unattended <mode>', 'unattended policy for current member')
1343
+ .option('--cwd <path>', 'working directory for current member')
1344
+ .option('--model <id>', 'model for current member')
1345
+ .option('--effort <level>', 'reasoning effort for current member')
1346
+ .option('--loops-file <path>', 'owner-only loops: YAML for current member')
1347
+ .option('--no-loops', 'disable loops for current member')
950
1348
  .option('--json', 'JSON output')
951
- .action(async (id, opts) => {
1349
+ .action(async (id, opts, command) => {
952
1350
  try {
1351
+ const previous = getTask(id).state;
1352
+ console.error('warning: `fleet task work` is deprecated; use `fleet task start`');
953
1353
  const result = await taskRoomService(opts.configuration).ensureTaskWork({
954
1354
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, template: opts.template,
1355
+ members: cliMemberOverrides(opts.membersFile, commandArgv(command)),
1356
+ anonymous: cliAnonymousOverride(commandArgv(command)),
955
1357
  });
956
1358
  const t = result.task;
1359
+ auditTask('work', t, previous);
957
1360
  if (result.status === 'already_active') {
958
1361
  if (opts.json) {
959
1362
  console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
@@ -992,6 +1395,7 @@ export function registerTaskCommands(parent, cOpt) {
992
1395
  .option('--json', 'JSON output')
993
1396
  .action(async (id, opts) => {
994
1397
  try {
1398
+ const previous = getTask(id).state;
995
1399
  let summary = opts.summary;
996
1400
  if (opts.summaryFile)
997
1401
  summary = readFileSync(opts.summaryFile, 'utf8');
@@ -999,10 +1403,13 @@ export function registerTaskCommands(parent, cOpt) {
999
1403
  const plan = await taskRoomService(opts.configuration).finishTask({
1000
1404
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
1001
1405
  });
1406
+ if (plan.task.terminal_intent?.status === 'pending')
1407
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
1002
1408
  const settled = plan.settlementRequired
1003
1409
  ? await launchTaskSettleWorker(id, opts.configuration)
1004
1410
  : { task: plan.task, timedOut: false };
1005
1411
  const t = settled.task;
1412
+ auditTask('finish', t, previous);
1006
1413
  if (opts.json) {
1007
1414
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
1008
1415
  return;
@@ -1032,15 +1439,32 @@ export function registerRoomCommands(parent, cOpt) {
1032
1439
  .description('create a standalone room')
1033
1440
  .requiredOption('--name <name>', 'room name')
1034
1441
  .option('--template <name>', 'room template')
1442
+ .option('--anonymous', 'create an anonymous Cowork room')
1443
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
1035
1444
  .option('--goal <text>', 'room goal')
1036
1445
  .option('--brief <text>', 'room briefing')
1037
1446
  .option('--brief-file <path>', 'room briefing from file')
1447
+ .option('--members-file <path>', 'typed YAML member overrides')
1448
+ .option('--member <slot>', 'begin a typed member override block')
1449
+ .option('--agent-template <id>', 'Agent Template for current member')
1450
+ .option('--brain <id>', 'Brain preset for current member')
1451
+ .option('--role <id>', 'Role preset for current member')
1452
+ .option('--approval <mode>', 'approval for current member')
1453
+ .option('--filesystem <mode>', 'filesystem for current member')
1454
+ .option('--unattended <mode>', 'unattended policy for current member')
1455
+ .option('--cwd <path>', 'working directory for current member')
1456
+ .option('--model <id>', 'model for current member')
1457
+ .option('--effort <level>', 'reasoning effort for current member')
1458
+ .option('--loops-file <path>', 'owner-only loops: YAML for current member')
1459
+ .option('--no-loops', 'disable loops for current member')
1038
1460
  .option('--json', 'JSON output')
1039
- .action(async (opts) => {
1461
+ .action(async (opts, command) => {
1040
1462
  try {
1041
1463
  const record = await taskRoomService(opts.configuration).createRoom({
1042
1464
  actor: { kind: 'local_control', surface: 'cli' }, name: opts.name,
1043
1465
  template: opts.template, goal: opts.goal, brief: opts.brief, briefFile: opts.briefFile,
1466
+ members: cliMemberOverrides(opts.membersFile, commandArgv(command)),
1467
+ anonymous: cliAnonymousOverride(commandArgv(command)),
1044
1468
  });
1045
1469
  if (opts.json) {
1046
1470
  console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
@@ -1109,7 +1533,9 @@ export function registerRoomCommands(parent, cOpt) {
1109
1533
  ],
1110
1534
  sections: [
1111
1535
  ...(cowork.seats.length ? [{
1112
- heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`),
1536
+ heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
1537
+ + seatConfigurationSuffix(r?.member_seats
1538
+ .find(seat => seat.identity_cid === s.identity_cid)?.launch?.presentation)),
1113
1539
  }] : []),
1114
1540
  ...(r ? roomStartupSections(r) : []),
1115
1541
  ],
@@ -1126,7 +1552,7 @@ export function registerRoomCommands(parent, cOpt) {
1126
1552
  .option('--json', 'JSON output')
1127
1553
  .action(async (id, opts) => {
1128
1554
  try {
1129
- const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
1555
+ const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
1130
1556
  const url = `http://localhost:4460/room/${id}`;
1131
1557
  if (opts.json) {
1132
1558
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
@@ -1171,7 +1597,9 @@ export function registerRoomCommands(parent, cOpt) {
1171
1597
  ],
1172
1598
  sections: [{
1173
1599
  heading: 'Members',
1174
- ...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`) }
1600
+ ...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
1601
+ + seatConfigurationSuffix(r?.member_seats
1602
+ .find(seat => seat.identity_cid === s.identity_cid)?.launch?.presentation)) }
1175
1603
  : { items: ['No members found.'] }),
1176
1604
  }],
1177
1605
  }));
@@ -1186,10 +1614,16 @@ export function registerRoomCommands(parent, cOpt) {
1186
1614
  try {
1187
1615
  if (id !== confirmId)
1188
1616
  throw taskRoomPublicError('room_confirmation_mismatch');
1617
+ const previous = getRoomRecord(id);
1189
1618
  await taskRoomService(opts.configuration).requestRoomDeletion({
1190
1619
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1191
1620
  });
1621
+ const closing = getRoomRecord(id);
1622
+ if (previous && closing && previous.state !== closing.state)
1623
+ auditRoom('close', closing, previous.state, closing.state);
1192
1624
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1625
+ if (closing)
1626
+ auditRoom('delete', closing, closing.state, settled.deleted ? 'deleted' : 'closing');
1193
1627
  if (opts.json) {
1194
1628
  console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
1195
1629
  return;
@@ -1226,11 +1660,14 @@ export function registerRoomCommands(parent, cOpt) {
1226
1660
  .option('--json', 'JSON output')
1227
1661
  .action(async (id, opts) => {
1228
1662
  try {
1663
+ const previous = getRoomRecord(id);
1229
1664
  const recovered = await taskRoomService(opts.configuration).recoverRoom({
1230
1665
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1231
1666
  });
1232
1667
  if (recovered.kind === 'deletion_worker_required') {
1233
1668
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1669
+ if (previous)
1670
+ auditRoom('recover', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
1234
1671
  if (opts.json) {
1235
1672
  console.log(JSON.stringify({
1236
1673
  schema_version: 1, room: null, orchestration: null,
@@ -1251,6 +1688,8 @@ export function registerRoomCommands(parent, cOpt) {
1251
1688
  return;
1252
1689
  }
1253
1690
  const { room: cowork, orchestration: r, issues: actions } = recovered;
1691
+ if (r)
1692
+ auditRoom('recover', r, previous?.state ?? 'unresolved', r.state);
1254
1693
  if (opts.json) {
1255
1694
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
1256
1695
  return;