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

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 (199) hide show
  1. package/README.md +169 -54
  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 +426 -78
  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 +227 -73
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +67 -2
  22. package/dist/config.js +260 -30
  23. package/dist/daemon-recovery.d.ts +93 -0
  24. package/dist/daemon-recovery.js +328 -0
  25. package/dist/docs.d.ts +1 -1
  26. package/dist/docs.js +171 -66
  27. package/dist/doctor.d.ts +2 -0
  28. package/dist/doctor.js +54 -4
  29. package/dist/fleet-command-audit.d.ts +165 -0
  30. package/dist/fleet-command-audit.js +543 -0
  31. package/dist/fleet-proxy.d.ts +7 -2
  32. package/dist/fleet-proxy.js +30 -8
  33. package/dist/harness/agent-session.d.ts +5 -0
  34. package/dist/harness/claude-code-session.d.ts +2 -0
  35. package/dist/harness/claude-code-session.js +6 -1
  36. package/dist/harness/claude-code.js +2 -0
  37. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  38. package/dist/harness/codex-app-server-proxy.js +348 -14
  39. package/dist/harness/codex-session.d.ts +11 -2
  40. package/dist/harness/codex-session.js +22 -2
  41. package/dist/harness/codex.d.ts +5 -1
  42. package/dist/harness/codex.js +110 -10
  43. package/dist/harness/types.d.ts +1 -1
  44. package/dist/index.d.ts +5 -4
  45. package/dist/index.js +4 -3
  46. package/dist/init-guidance.d.ts +1 -1
  47. package/dist/init-guidance.js +4 -3
  48. package/dist/init-wizard.d.ts +98 -0
  49. package/dist/init-wizard.js +629 -0
  50. package/dist/lifecycle-summary.d.ts +118 -0
  51. package/dist/lifecycle-summary.js +161 -0
  52. package/dist/loops/config.js +3 -2
  53. package/dist/model-env.d.ts +2 -2
  54. package/dist/model-env.js +1 -1
  55. package/dist/model-recovery.d.ts +1 -1
  56. package/dist/ops.js +2 -17
  57. package/dist/owner-channel/channel.d.ts +58 -0
  58. package/dist/owner-channel/channel.js +321 -42
  59. package/dist/owner-channel/commands.d.ts +6 -1
  60. package/dist/owner-channel/commands.js +34 -19
  61. package/dist/owner-channel/state.d.ts +2 -0
  62. package/dist/owner-channel/state.js +8 -2
  63. package/dist/permissions.js +1 -1
  64. package/dist/preset-bootstrap.d.ts +10 -0
  65. package/dist/preset-bootstrap.js +85 -0
  66. package/dist/preset-migration.d.ts +42 -0
  67. package/dist/preset-migration.js +414 -0
  68. package/dist/reports/artifact.d.ts +10 -0
  69. package/dist/reports/artifact.js +32 -0
  70. package/dist/reports/index.d.ts +5 -0
  71. package/dist/reports/index.js +5 -0
  72. package/dist/reports/render.d.ts +6 -0
  73. package/dist/reports/render.js +240 -0
  74. package/dist/reports/service.d.ts +42 -0
  75. package/dist/reports/service.js +57 -0
  76. package/dist/reports/tasks.d.ts +52 -0
  77. package/dist/reports/tasks.js +177 -0
  78. package/dist/reports/types.d.ts +315 -0
  79. package/dist/reports/types.js +6 -0
  80. package/dist/resolved-plan.js +4 -0
  81. package/dist/rooms-tasks/cli.d.ts +2 -0
  82. package/dist/rooms-tasks/cli.js +503 -75
  83. package/dist/rooms-tasks/close.d.ts +9 -1
  84. package/dist/rooms-tasks/close.js +20 -4
  85. package/dist/rooms-tasks/config.js +16 -14
  86. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  87. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  88. package/dist/rooms-tasks/deletion.d.ts +51 -0
  89. package/dist/rooms-tasks/deletion.js +217 -0
  90. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  91. package/dist/rooms-tasks/external-worker.js +14 -9
  92. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  93. package/dist/rooms-tasks/launch-snapshot.js +176 -0
  94. package/dist/rooms-tasks/member-overrides.d.ts +27 -0
  95. package/dist/rooms-tasks/member-overrides.js +170 -0
  96. package/dist/rooms-tasks/provision.js +179 -49
  97. package/dist/rooms-tasks/room-state.d.ts +1 -0
  98. package/dist/rooms-tasks/room-state.js +12 -1
  99. package/dist/rooms-tasks/task-state.d.ts +115 -3
  100. package/dist/rooms-tasks/task-state.js +361 -11
  101. package/dist/rooms-tasks/templates.d.ts +4 -2
  102. package/dist/rooms-tasks/templates.js +42 -78
  103. package/dist/rooms-tasks/terminal.d.ts +3 -0
  104. package/dist/rooms-tasks/terminal.js +7 -3
  105. package/dist/rooms-tasks/types.d.ts +94 -17
  106. package/dist/rooms-tasks/types.js +14 -4
  107. package/dist/runner.d.ts +9 -2
  108. package/dist/runner.js +107 -40
  109. package/dist/session/acp.d.ts +9 -1
  110. package/dist/session/acp.js +35 -9
  111. package/dist/session/arbiter.d.ts +2 -1
  112. package/dist/session/arbiter.js +2 -0
  113. package/dist/session/codex-app-server-transport.d.ts +53 -0
  114. package/dist/session/codex-app-server-transport.js +184 -0
  115. package/dist/session/codex-app-server.d.ts +122 -0
  116. package/dist/session/codex-app-server.js +1031 -0
  117. package/dist/session/control.d.ts +14 -1
  118. package/dist/session/control.js +38 -0
  119. package/dist/session/conversation-types.d.ts +4 -4
  120. package/dist/session/types.d.ts +24 -6
  121. package/dist/session/types.js +28 -0
  122. package/dist/spawn.d.ts +6 -31
  123. package/dist/spawn.js +57 -234
  124. package/dist/temp-lifecycle.d.ts +1 -1
  125. package/dist/watchdog/config.d.ts +10 -7
  126. package/dist/watchdog/config.js +29 -19
  127. package/dist/watchdog/run.js +4 -13
  128. package/dist/web/fleet-config-service.d.ts +1 -0
  129. package/dist/web/fleet-config-service.js +27 -8
  130. package/dist/web/runtime.js +11 -6
  131. package/dist/web/server.js +28 -1
  132. package/dist/web/topology-promote.js +8 -1
  133. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  134. package/dist/web-app/index.html +1 -1
  135. package/package.json +3 -2
  136. package/presets/brain-catalog.json +16 -0
  137. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  138. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  139. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  140. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  143. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  144. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  147. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  148. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  149. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  152. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  153. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  154. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  190. package/presets/fleet/roles/Coordinator.yaml +30 -0
  191. package/presets/fleet/roles/Critic.yaml +19 -0
  192. package/presets/fleet/roles/Developer.yaml +19 -0
  193. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  194. package/presets/fleet/room_templates/pair.yaml +10 -0
  195. package/presets/fleet/room_templates/single.yaml +7 -0
  196. package/presets/fleet/room_templates/team.yaml +12 -0
  197. package/presets/fleet.yaml +7 -0
  198. package/presets/manifest.json +6 -0
  199. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -1,14 +1,49 @@
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',
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], argv[i + 1]);
18
+ i += 1;
19
+ }
20
+ if (membersFile && grouped.length)
21
+ throw new Error('--members-file cannot be combined with grouped --member options');
22
+ if (membersFile)
23
+ return readMembersFile(membersFile);
24
+ return grouped.length ? parseGroupedMemberArgs(grouped) : undefined;
25
+ }
26
+ function commandArgv(command) {
27
+ let root = command;
28
+ while (root.parent)
29
+ root = root.parent;
30
+ return root.rawArgs ?? process.argv.slice(2);
31
+ }
32
+ function cliAnonymousOverride(argv) {
33
+ const enabled = argv.includes('--anonymous');
34
+ const disabled = argv.includes('--no-anonymous');
35
+ if (enabled && disabled)
36
+ throw new Error('--anonymous and --no-anonymous are mutually exclusive');
37
+ return enabled ? true : disabled ? false : undefined;
38
+ }
39
+ import { getTask, getDeletingTask, activateTask, TaskStateError, } from './task-state.js';
7
40
  import { createRoomRecord, getRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
8
41
  import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
9
42
  import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
10
43
  import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
11
44
  import { TaskListError } from './task-lists.js';
45
+ import { recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
46
+ import { renderAgentConfiguration } from '../lifecycle-summary.js';
12
47
  class TaskRoomPublicError extends Error {
13
48
  code;
14
49
  fields;
@@ -41,7 +76,7 @@ function taskRoomPublicFailure(error) {
41
76
  case 'template_not_found': return {
42
77
  legacy: `template not found: ${f.template ?? 'requested-template'}`, kind: 'not_found',
43
78
  detail: `The requested template ${f.template ?? ''}`.trim() + ' was not found.',
44
- action: 'Run ours-fleet template list and retry with an available template.',
79
+ action: 'Run interactive ours-fleet init to review and create a complete default setup, then run ours-fleet template list.',
45
80
  };
46
81
  case 'template_mismatch': return {
47
82
  legacy: `template ${f.requested ?? 'requested-template'} does not match room ${f.room ?? 'requested-room'}'s provisioned template ${f.provisioned ?? 'recorded-template'}`,
@@ -89,8 +124,29 @@ function die(e) {
89
124
  const msg = e instanceof TaskRoomPublicError
90
125
  ? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
91
126
  process.stderr.write(`error: ${msg}\n`);
127
+ recordFleetAuditFailure(classifyTaskRoomFailure(e));
92
128
  process.exit(1);
93
129
  }
130
+ function classifyTaskRoomFailure(e) {
131
+ if (e instanceof TaskRoomApplicationError)
132
+ e = taskRoomPublicError(e.code, e.fields);
133
+ if (e instanceof TaskRoomPublicError || e instanceof TaskListError || e instanceof ConfigError)
134
+ return { class: 'validation', effect: 'not_started' };
135
+ if (e instanceof TaskStateError) {
136
+ const known = /^task not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
137
+ || e.message === 'invalid task ID: expected the canonical 17-character lowercase ID'
138
+ || isKnownTaskStateMessage(e.message);
139
+ return known ? { class: 'validation', effect: 'not_started' }
140
+ : { class: 'runtime', effect: 'unknown' };
141
+ }
142
+ if (e instanceof RoomStateError) {
143
+ const known = /^room not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
144
+ || isKnownRoomStateMessage(e.message);
145
+ return known ? { class: 'validation', effect: 'not_started' }
146
+ : { class: 'runtime', effect: 'unknown' };
147
+ }
148
+ return { class: 'runtime', effect: 'unknown' };
149
+ }
94
150
  const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
95
151
  const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
96
152
  function isKnownTaskStateMessage(message) {
@@ -104,7 +160,7 @@ function isKnownTaskStateMessage(message) {
104
160
  new RegExp(`^task ${SAFE_ID_WORD} is not tied to room ${SAFE_ID_WORD}$`, 'u'),
105
161
  new RegExp(`^task ${SAFE_ID_WORD} has no terminal intent$`, 'u'),
106
162
  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'),
163
+ 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
164
  ].some(pattern => pattern.test(message));
109
165
  }
110
166
  function isKnownRoomStateMessage(message) {
@@ -127,6 +183,7 @@ function dieTaskRoom(e) {
127
183
  action: failure.action,
128
184
  });
129
185
  process.stderr.write(`${output}\n`);
186
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
130
187
  process.exit(1);
131
188
  }
132
189
  if (e instanceof TaskListError) {
@@ -142,6 +199,7 @@ function dieTaskRoom(e) {
142
199
  : 'Use a valid NFC-normalized name of at most 64 Unicode code points.',
143
200
  });
144
201
  process.stderr.write(`${output}\n`);
202
+ recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
145
203
  process.exit(1);
146
204
  }
147
205
  const taskMissing = e instanceof TaskStateError
@@ -167,6 +225,7 @@ function dieTaskRoom(e) {
167
225
  : 'Retry once; if it repeats, run ours-fleet doctor and inspect the role logs.',
168
226
  });
169
227
  process.stderr.write(`${output}\n`);
228
+ recordFleetAuditFailure(classifyTaskRoomFailure(e));
170
229
  process.exit(1);
171
230
  }
172
231
  const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
@@ -175,23 +234,118 @@ const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
175
234
  + ` — List ${markdownCode(t.list_name ?? 'default')}`
176
235
  + (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
177
236
  });
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
- });
237
+ const taskActionMarkdown = (title, task, fields = []) => {
238
+ recordFleetAuditResource('task', task.task_id);
239
+ if (task.room_id)
240
+ recordFleetAuditResource('room', task.room_id);
241
+ return renderMarkdownResult({
242
+ icon: '📋', title,
243
+ fields: [
244
+ { label: 'ID', value: task.task_id, kind: 'code' },
245
+ { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
246
+ { label: 'List', value: task.list_name ?? 'default', kind: 'code' },
247
+ ...fields,
248
+ ],
249
+ });
250
+ };
251
+ /**
252
+ * Shared launch-configuration line for CLI member listings. Pre-upgrade seats
253
+ * without a captured presentation keep the minimal listing rather than
254
+ * claiming resolved facts. Output is already escaped (markdownItems boundary).
255
+ */
256
+ function seatConfigurationSuffix(presentation) {
257
+ return presentation ? ` — ${renderAgentConfiguration(presentation)}` : '';
258
+ }
259
+ function safeSelectionSummary(definition, kind) {
260
+ const selected = definition?.[kind];
261
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
262
+ return 'unresolved';
263
+ const selection = selected;
264
+ if (selection.kind === 'ref' && typeof selection.id === 'string')
265
+ return `ref:${selection.id} (reference)`;
266
+ if (selection.kind === 'inline' && typeof selection.fingerprint === 'string')
267
+ return `inline:${selection.fingerprint} (inline)`;
268
+ if (typeof selection.ref === 'string')
269
+ return `ref:${selection.ref}`;
270
+ if (!Object.hasOwn(selection, 'inline'))
271
+ return 'unresolved';
272
+ const canonical = (value) => Array.isArray(value)
273
+ ? `[${value.map(canonical).join(',')}]`
274
+ : value && typeof value === 'object'
275
+ ? `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
276
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(',')}}`
277
+ : JSON.stringify(value);
278
+ const fingerprint = createHash('sha256').update(canonical(selection.inline)).digest('hex').slice(0, 16);
279
+ return `inline:sha256:${fingerprint}`;
280
+ }
281
+ function safePermissionsSummary(definition) {
282
+ const permissions = definition?.permissions;
283
+ if (!permissions || typeof permissions !== 'object' || Array.isArray(permissions))
284
+ return undefined;
285
+ const policy = permissions;
286
+ const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof policy[key] === 'string' ? [`${key}=${policy[key]}`] : []);
287
+ return fields.length ? fields.join(',') : undefined;
288
+ }
289
+ function auditTask(operation, task, previousState, newState = task.state, revision) {
290
+ const materialSameState = operation === 'block' || operation === 'unblock' || operation === 'settling';
291
+ if (previousState === newState && !materialSameState)
292
+ return;
293
+ recordFleetAuditResource('task', task.task_id);
294
+ if (task.room_id)
295
+ recordFleetAuditResource('room', task.room_id);
296
+ const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
297
+ const definitions = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.agent_definition]) ?? []);
298
+ const presentations = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.presentation]) ?? []);
299
+ const semanticOperation = operation === 'recover'
300
+ ? newState === 'active' ? 'work' : newState === 'done' ? 'done'
301
+ : newState === 'cancelled' ? 'cancel' : undefined
302
+ : operation;
303
+ if (!semanticOperation)
304
+ return;
305
+ recordFleetAuditPresentation({ kind: 'task', operation: semanticOperation, id: task.task_id, title: task.title,
306
+ previousState, newState,
307
+ revision: revision ?? task.blocked?.at ?? task.ended_at ?? task.started_at ?? task.created_at,
308
+ list: task.list_name ?? 'default',
309
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
310
+ roomId: task.room_id, agents: [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => ({ name: member.name,
311
+ brain: safeSelectionSummary(definitions.get(member.name), 'brain'),
312
+ role: safeSelectionSummary(definitions.get(member.name), 'role') === 'unresolved'
313
+ ? member.cowork_role : safeSelectionSummary(definitions.get(member.name), 'role'),
314
+ permissions: safePermissionsSummary(definitions.get(member.name)),
315
+ configuration: presentations.get(member.name) })) });
316
+ }
317
+ function auditRoom(operation, room, previousState, newState = room.state) {
318
+ if (previousState === newState)
319
+ return;
320
+ recordFleetAuditResource('room', room.room_id);
321
+ const semanticOperation = operation === 'recover'
322
+ ? newState === 'active' ? 'activate' : newState === 'deleted' ? 'delete'
323
+ : newState === 'closing' ? 'close' : undefined
324
+ : operation;
325
+ if (!semanticOperation)
326
+ return;
327
+ recordFleetAuditPresentation({ kind: 'room', operation: semanticOperation, id: room.room_id, previousState, newState,
328
+ revision: room.closed_at ?? room.activated_at ?? room.close?.accepted_at ?? room.created_at,
329
+ name: room.room_name, taskId: room.task_id,
330
+ template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
331
+ participants: [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(member => ({ name: member.role_name, id: member.identity_cid,
332
+ brain: safeSelectionSummary(member.launch?.agent_definition, 'brain'),
333
+ role: safeSelectionSummary(member.launch?.agent_definition, 'role') === 'unresolved'
334
+ ? member.cowork_role : safeSelectionSummary(member.launch?.agent_definition, 'role'),
335
+ permissions: safePermissionsSummary(member.launch?.agent_definition),
336
+ configuration: member.launch?.presentation })) });
337
+ }
338
+ const roomActionMarkdown = (title, room, fields = []) => {
339
+ recordFleetAuditResource('room', room.room_id);
340
+ return renderMarkdownResult({
341
+ icon: '🏠', title,
342
+ fields: [
343
+ { label: 'ID', value: room.room_id, kind: 'code' },
344
+ { label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
345
+ ...fields,
346
+ ],
347
+ });
348
+ };
195
349
  const PUBLIC_SETTLE_WAIT_MS = 60_000;
196
350
  const PUBLIC_SETTLE_POLL_MS = 100;
197
351
  const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
@@ -208,6 +362,10 @@ async function launchTaskSettleWorker(taskId, configPath) {
208
362
  actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
209
363
  recoveryHint: `External settle worker failed to start. Retry task recover ${taskId}.`,
210
364
  });
365
+ const task = getTask(taskId);
366
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
367
+ state: task.state, category: 'settlement_failed',
368
+ eventId: task.terminal_intent?.error_at ?? task.terminal_intent?.accepted_at ?? task.created_at });
211
369
  throw error;
212
370
  }
213
371
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -220,7 +378,54 @@ async function launchTaskSettleWorker(taskId, configPath) {
220
378
  }
221
379
  await sleep(PUBLIC_SETTLE_POLL_MS);
222
380
  }
223
- return { task: getTask(taskId), timedOut: true };
381
+ const task = getTask(taskId);
382
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
383
+ state: task.state, category: 'settlement_pending',
384
+ eventId: task.terminal_intent?.accepted_at ?? task.created_at });
385
+ return { task, timedOut: true };
386
+ }
387
+ /** Launch the deletion settle worker and wait boundedly for physical absence. */
388
+ async function launchTaskDeleteWorker(taskId, configPath) {
389
+ const readDeletion = () => {
390
+ try {
391
+ const task = getDeletingTask(taskId);
392
+ return { present: true, errorAt: task.deletion?.error_at, error: task.deletion?.error };
393
+ }
394
+ catch (error) {
395
+ // Only proven physical absence counts as deleted; anything else propagates.
396
+ if (error instanceof TaskStateError && /task not found/.test(error.message))
397
+ return { present: false };
398
+ throw error;
399
+ }
400
+ };
401
+ const before = readDeletion();
402
+ if (!before.present)
403
+ return { deleted: true, timedOut: false };
404
+ try {
405
+ await launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, configPath);
406
+ }
407
+ catch (error) {
408
+ await taskRoomService(configPath).recordDeletionError({
409
+ actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
410
+ recoveryHint: `External delete worker failed to start. Retry task delete ${taskId} ${taskId}.`,
411
+ }).catch(() => { });
412
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
413
+ state: 'deleting', category: 'settlement_failed', eventId: new Date().toISOString() });
414
+ throw error;
415
+ }
416
+ const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
417
+ while (Date.now() < deadline) {
418
+ const current = readDeletion();
419
+ if (!current.present)
420
+ return { deleted: true, timedOut: false };
421
+ if (current.errorAt !== before.errorAt && current.error) {
422
+ throw new TaskStateError(current.error);
423
+ }
424
+ await sleep(PUBLIC_SETTLE_POLL_MS);
425
+ }
426
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
427
+ state: 'deleting', category: 'settlement_pending', eventId: new Date().toISOString() });
428
+ return { deleted: false, timedOut: true };
224
429
  }
225
430
  async function launchRoomDeleteWorker(roomId, configPath) {
226
431
  const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
@@ -232,6 +437,11 @@ async function launchRoomDeleteWorker(roomId, configPath) {
232
437
  actor: { kind: 'local_control', surface: 'cli' }, roomId, error: errorText(error),
233
438
  recoveryHint: `External delete worker failed to start. Retry room delete ${roomId} ${roomId}.`,
234
439
  });
440
+ const room = getRoomRecord(roomId);
441
+ if (room)
442
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
443
+ state: room.state, category: 'cleanup_failed',
444
+ eventId: room.close?.error_at ?? room.close?.accepted_at ?? room.created_at });
235
445
  throw error;
236
446
  }
237
447
  const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
@@ -244,7 +454,12 @@ async function launchRoomDeleteWorker(roomId, configPath) {
244
454
  }
245
455
  await sleep(PUBLIC_SETTLE_POLL_MS);
246
456
  }
247
- return { deleted: getRoomRecord(roomId) === undefined, timedOut: true };
457
+ const remaining = getRoomRecord(roomId);
458
+ if (remaining)
459
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: roomId,
460
+ state: remaining.state, category: 'cleanup_pending',
461
+ eventId: remaining.close?.accepted_at ?? remaining.created_at });
462
+ return { deleted: remaining === undefined, timedOut: true };
248
463
  }
249
464
  function loadCfg(opts) {
250
465
  return loadConfig(opts.configuration);
@@ -271,7 +486,7 @@ function resolveRoomTemplate(cfg, name) {
271
486
  const template = resolveTemplate(name, allTemplates(cfg));
272
487
  if (!template)
273
488
  throw new Error(`template not found: ${name}`);
274
- return snapshotTemplate(template);
489
+ return snapshotTemplate(template, cfg.agentTemplates);
275
490
  }
276
491
  function durableTaskRoomTemplate(task, room) {
277
492
  const snapshot = room.template_snapshot;
@@ -315,22 +530,36 @@ async function provisionRoom(cfg, input) {
315
530
  throw new ConfigError('rooms.owner: public_invite or public_invite_file is required when attach_owner is enabled');
316
531
  const cowork = coworkFor(cfg);
317
532
  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);
533
+ const unlockSnapshot = input.template ? acquireLaunchSnapshotLock() : undefined;
534
+ let launchTemplate;
535
+ let record;
536
+ try {
537
+ launchTemplate = input.template
538
+ ? sealTemplateSnapshot(input.template, cfg.agentTemplates ?? {}) : undefined;
539
+ const briefing = input.brief?.trim() || launchTemplate?.contract?.trim() || goal;
540
+ const created = await cowork.createRoom({
541
+ room_name: input.name,
542
+ goal,
543
+ briefing,
544
+ quiet_membership: launchTemplate?.room?.quiet_membership,
545
+ anonymous: launchTemplate?.room?.anonymous,
546
+ });
547
+ record = createRoomRecord({
548
+ room_id: created.room_id,
549
+ room_name: input.name,
550
+ room_identity_cid: created.identity_cid,
551
+ task_id: input.taskId,
552
+ template_snapshot: launchTemplate,
553
+ });
554
+ input.onCreated?.(record);
555
+ }
556
+ catch (error) {
557
+ unlockSnapshot?.();
558
+ if (launchTemplate?.launch_snapshot_hash)
559
+ releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
560
+ throw error;
561
+ }
562
+ unlockSnapshot?.();
334
563
  record = advanceSaga(record.room_id, 'create_room', 1);
335
564
  if (attachOwner) {
336
565
  try {
@@ -346,21 +575,36 @@ async function provisionRoom(cfg, input) {
346
575
  setSagaError(record.room_id, error instanceof Error ? error.message : String(error), mismatch
347
576
  ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
348
577
  : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
578
+ const failed = getRoomRecord(record.room_id);
579
+ if (failed)
580
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
581
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
582
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
349
583
  throw error;
350
584
  }
351
585
  }
352
586
  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
- });
587
+ if (launchTemplate && launchTemplate.members.length > 0) {
588
+ try {
589
+ record = await provisionMembers({
590
+ cfg,
591
+ cowork,
592
+ roomId: record.room_id,
593
+ taskId: input.taskId,
594
+ template: launchTemplate,
595
+ binPath: getBinPath(),
596
+ brief: input.brief,
597
+ goal: input.goal,
598
+ });
599
+ }
600
+ catch (error) {
601
+ const failed = getRoomRecord(record.room_id);
602
+ if (failed)
603
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
604
+ id: failed.room_id, state: failed.state, category: 'provision_failed',
605
+ eventId: `${failed.created_at}:${failed.saga.phase}:${failed.saga.step_index}` });
606
+ throw error;
607
+ }
364
608
  }
365
609
  else {
366
610
  record = activateRoom(record.room_id);
@@ -382,12 +626,11 @@ export function registerTemplateCommands(parent, cOpt) {
382
626
  return;
383
627
  }
384
628
  if (!templates.length) {
385
- console.log('No templates configured.');
629
+ console.log('No templates configured. Run interactive ours-fleet init to review and create a complete default setup.');
386
630
  return;
387
631
  }
388
632
  for (const t of templates) {
389
- const tag = t.builtin ? ' (built-in)' : '';
390
- console.log(`${t.name}@${t.version}${tag} ${t.description}`);
633
+ console.log(`${t.name}@${t.version} ${t.description} [file: ${t.sourceFile ?? 'manifest'}]`);
391
634
  }
392
635
  }
393
636
  catch (e) {
@@ -405,11 +648,13 @@ export function registerTemplateCommands(parent, cOpt) {
405
648
  return;
406
649
  }
407
650
  console.log(`${t.name}@${t.version} ${t.description}`);
651
+ console.log(`Source: ${t.sourceFile ?? 'manifest'}`);
408
652
  if (t.contract)
409
653
  console.log(`\nContract:\n${t.contract}`);
410
654
  console.log(`\nMembers:`);
411
655
  for (const m of t.members)
412
- console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
656
+ console.log(` ${m.slot}: ${m.count}× ${m.role} (`
657
+ + `agent template: ${m.agent_template})`);
413
658
  console.log(`\nContent hash: ${t.content_hash}`);
414
659
  }
415
660
  catch (e) {
@@ -452,17 +697,33 @@ export function registerTaskCommands(parent, cOpt) {
452
697
  .option('--brief-file <path>', 'task brief from file')
453
698
  .option('--backlog', 'create in backlog (do not start immediately)')
454
699
  .option('--no-room', 'create task without a room')
700
+ .option('--anonymous', 'create an anonymous Cowork room')
701
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
455
702
  .option('--idempotency-key <key>', 'idempotency key')
456
703
  .option('--list <name>', 'task list (default: default)')
704
+ .option('--members-file <path>', 'typed YAML member overrides')
705
+ .option('--member <slot>', 'begin a typed member override block')
706
+ .option('--agent-template <id>', 'Agent Template for the current member block')
707
+ .option('--brain <id>', 'Brain preset for the current member block')
708
+ .option('--role <id>', 'Role preset for the current member block')
709
+ .option('--approval <mode>', 'approval for the current member block')
710
+ .option('--filesystem <mode>', 'filesystem for the current member block')
711
+ .option('--unattended <mode>', 'unattended policy for the current member block')
712
+ .option('--cwd <path>', 'working directory for the current member block')
713
+ .option('--model <id>', 'model for the current member block')
714
+ .option('--effort <level>', 'reasoning effort for the current member block')
457
715
  .option('--json', 'JSON output')
458
- .action(async (opts) => {
716
+ .action(async (opts, command) => {
459
717
  try {
718
+ const members = cliMemberOverrides(opts.membersFile, commandArgv(command));
460
719
  const record = await taskRoomService(opts.configuration).createTask({
461
720
  actor: { kind: 'local_control', surface: 'cli' }, title: opts.title,
462
721
  brief: opts.brief, briefFile: opts.briefFile, template: opts.template,
463
722
  backlog: opts.backlog, noRoom: opts.room === false,
723
+ anonymous: cliAnonymousOverride(commandArgv(command)),
464
724
  idempotencyKey: opts.idempotencyKey, origin: { type: 'cli' },
465
725
  list: opts.list,
726
+ members,
466
727
  });
467
728
  if (opts.json) {
468
729
  console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
@@ -487,7 +748,7 @@ export function registerTaskCommands(parent, cOpt) {
487
748
  .option('--list <name>', 'filter by task list')
488
749
  .option('--group-by-list', 'group deterministic results by list (JSON)')
489
750
  .option('--json', 'JSON output')
490
- .action((opts) => {
751
+ .action(async (opts) => {
491
752
  try {
492
753
  let stateFilter;
493
754
  if (opts.state && opts.state !== 'all') {
@@ -513,9 +774,10 @@ export function registerTaskCommands(parent, cOpt) {
513
774
  taskCmd.command('lists')
514
775
  .description('list named task lists')
515
776
  .option('--json', 'JSON output')
516
- .action((opts) => {
777
+ .action(async (opts) => {
517
778
  try {
518
- const lists = taskRoomService().listTaskLists();
779
+ const service = taskRoomService(opts.configuration);
780
+ const lists = service.listTaskLists();
519
781
  if (opts.json) {
520
782
  console.log(JSON.stringify({ schema_version: 1, lists }, null, 2));
521
783
  return;
@@ -606,9 +868,10 @@ export function registerTaskCommands(parent, cOpt) {
606
868
  taskCmd.command('show <id>')
607
869
  .description('show task details')
608
870
  .option('--json', 'JSON output')
609
- .action((id, opts) => {
871
+ .action(async (id, opts) => {
610
872
  try {
611
- const { task: t, orchestration: room } = taskRoomService().getTask(id);
873
+ const service = taskRoomService();
874
+ const { task: t, orchestration: room } = service.getTask(id);
612
875
  if (opts.json) {
613
876
  console.log(JSON.stringify({
614
877
  schema_version: 1, task: t, orchestration: room ?? null,
@@ -633,7 +896,9 @@ export function registerTaskCommands(parent, cOpt) {
633
896
  ],
634
897
  sections: [
635
898
  ...(t.member_roles.length ? [{
636
- heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`),
899
+ heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`
900
+ + seatConfigurationSuffix(room?.member_seats
901
+ .find(seat => seat.role_name === m.name)?.launch?.presentation)),
637
902
  }] : []),
638
903
  ...(room ? roomStartupSections(room) : []),
639
904
  ],
@@ -646,12 +911,28 @@ export function registerTaskCommands(parent, cOpt) {
646
911
  }
647
912
  });
648
913
  cOpt(taskCmd.command('start <id>'))
649
- .description('start a backlog task')
914
+ .description('idempotently select a plan, provision, and start a task')
915
+ .option('--template <name>', 'room template')
916
+ .option('--anonymous', 'create an anonymous Cowork room')
917
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
918
+ .option('--members-file <path>', 'typed YAML member overrides')
919
+ .option('--member <slot>', 'begin a typed member override block')
920
+ .option('--agent-template <id>', 'Agent Template for current member')
921
+ .option('--brain <id>', 'Brain preset for current member')
922
+ .option('--role <id>', 'Role preset for current member')
923
+ .option('--approval <mode>', 'approval for current member')
924
+ .option('--filesystem <mode>', 'filesystem for current member')
925
+ .option('--unattended <mode>', 'unattended policy for current member')
926
+ .option('--cwd <path>', 'working directory for current member')
927
+ .option('--model <id>', 'model for current member')
928
+ .option('--effort <level>', 'reasoning effort for current member')
650
929
  .option('--json', 'JSON output')
651
- .action(async (id, opts) => {
930
+ .action(async (id, opts, command) => {
652
931
  try {
653
932
  const t = await taskRoomService(opts.configuration).startTask({
654
933
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
934
+ template: opts.template, members: cliMemberOverrides(opts.membersFile, commandArgv(command)),
935
+ anonymous: cliAnonymousOverride(commandArgv(command)),
655
936
  });
656
937
  if (opts.json) {
657
938
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
@@ -676,9 +957,11 @@ export function registerTaskCommands(parent, cOpt) {
676
957
  .option('--json', 'JSON output')
677
958
  .action((id, opts) => {
678
959
  try {
960
+ const previous = getTask(id).state;
679
961
  const t = taskRoomService().blockTask({
680
962
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
681
963
  });
964
+ auditTask('block', t, previous);
682
965
  if (opts.json) {
683
966
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
684
967
  return;
@@ -696,9 +979,12 @@ export function registerTaskCommands(parent, cOpt) {
696
979
  .option('--json', 'JSON output')
697
980
  .action((id, opts) => {
698
981
  try {
982
+ const prior = getTask(id);
983
+ const previous = prior.state;
699
984
  const t = taskRoomService().unblockTask({
700
985
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
701
986
  });
987
+ auditTask('unblock', t, previous, t.state, prior.blocked?.at);
702
988
  if (opts.json) {
703
989
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
704
990
  return;
@@ -716,9 +1002,11 @@ export function registerTaskCommands(parent, cOpt) {
716
1002
  .option('--json', 'JSON output')
717
1003
  .action((id, opts) => {
718
1004
  try {
1005
+ const previous = getTask(id).state;
719
1006
  const t = taskRoomService().reviewTask({
720
1007
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
721
1008
  });
1009
+ auditTask('review', t, previous);
722
1010
  if (opts.json) {
723
1011
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
724
1012
  return;
@@ -738,6 +1026,7 @@ export function registerTaskCommands(parent, cOpt) {
738
1026
  .option('--json', 'JSON output')
739
1027
  .action(async (id, opts) => {
740
1028
  try {
1029
+ const previous = getTask(id).state;
741
1030
  let summary = opts.summary;
742
1031
  if (opts.summaryFile)
743
1032
  summary = readFileSync(opts.summaryFile, 'utf8');
@@ -745,10 +1034,13 @@ export function registerTaskCommands(parent, cOpt) {
745
1034
  const plan = await taskRoomService(opts.configuration).completeTask({
746
1035
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
747
1036
  });
1037
+ if (plan.task.terminal_intent?.status === 'pending')
1038
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
748
1039
  const settled = plan.settlementRequired
749
1040
  ? await launchTaskSettleWorker(id, opts.configuration)
750
1041
  : { task: plan.task, timedOut: false };
751
1042
  const t = settled.task;
1043
+ auditTask('done', t, previous);
752
1044
  if (opts.json) {
753
1045
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
754
1046
  return;
@@ -776,13 +1068,17 @@ export function registerTaskCommands(parent, cOpt) {
776
1068
  try {
777
1069
  if (id !== confirmId)
778
1070
  throw taskRoomPublicError('task_confirmation_mismatch');
1071
+ const previous = getTask(id).state;
779
1072
  const plan = await taskRoomService(opts.configuration).cancelTask({
780
1073
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
781
1074
  });
1075
+ if (plan.task.terminal_intent?.status === 'pending')
1076
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
782
1077
  const settled = plan.settlementRequired
783
1078
  ? await launchTaskSettleWorker(id, opts.configuration)
784
1079
  : { task: plan.task, timedOut: false };
785
1080
  const t = settled.task;
1081
+ auditTask('cancel', t, previous);
786
1082
  if (opts.json) {
787
1083
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
788
1084
  return;
@@ -804,21 +1100,46 @@ export function registerTaskCommands(parent, cOpt) {
804
1100
  }
805
1101
  });
806
1102
  cOpt(taskCmd.command('delete <id> <confirm-id>'))
807
- .description('delete a done task from the backlog (requires ID twice for confirmation)')
1103
+ .description('permanently delete a task in any lifecycle state (requires ID twice for confirmation)')
808
1104
  .option('--json', 'JSON output')
809
- .action((id, confirmId, opts) => {
1105
+ .action(async (id, confirmId, opts) => {
810
1106
  try {
811
1107
  if (id !== confirmId)
812
1108
  throw taskRoomPublicError('task_confirmation_mismatch');
813
- const deleted = taskRoomService().deleteTask({
1109
+ const accepted = await taskRoomService(opts.configuration).requestTaskDeletion({
814
1110
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
815
1111
  });
1112
+ if (accepted.status === 'already_absent') {
1113
+ if (opts.json) {
1114
+ console.log(JSON.stringify({
1115
+ schema_version: 1, task_id: id, deleted: false, already_absent: true,
1116
+ }, null, 2));
1117
+ return;
1118
+ }
1119
+ console.log(renderMarkdownResult({
1120
+ icon: '🗑️', title: 'Task already absent',
1121
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1122
+ }));
1123
+ return;
1124
+ }
1125
+ const settled = await launchTaskDeleteWorker(id, opts.configuration);
816
1126
  if (opts.json) {
817
- console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
1127
+ console.log(JSON.stringify({
1128
+ schema_version: 1, task_id: id, accepted: true,
1129
+ deleted: settled.deleted, pending: settled.timedOut,
1130
+ }, null, 2));
1131
+ return;
1132
+ }
1133
+ if (!settled.deleted) {
1134
+ console.log(renderMarkdownFailure({
1135
+ kind: 'pending', subject: `task delete ${id} ${id}`,
1136
+ detail: 'The deletion was accepted and cleanup is still settling.',
1137
+ action: `Re-run ours-fleet task delete ${id} ${id} or ours-fleet task recover ${id}.`,
1138
+ }));
818
1139
  return;
819
1140
  }
820
1141
  console.log(renderMarkdownResult({
821
- icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
1142
+ icon: '🗑️', title: 'Task deleted',
822
1143
  fields: [{ label: 'ID', value: id, kind: 'code' }],
823
1144
  }));
824
1145
  }
@@ -833,9 +1154,29 @@ export function registerTaskCommands(parent, cOpt) {
833
1154
  .option('--json', 'JSON output')
834
1155
  .action(async (id, opts) => {
835
1156
  try {
1157
+ const previous = getTask(id).state;
836
1158
  const app = taskRoomService(opts.configuration);
837
1159
  const actor = { kind: 'local_control', surface: 'cli' };
838
1160
  const begin = await app.beginTaskRecovery({ actor, taskId: id });
1161
+ if (begin.kind === 'deletion_worker_required') {
1162
+ const settled = await launchTaskDeleteWorker(id, opts.configuration);
1163
+ const payload = { schema_version: 1, task_id: id, deleted: settled.deleted };
1164
+ if (opts.json) {
1165
+ console.log(JSON.stringify(payload, null, 2));
1166
+ return;
1167
+ }
1168
+ console.log(settled.deleted
1169
+ ? renderMarkdownResult({
1170
+ icon: '🗑️', title: 'Task deletion completed by recovery',
1171
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1172
+ })
1173
+ : renderMarkdownFailure({
1174
+ kind: 'pending', subject: `task recover ${id}`,
1175
+ detail: 'The task is pending deletion and cleanup is still settling.',
1176
+ action: `Run ours-fleet task delete ${id} ${id} to retry.`,
1177
+ }));
1178
+ return;
1179
+ }
839
1180
  const recovered = begin.kind === 'terminal_worker_required'
840
1181
  ? await (async () => {
841
1182
  const settled = await launchTaskSettleWorker(id, opts.configuration);
@@ -843,6 +1184,7 @@ export function registerTaskCommands(parent, cOpt) {
843
1184
  })()
844
1185
  : begin.result;
845
1186
  const t = recovered.task;
1187
+ auditTask('recover', t, previous);
846
1188
  const room = recovered.room;
847
1189
  const recoveryActions = recovered.issues.map(issue => {
848
1190
  if (issue.code === 'terminal_pending')
@@ -913,6 +1255,35 @@ export function registerTaskCommands(parent, cOpt) {
913
1255
  dieTaskRoom(e);
914
1256
  }
915
1257
  });
1258
+ cOpt(taskCmd.command('_settle_delete <id>', { hidden: true }))
1259
+ .description('internal: settle a previously accepted task deletion')
1260
+ .option('--json', 'JSON output')
1261
+ .action(async (id, opts) => {
1262
+ try {
1263
+ const app = taskRoomService(opts.configuration);
1264
+ const result = await app.settleTaskDeletion({
1265
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
1266
+ });
1267
+ if (opts.json) {
1268
+ console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
1269
+ return;
1270
+ }
1271
+ console.log(renderMarkdownResult({
1272
+ icon: '🗑️', title: result.deleted ? 'Task deletion settled' : 'Task already absent',
1273
+ fields: [{ label: 'ID', value: id, kind: 'code' }],
1274
+ }));
1275
+ }
1276
+ catch (e) {
1277
+ await taskRoomService(opts.configuration).recordDeletionError({
1278
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
1279
+ error: errorText(e),
1280
+ recoveryHint: `External delete worker failed. Retry task delete ${id} ${id}.`,
1281
+ }).catch(() => { });
1282
+ if (opts.json)
1283
+ die(e);
1284
+ dieTaskRoom(e);
1285
+ }
1286
+ });
916
1287
  cOpt(taskCmd.command('_recover <id>', { hidden: true }))
917
1288
  .description('internal: settle and continue task recovery')
918
1289
  .option('--json', 'JSON output')
@@ -921,6 +1292,11 @@ export function registerTaskCommands(parent, cOpt) {
921
1292
  const actor = { kind: 'internal_worker', surface: 'cli' };
922
1293
  try {
923
1294
  const begin = await app.beginTaskRecovery({ actor, taskId: id });
1295
+ if (begin.kind === 'deletion_worker_required') {
1296
+ const settled = await app.settleTaskDeletion({ actor, taskId: id });
1297
+ console.log(JSON.stringify({ schema_version: 1, deletion: settled }, null, 2));
1298
+ return;
1299
+ }
924
1300
  const result = begin.kind === 'terminal_worker_required'
925
1301
  ? await (async () => {
926
1302
  try {
@@ -945,15 +1321,33 @@ export function registerTaskCommands(parent, cOpt) {
945
1321
  }
946
1322
  });
947
1323
  cOpt(taskCmd.command('work <id>'))
948
- .description('ensure a task has a room and agents running')
1324
+ .description('deprecated alias for task start')
949
1325
  .option('--template <name>', 'room template')
1326
+ .option('--anonymous', 'create an anonymous Cowork room')
1327
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
1328
+ .option('--members-file <path>', 'typed YAML member overrides')
1329
+ .option('--member <slot>', 'begin a typed member override block')
1330
+ .option('--agent-template <id>', 'Agent Template for current member')
1331
+ .option('--brain <id>', 'Brain preset for current member')
1332
+ .option('--role <id>', 'Role preset for current member')
1333
+ .option('--approval <mode>', 'approval for current member')
1334
+ .option('--filesystem <mode>', 'filesystem for current member')
1335
+ .option('--unattended <mode>', 'unattended policy for current member')
1336
+ .option('--cwd <path>', 'working directory for current member')
1337
+ .option('--model <id>', 'model for current member')
1338
+ .option('--effort <level>', 'reasoning effort for current member')
950
1339
  .option('--json', 'JSON output')
951
- .action(async (id, opts) => {
1340
+ .action(async (id, opts, command) => {
952
1341
  try {
1342
+ const previous = getTask(id).state;
1343
+ console.error('warning: `fleet task work` is deprecated; use `fleet task start`');
953
1344
  const result = await taskRoomService(opts.configuration).ensureTaskWork({
954
1345
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, template: opts.template,
1346
+ members: cliMemberOverrides(opts.membersFile, commandArgv(command)),
1347
+ anonymous: cliAnonymousOverride(commandArgv(command)),
955
1348
  });
956
1349
  const t = result.task;
1350
+ auditTask('work', t, previous);
957
1351
  if (result.status === 'already_active') {
958
1352
  if (opts.json) {
959
1353
  console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
@@ -992,6 +1386,7 @@ export function registerTaskCommands(parent, cOpt) {
992
1386
  .option('--json', 'JSON output')
993
1387
  .action(async (id, opts) => {
994
1388
  try {
1389
+ const previous = getTask(id).state;
995
1390
  let summary = opts.summary;
996
1391
  if (opts.summaryFile)
997
1392
  summary = readFileSync(opts.summaryFile, 'utf8');
@@ -999,10 +1394,13 @@ export function registerTaskCommands(parent, cOpt) {
999
1394
  const plan = await taskRoomService(opts.configuration).finishTask({
1000
1395
  actor: { kind: 'local_control', surface: 'cli' }, taskId: id, outcome,
1001
1396
  });
1397
+ if (plan.task.terminal_intent?.status === 'pending')
1398
+ auditTask('settling', plan.task, previous, previous, plan.task.terminal_intent.accepted_at);
1002
1399
  const settled = plan.settlementRequired
1003
1400
  ? await launchTaskSettleWorker(id, opts.configuration)
1004
1401
  : { task: plan.task, timedOut: false };
1005
1402
  const t = settled.task;
1403
+ auditTask('finish', t, previous);
1006
1404
  if (opts.json) {
1007
1405
  console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
1008
1406
  return;
@@ -1032,15 +1430,30 @@ export function registerRoomCommands(parent, cOpt) {
1032
1430
  .description('create a standalone room')
1033
1431
  .requiredOption('--name <name>', 'room name')
1034
1432
  .option('--template <name>', 'room template')
1433
+ .option('--anonymous', 'create an anonymous Cowork room')
1434
+ .option('--no-anonymous', 'explicitly disable template anonymous mode')
1035
1435
  .option('--goal <text>', 'room goal')
1036
1436
  .option('--brief <text>', 'room briefing')
1037
1437
  .option('--brief-file <path>', 'room briefing from file')
1438
+ .option('--members-file <path>', 'typed YAML member overrides')
1439
+ .option('--member <slot>', 'begin a typed member override block')
1440
+ .option('--agent-template <id>', 'Agent Template for current member')
1441
+ .option('--brain <id>', 'Brain preset for current member')
1442
+ .option('--role <id>', 'Role preset for current member')
1443
+ .option('--approval <mode>', 'approval for current member')
1444
+ .option('--filesystem <mode>', 'filesystem for current member')
1445
+ .option('--unattended <mode>', 'unattended policy for current member')
1446
+ .option('--cwd <path>', 'working directory for current member')
1447
+ .option('--model <id>', 'model for current member')
1448
+ .option('--effort <level>', 'reasoning effort for current member')
1038
1449
  .option('--json', 'JSON output')
1039
- .action(async (opts) => {
1450
+ .action(async (opts, command) => {
1040
1451
  try {
1041
1452
  const record = await taskRoomService(opts.configuration).createRoom({
1042
1453
  actor: { kind: 'local_control', surface: 'cli' }, name: opts.name,
1043
1454
  template: opts.template, goal: opts.goal, brief: opts.brief, briefFile: opts.briefFile,
1455
+ members: cliMemberOverrides(opts.membersFile, commandArgv(command)),
1456
+ anonymous: cliAnonymousOverride(commandArgv(command)),
1044
1457
  });
1045
1458
  if (opts.json) {
1046
1459
  console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
@@ -1109,7 +1522,9 @@ export function registerRoomCommands(parent, cOpt) {
1109
1522
  ],
1110
1523
  sections: [
1111
1524
  ...(cowork.seats.length ? [{
1112
- heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`),
1525
+ heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
1526
+ + seatConfigurationSuffix(r?.member_seats
1527
+ .find(seat => seat.identity_cid === s.identity_cid)?.launch?.presentation)),
1113
1528
  }] : []),
1114
1529
  ...(r ? roomStartupSections(r) : []),
1115
1530
  ],
@@ -1126,7 +1541,7 @@ export function registerRoomCommands(parent, cOpt) {
1126
1541
  .option('--json', 'JSON output')
1127
1542
  .action(async (id, opts) => {
1128
1543
  try {
1129
- const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
1544
+ const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
1130
1545
  const url = `http://localhost:4460/room/${id}`;
1131
1546
  if (opts.json) {
1132
1547
  console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
@@ -1171,7 +1586,9 @@ export function registerRoomCommands(parent, cOpt) {
1171
1586
  ],
1172
1587
  sections: [{
1173
1588
  heading: 'Members',
1174
- ...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`) }
1589
+ ...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
1590
+ + seatConfigurationSuffix(r?.member_seats
1591
+ .find(seat => seat.identity_cid === s.identity_cid)?.launch?.presentation)) }
1175
1592
  : { items: ['No members found.'] }),
1176
1593
  }],
1177
1594
  }));
@@ -1186,10 +1603,16 @@ export function registerRoomCommands(parent, cOpt) {
1186
1603
  try {
1187
1604
  if (id !== confirmId)
1188
1605
  throw taskRoomPublicError('room_confirmation_mismatch');
1606
+ const previous = getRoomRecord(id);
1189
1607
  await taskRoomService(opts.configuration).requestRoomDeletion({
1190
1608
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1191
1609
  });
1610
+ const closing = getRoomRecord(id);
1611
+ if (previous && closing && previous.state !== closing.state)
1612
+ auditRoom('close', closing, previous.state, closing.state);
1192
1613
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1614
+ if (closing)
1615
+ auditRoom('delete', closing, closing.state, settled.deleted ? 'deleted' : 'closing');
1193
1616
  if (opts.json) {
1194
1617
  console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
1195
1618
  return;
@@ -1226,11 +1649,14 @@ export function registerRoomCommands(parent, cOpt) {
1226
1649
  .option('--json', 'JSON output')
1227
1650
  .action(async (id, opts) => {
1228
1651
  try {
1652
+ const previous = getRoomRecord(id);
1229
1653
  const recovered = await taskRoomService(opts.configuration).recoverRoom({
1230
1654
  actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
1231
1655
  });
1232
1656
  if (recovered.kind === 'deletion_worker_required') {
1233
1657
  const settled = await launchRoomDeleteWorker(id, opts.configuration);
1658
+ if (previous)
1659
+ auditRoom('recover', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
1234
1660
  if (opts.json) {
1235
1661
  console.log(JSON.stringify({
1236
1662
  schema_version: 1, room: null, orchestration: null,
@@ -1251,6 +1677,8 @@ export function registerRoomCommands(parent, cOpt) {
1251
1677
  return;
1252
1678
  }
1253
1679
  const { room: cowork, orchestration: r, issues: actions } = recovered;
1680
+ if (r)
1681
+ auditRoom('recover', r, previous?.state ?? 'unresolved', r.state);
1254
1682
  if (opts.json) {
1255
1683
  console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
1256
1684
  return;