@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
@@ -10,7 +10,7 @@ import { applyRole } from '../ops.js';
10
10
  import { daemonIdentityInventoryProvisioner, ensureIdentity, } from '../creation.js';
11
11
  import { runOnce, START_STAGGER_FILE } from '../runner.js';
12
12
  import { agentDir, tmpRoot } from '../paths.js';
13
- import { loadConfig, resolveMonitorConfig, resolveWorklogPolicy, ROLE_NAME_RE, } from '../config.js';
13
+ import { loadConfig, ROLE_NAME_RE, } from '../config.js';
14
14
  import { getAdapter } from '../harness/registry.js';
15
15
  import { redactLogLine } from '../application/log-service.js';
16
16
  import { controlRequest, controlSocketPath } from '../session/control.js';
@@ -84,19 +84,10 @@ function readTail(runDir) {
84
84
  }
85
85
  }
86
86
  /** The temp role every watchdog-family run launches under. Isolation is opt-in. */
87
- function buildWatchdogRole(wd, cfg) {
87
+ function buildWatchdogRole(wd, _cfg) {
88
88
  return {
89
- name: wd.identity, sourceFile: '(watchdog)',
90
- harness: wd.harness, session: wd.session,
91
- identity: wd.identity, model: wd.model,
92
- // Watchdogs are observe-only by contract, but their sanctioned status commands
93
- // must reach host control sockets. Keep approvals/unattended escalation denied
94
- // while disabling the harness's native filesystem/network sandbox.
95
- permissions: { approval: 'deny', filesystem: 'unrestricted', unattended: 'deny' },
96
- permissionsDeclared: true,
97
- monitor: resolveMonitorConfig(cfg.defaults.monitor, undefined),
98
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, undefined),
99
- isolation: wd.isolation,
89
+ ...structuredClone(wd.resolvedAgent),
90
+ name: wd.identity, sourceFile: '(watchdog)', identity: wd.identity,
100
91
  };
101
92
  }
102
93
  /** Discover temporary sessions that are live now, not merely stale dirs on disk. */
@@ -3,6 +3,7 @@ export declare const REDACTED_ENV_VALUE = "__OURS_FLEET_SECRET_REDACTED__";
3
3
  export interface EditableFleetModel {
4
4
  manifest: Record<string, unknown>;
5
5
  agents: Record<string, Record<string, unknown>>;
6
+ agent_templates: Record<string, Record<string, unknown>>;
6
7
  }
7
8
  export interface ConfigReadResult {
8
9
  path: string;
@@ -12,7 +12,7 @@ import { isSensitiveConfigKey } from '../sensitive-config.js';
12
12
  export const REDACTED_ENV_VALUE = '__OURS_FLEET_SECRET_REDACTED__';
13
13
  const MAX_CONFIG_BYTES = 256 * 1024;
14
14
  const DIFF_CONTEXT_LINES = 3;
15
- const KINDS = ['agents', 'roles', 'brains'];
15
+ const KINDS = ['agents', 'agent_templates', 'roles', 'brains', 'room_templates'];
16
16
  const emptyReport = () => ({ ok: true, checks: [] });
17
17
  export class FleetConfigService {
18
18
  path;
@@ -176,7 +176,8 @@ function readSources(manifest) {
176
176
  }
177
177
  trustedDirectory(dir);
178
178
  for (const name of readdirSync(dir).sort()) {
179
- if (kind === 'agents' && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
179
+ if ((kind === 'agents' || kind === 'agent_templates' || kind === 'room_templates')
180
+ && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
180
181
  continue;
181
182
  documents.set(join(kind, name), trustedFile(join(dir, name)));
182
183
  }
@@ -187,20 +188,26 @@ function modelFrom(sources) {
187
188
  const manifestKey = basename(sources.manifest);
188
189
  const manifest = parseFleetDocument(sources.manifest, sources.documents.get(manifestKey), 'strict').value;
189
190
  const agents = {};
191
+ const agent_templates = {};
190
192
  for (const [rel, source] of sources.documents)
191
193
  if (rel.startsWith(`agents/`)) {
192
194
  const id = basename(rel, extname(rel));
193
195
  agents[id] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
194
196
  }
195
- return { manifest, agents };
197
+ for (const [rel, source] of sources.documents)
198
+ if (rel.startsWith(`agent_templates/`)) {
199
+ const id = basename(rel, extname(rel));
200
+ agent_templates[id] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
201
+ }
202
+ return { manifest, agents, agent_templates };
196
203
  }
197
204
  function assertModel(value) {
198
205
  if (!value || typeof value !== 'object' || Array.isArray(value))
199
206
  throw new FleetError('invalid_request', 'model must be an object');
200
207
  const input = value;
201
- if (Object.keys(input).sort().join(',') !== 'agents,manifest')
202
- throw new FleetError('invalid_request', 'model allows exactly manifest and agents');
203
- for (const key of ['manifest', 'agents'])
208
+ if (Object.keys(input).sort().join(',') !== 'agent_templates,agents,manifest')
209
+ throw new FleetError('invalid_request', 'model allows exactly manifest, agents, and agent_templates');
210
+ for (const key of ['manifest', 'agents', 'agent_templates'])
204
211
  if (!input[key] || typeof input[key] !== 'object' || Array.isArray(input[key]))
205
212
  throw new FleetError('invalid_request', `${key} must be an object`);
206
213
  for (const [id, doc] of Object.entries(input.agents)) {
@@ -209,6 +216,12 @@ function assertModel(value) {
209
216
  if (!doc || typeof doc !== 'object' || Array.isArray(doc))
210
217
  throw new FleetError('invalid_request', `Agent '${id}' must be an object`);
211
218
  }
219
+ for (const [id, doc] of Object.entries(input.agent_templates)) {
220
+ if (!ROLE_NAME_RE.test(id))
221
+ throw new FleetError('invalid_request', `invalid Agent Template ID '${id}'`);
222
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
223
+ throw new FleetError('invalid_request', `Agent Template '${id}' must be an object`);
224
+ }
212
225
  return structuredClone(input);
213
226
  }
214
227
  function renderProposal(current, model) {
@@ -221,8 +234,14 @@ function renderProposal(current, model) {
221
234
  next.set(oldRel ?? join('agents', `${id}.yaml`), oldRel
222
235
  ? render(current.documents.get(oldRel), doc) : stringify(doc));
223
236
  }
237
+ for (const [id, doc] of Object.entries(model.agent_templates).sort(([a], [b]) => a.localeCompare(b))) {
238
+ const oldRel = [...current.documents.keys()].find(rel => rel.startsWith('agent_templates/')
239
+ && basename(rel, extname(rel)) === id);
240
+ next.set(oldRel ?? join('agent_templates', `${id}.yaml`), oldRel
241
+ ? render(current.documents.get(oldRel), doc) : stringify(doc));
242
+ }
224
243
  for (const [rel, source] of current.documents)
225
- if (rel.startsWith('roles/') || rel.startsWith('brains/'))
244
+ if (rel.startsWith('roles/') || rel.startsWith('brains/') || rel.startsWith('room_templates/'))
226
245
  next.set(rel, source);
227
246
  return next;
228
247
  }
@@ -424,7 +443,7 @@ function sourceSensitiveValues(sources) {
424
443
  continue;
425
444
  try {
426
445
  const parsed = parse(source);
427
- values.push(...sensitiveValues({ manifest: {}, agents: { source: parsed } }));
446
+ values.push(...sensitiveValues({ manifest: {}, agents: { source: parsed }, agent_templates: {} }));
428
447
  }
429
448
  catch { /* the authoritative parser reports malformed YAML */ }
430
449
  }
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
4
4
  import { loadConfig } from '../config.js';
5
5
  import { RoleRepository } from '../application/role-repository.js';
6
6
  import { FleetQueryService } from '../application/fleet-query-service.js';
7
- import { AcpRoleSessionAdapter } from '../application/session-control.js';
7
+ import { RoleSessionControlAdapter } from '../application/session-control.js';
8
8
  import { StructuredLogService } from '../application/log-service.js';
9
9
  import { RoleCommandService } from '../application/role-command-service.js';
10
10
  import { RoleCreationService } from '../application/role-creation-service.js';
@@ -64,18 +64,22 @@ export async function startWebConsole(options) {
64
64
  const repository = new RoleRepository({
65
65
  configPath: options.configPath,
66
66
  probeBackend: async (name) => {
67
- let acp = false;
67
+ let backend;
68
68
  const permanent = resolve(home(), '.ours-fleet', 'agents', name);
69
69
  const temporary = resolve(home(), '.ours-fleet', 'tmp', name);
70
70
  for (const dir of [permanent, temporary]) {
71
71
  if (!existsSync(controlSocketPath(dir)))
72
72
  continue;
73
73
  try {
74
- acp = (await controlRequest(dir, { command: 'snapshot' }, 500)).ok;
74
+ const response = await controlRequest(dir, { command: 'snapshot' }, 500);
75
+ const snapshot = response.result;
76
+ if (response.ok && (snapshot?.backend === 'acp'
77
+ || snapshot?.backend === 'codex-app-server'))
78
+ backend = snapshot.backend;
75
79
  }
76
80
  catch { /* stale socket is evidence, not reachability */ }
77
81
  }
78
- return { acp };
82
+ return { backend };
79
83
  },
80
84
  });
81
85
  const events = new FleetEventBus();
@@ -161,11 +165,12 @@ export async function startWebConsole(options) {
161
165
  const role = await repository.get(roleId);
162
166
  if (!role)
163
167
  throw new FleetError('role_not_found', `no such role '${roleId}'`);
164
- if (role.configuredBackend === 'acp') {
168
+ if (role.configuredBackend === 'acp'
169
+ || role.configuredBackend === 'codex-app-server') {
165
170
  const dir = repository.stateDir(role);
166
171
  if (!dir)
167
172
  throw new FleetError('control_unavailable', 'role state directory is unavailable');
168
- return new AcpRoleSessionAdapter(dir);
173
+ return new RoleSessionControlAdapter(dir);
169
174
  }
170
175
  throw new FleetError('capability_unavailable', 'role session backend is unavailable');
171
176
  },
@@ -183,12 +183,39 @@ export async function buildWebServer(services, boundary, options = {}) {
183
183
  throw new FleetError('invalid_request', 'invalid task state filter');
184
184
  if (query.groupByList !== undefined && !['true', 'false'].includes(query.groupByList))
185
185
  throw new FleetError('invalid_request', 'groupByList must be true or false');
186
+ if (query.includeDeleting !== undefined && !['true', 'false'].includes(query.includeDeleting))
187
+ throw new FleetError('invalid_request', 'includeDeleting must be true or false');
186
188
  const state = query.state && query.state !== 'all' ? query.state : undefined;
187
- const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}) };
189
+ const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
190
+ ...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
188
191
  return taskApi(() => query.groupByList === 'true'
189
192
  ? { groups: requireTaskRooms().groupedTasks(filter) }
190
193
  : { tasks: requireTaskRooms().listTasks(filter) });
191
194
  });
195
+ app.delete('/api/v1/tasks/:id', async (request, reply) => {
196
+ auth.authenticate(request, true);
197
+ const taskId = request.params.id;
198
+ const confirm = request.query.confirm;
199
+ // Exact-ID-twice confirmation is validated before existence or idempotency.
200
+ if (confirm !== taskId)
201
+ throw new FleetError('invalid_request', 'confirm must exactly repeat the task id');
202
+ const accepted = await taskApi(() => requireTaskRooms().requestTaskDeletion({
203
+ actor: { kind: 'local_control', surface: 'web' }, taskId,
204
+ }));
205
+ if (accepted.status === 'already_absent')
206
+ return { task_id: taskId, deleted: false, already_absent: true };
207
+ // Cleanup runs in an external worker outside the request lifecycle; the
208
+ // request waits boundedly and reports pending — never fabricated success.
209
+ const outcome = await requireTaskRooms().launchTaskDeletionWorker({ taskId, waitMs: 5_000 });
210
+ if (outcome.deleted)
211
+ return { task_id: taskId, deleted: true };
212
+ reply.code(202);
213
+ return {
214
+ task_id: taskId, accepted: true, deletion: 'pending',
215
+ ...(outcome.error ? { error: outcome.error } : {}),
216
+ recovery: `Repeat DELETE /api/v1/tasks/${taskId}?confirm=${taskId} or run 'ours-fleet task recover ${taskId}'.`,
217
+ };
218
+ });
192
219
  app.post('/api/v1/tasks', async (request, reply) => {
193
220
  auth.authenticate(request, true);
194
221
  const body = request.body;
@@ -126,7 +126,14 @@ function agentEntry(node) {
126
126
  */
127
127
  function watchdogEntry(node, merged) {
128
128
  const scoped = linked(merged, node.id, 'watches');
129
- return { ...pick(node, WATCHDOG_FIELDS), ...(scoped.length ? { watch: scoped } : {}) };
129
+ return {
130
+ ...pick(node, WATCHDOG_FIELDS),
131
+ agent: {
132
+ role: { inline: {} },
133
+ brain: { inline: { harness: 'claude-code', ...pick(node, BRAIN_FIELDS) } },
134
+ },
135
+ ...(scoped.length ? { watch: scoped } : {}),
136
+ };
130
137
  }
131
138
  function loopEntry(node, merged) {
132
139
  const roles = linked(merged, node.id, 'targets');