@ours.network/fleet 1.1.0-nightly.9 → 1.1.0

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 (200) hide show
  1. package/README.md +231 -46
  2. package/dist/application/capabilities.js +18 -6
  3. package/dist/application/fleet-query-service.js +7 -4
  4. package/dist/application/role-command-service.js +9 -0
  5. package/dist/application/role-creation-service.d.ts +4 -4
  6. package/dist/application/role-creation-service.js +8 -1
  7. package/dist/application/role-repository.d.ts +4 -2
  8. package/dist/application/role-repository.js +6 -3
  9. package/dist/application/session-control.d.ts +3 -1
  10. package/dist/application/session-control.js +4 -2
  11. package/dist/application/task-room-service.d.ts +92 -39
  12. package/dist/application/task-room-service.js +562 -289
  13. package/dist/briefing.js +79 -22
  14. package/dist/build-info.json +5 -5
  15. package/dist/canonical-json.d.ts +2 -0
  16. package/dist/canonical-json.js +10 -0
  17. package/dist/cli.js +129 -47
  18. package/dist/config.d.ts +38 -3
  19. package/dist/config.js +193 -28
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +7 -4
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +181 -41
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +40 -4
  26. package/dist/fleet-command-audit.d.ts +22 -1
  27. package/dist/fleet-command-audit.js +355 -50
  28. package/dist/fleet-proxy.d.ts +5 -2
  29. package/dist/harness/claude-code-session.js +2 -1
  30. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  31. package/dist/harness/codex-app-server-proxy.js +348 -14
  32. package/dist/harness/codex-session.d.ts +9 -2
  33. package/dist/harness/codex-session.js +18 -2
  34. package/dist/harness/codex.d.ts +5 -1
  35. package/dist/harness/codex.js +104 -10
  36. package/dist/harness/types.d.ts +1 -1
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +3 -2
  39. package/dist/init-guidance.d.ts +1 -1
  40. package/dist/init-guidance.js +1 -1
  41. package/dist/init-wizard.d.ts +98 -0
  42. package/dist/init-wizard.js +631 -0
  43. package/dist/lifecycle-summary.d.ts +134 -0
  44. package/dist/lifecycle-summary.js +179 -0
  45. package/dist/loops/config.d.ts +10 -1
  46. package/dist/loops/config.js +38 -5
  47. package/dist/model-recovery.d.ts +1 -1
  48. package/dist/ops.js +2 -17
  49. package/dist/owner-channel/channel.d.ts +19 -3
  50. package/dist/owner-channel/channel.js +301 -120
  51. package/dist/owner-channel/commands.d.ts +28 -6
  52. package/dist/owner-channel/commands.js +109 -27
  53. package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
  54. package/dist/owner-channel/lifecycle-outbox.js +87 -0
  55. package/dist/owner-channel/ours-client.d.ts +19 -0
  56. package/dist/owner-channel/ours-client.js +3 -0
  57. package/dist/permissions.js +1 -1
  58. package/dist/preset-migration.d.ts +42 -0
  59. package/dist/preset-migration.js +450 -0
  60. package/dist/reports/artifact.d.ts +10 -0
  61. package/dist/reports/artifact.js +32 -0
  62. package/dist/reports/index.d.ts +5 -0
  63. package/dist/reports/index.js +5 -0
  64. package/dist/reports/render.d.ts +6 -0
  65. package/dist/reports/render.js +240 -0
  66. package/dist/reports/service.d.ts +42 -0
  67. package/dist/reports/service.js +57 -0
  68. package/dist/reports/tasks.d.ts +52 -0
  69. package/dist/reports/tasks.js +177 -0
  70. package/dist/reports/types.d.ts +315 -0
  71. package/dist/reports/types.js +6 -0
  72. package/dist/resolved-plan.js +13 -1
  73. package/dist/rooms-tasks/cli.d.ts +2 -0
  74. package/dist/rooms-tasks/cli.js +361 -285
  75. package/dist/rooms-tasks/close.d.ts +9 -1
  76. package/dist/rooms-tasks/close.js +23 -7
  77. package/dist/rooms-tasks/config.js +16 -25
  78. package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
  79. package/dist/rooms-tasks/cowork-adapter.js +13 -0
  80. package/dist/rooms-tasks/deletion.d.ts +51 -0
  81. package/dist/rooms-tasks/deletion.js +217 -0
  82. package/dist/rooms-tasks/external-worker.d.ts +8 -0
  83. package/dist/rooms-tasks/external-worker.js +38 -0
  84. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  85. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  86. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  87. package/dist/rooms-tasks/member-overrides.js +204 -0
  88. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  89. package/dist/rooms-tasks/member-startup.js +9 -2
  90. package/dist/rooms-tasks/provision.js +146 -83
  91. package/dist/rooms-tasks/room-state.d.ts +1 -0
  92. package/dist/rooms-tasks/room-state.js +12 -1
  93. package/dist/rooms-tasks/task-room-name.d.ts +12 -0
  94. package/dist/rooms-tasks/task-room-name.js +61 -0
  95. package/dist/rooms-tasks/task-state.d.ts +115 -3
  96. package/dist/rooms-tasks/task-state.js +361 -11
  97. package/dist/rooms-tasks/templates.d.ts +4 -1
  98. package/dist/rooms-tasks/templates.js +34 -3
  99. package/dist/rooms-tasks/terminal.d.ts +3 -0
  100. package/dist/rooms-tasks/terminal.js +10 -4
  101. package/dist/rooms-tasks/types.d.ts +92 -7
  102. package/dist/rooms-tasks/types.js +14 -1
  103. package/dist/runner.d.ts +3 -0
  104. package/dist/runner.js +91 -44
  105. package/dist/session/acp.d.ts +4 -1
  106. package/dist/session/acp.js +29 -13
  107. package/dist/session/arbiter.d.ts +2 -1
  108. package/dist/session/arbiter.js +2 -0
  109. package/dist/session/codex-app-server-transport.d.ts +53 -0
  110. package/dist/session/codex-app-server-transport.js +184 -0
  111. package/dist/session/codex-app-server.d.ts +122 -0
  112. package/dist/session/codex-app-server.js +1031 -0
  113. package/dist/session/control.d.ts +1 -1
  114. package/dist/session/control.js +17 -1
  115. package/dist/session/conversation-types.d.ts +4 -4
  116. package/dist/session/types.d.ts +24 -6
  117. package/dist/session/types.js +28 -0
  118. package/dist/spawn.d.ts +9 -0
  119. package/dist/spawn.js +77 -9
  120. package/dist/web/fleet-config-service.d.ts +1 -0
  121. package/dist/web/fleet-config-service.js +25 -7
  122. package/dist/web/runtime.js +11 -6
  123. package/dist/web/server.js +28 -1
  124. package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
  125. package/dist/web-app/index.html +1 -1
  126. package/package.json +4 -4
  127. package/presets/brain-catalog.json +17 -0
  128. package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
  129. package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
  130. package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
  131. package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
  132. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  133. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  134. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  135. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  136. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  137. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  138. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  139. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  140. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  141. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  142. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  143. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  144. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  145. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  146. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  147. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  148. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  149. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  150. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  151. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  152. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  153. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  154. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  155. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  156. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
  186. package/presets/fleet/roles/Coordinator.yaml +37 -0
  187. package/presets/fleet/roles/Critic.yaml +23 -1
  188. package/presets/fleet/roles/Developer.yaml +27 -5
  189. package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
  190. package/presets/fleet/room_templates/pair.yaml +9 -7
  191. package/presets/fleet/room_templates/single.yaml +5 -4
  192. package/presets/fleet/room_templates/team.yaml +12 -8
  193. package/presets/manifest.json +2 -1
  194. package/presets/fleet/agents/Architect.yaml +0 -3
  195. package/presets/fleet/agents/Secretary.yaml +0 -3
  196. package/presets/fleet/agents/Tester.yaml +0 -3
  197. package/presets/fleet/roles/Agent.yaml +0 -6
  198. package/presets/fleet/roles/Architect.yaml +0 -6
  199. package/presets/fleet/roles/Secretary.yaml +0 -6
  200. package/presets/fleet/roles/Tester.yaml +0 -6
@@ -9,7 +9,7 @@ export interface ControlRequest {
9
9
  version: 1 | 2 | 3;
10
10
  id: string;
11
11
  token: string;
12
- command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_finish';
12
+ command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_present' | 'fleet_audit_finish';
13
13
  text?: string;
14
14
  permissionId?: string;
15
15
  optionId?: string;
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node
3
3
  import { createConnection, createServer } from 'node:net';
4
4
  import { join } from 'node:path';
5
5
  import { SessionControlError } from './types.js';
6
- import { validateFleetAuditBegin, validateFleetAuditFinish, } from '../fleet-command-audit.js';
6
+ import { validateFleetAuditBegin, validateFleetAuditFinish, validateFleetAuditPresentations, } from '../fleet-command-audit.js';
7
7
  import { interruptSession, queueSessionPrompt, respondSessionPermission, respondSessionPermissionV2, } from '../application/session-mutations.js';
8
8
  const MAX_LINE_BYTES = 64 * 1024;
9
9
  /** Commands that require protocol version 3. */
@@ -236,6 +236,9 @@ export class RoleControlServer {
236
236
  features: [
237
237
  'events_since', 'observer_follow', 'retained_range',
238
238
  ...(this.session.conversationPage ? ['conversation_v3'] : []),
239
+ ...(this.session.capabilities?.steering ? ['steering'] : []),
240
+ ...(this.session.capabilities?.permissions ? ['permissions'] : []),
241
+ ...(this.session.capabilities?.messagePhases ? ['message_phases'] : []),
239
242
  ],
240
243
  },
241
244
  });
@@ -346,6 +349,19 @@ export class RoleControlServer {
346
349
  this.write(socket, { version: 1, id: request.id, ok: true, result });
347
350
  return;
348
351
  }
352
+ case 'fleet_audit_present': {
353
+ if (!this.fleetAuditor)
354
+ throw new SessionControlError('rejected', 'fleet auditing is unavailable');
355
+ try {
356
+ validateFleetAuditPresentations(request.audit?.presentations);
357
+ }
358
+ catch {
359
+ throw new SessionControlError('rejected', 'valid Fleet lifecycle presentations are required');
360
+ }
361
+ await this.fleetAuditor.present(request.audit.presentations);
362
+ this.write(socket, { version: 1, id: request.id, ok: true, result: { delivered: true } });
363
+ return;
364
+ }
349
365
  case 'owner_channel_manage': {
350
366
  if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
351
367
  throw new SessionControlError('rejected', 'owner-channel management action is required');
@@ -1,14 +1,14 @@
1
1
  import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.js';
2
2
  /**
3
- * Durable conversation domain schema for the ACP web console.
3
+ * Durable conversation domain schema for managed-session consoles.
4
4
  *
5
5
  * `SessionEvent` in ./types.js remains the compact diagnostic projection; the
6
6
  * types here describe the durable per-role conversation ledger. Nothing in this
7
- * file touches the wire: ACP updates are reduced into these shapes by the
8
- * normalizer, and the store assigns `seq`/`eventId`/timestamps.
7
+ * file touches the wire: transport updates are reduced into these shapes by a
8
+ * session adapter, and the store assigns `seq`/`eventId`/timestamps.
9
9
  */
10
10
  export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
11
- /** A well-formed ACP update this version cannot represent. Bounded, never a crash. */
11
+ /** A well-formed transport update this version cannot represent. Bounded, never a crash. */
12
12
  | 'unsupported';
13
13
  /** Where a conversation record came from. Typed provenance, never prompt text. */
14
14
  export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay';
@@ -33,7 +33,7 @@ export type PromptOrigin = {
33
33
  commandId: string;
34
34
  };
35
35
  export interface RuntimeSelectorMetadata {
36
- /** Exact provider/model identifier reported by the live ACP session. */
36
+ /** Exact provider/model identifier reported by the live managed session. */
37
37
  value: string;
38
38
  /** Complete provider-supplied label for that exact value, when available. */
39
39
  label?: string;
@@ -75,6 +75,8 @@ export interface TurnResult {
75
75
  export type ControlFailureKind = 'offline' | 'control-unavailable' | 'timeout' | 'rejected' | 'backend';
76
76
  /** Stable body-free reason shared by ACP recovery and durable ingress. */
77
77
  export declare const ACP_CANCEL_DEADLINE_EXCEEDED = "ACP_CANCEL_DEADLINE_EXCEEDED";
78
+ /** Native Codex equivalent; kept distinct so diagnostics name the failing transport. */
79
+ export declare const CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED = "CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED";
78
80
  export declare class SessionControlError extends Error {
79
81
  readonly kind: ControlFailureKind;
80
82
  /** Stable body-free machine reason for recovery/audit decisions. */
@@ -170,7 +172,7 @@ export interface SubmitPromptOptions {
170
172
  interruptSource?: TurnCancellationSource;
171
173
  /** Typed in-process provenance. Text markers never grant this origin. */
172
174
  origin?: PromptOrigin;
173
- /** Use the ACP steering extension when available; ignored by other backends. */
175
+ /** Inject into active work when the selected backend supports steering. */
174
176
  steer?: boolean;
175
177
  /** Audit-grade actor detail persisted with the conversation admission record. */
176
178
  actor?: {
@@ -200,11 +202,25 @@ export interface SessionSnapshot {
200
202
  activity?: SessionActivity;
201
203
  }
202
204
  export interface SessionActivity {
203
- /** ACP tool calls currently reserved (lifecycle open or permission pending). */
205
+ /** Tool calls currently reserved (lifecycle open or permission pending). */
204
206
  activeToolCalls: number;
205
207
  /** When the agent last sent ANY session update, replay excluded. */
206
208
  lastUpdateAt?: string;
207
209
  }
210
+ /** Provider-neutral behavior exposed by a session transport. */
211
+ export interface AgentSessionCapabilities {
212
+ streaming: boolean;
213
+ durableConversation: boolean;
214
+ promptInput: boolean;
215
+ interrupt: boolean;
216
+ steering: boolean;
217
+ permissions: boolean;
218
+ toolBoundaryDelivery: boolean;
219
+ messagePhases: boolean;
220
+ resume: boolean;
221
+ }
222
+ /** Conservative static capabilities used before a live session is reachable. */
223
+ export declare function sessionBackendCapabilities(backend: SessionBackendId, harness?: string): AgentSessionCapabilities;
208
224
  export type SessionEventKind = 'state' | 'agent_text' | 'thought' | 'tool_call' | 'tool_update' | 'permission' | 'monitor_delivery' | 'turn_stop' | 'error';
209
225
  /** What a settled permission request resolved to. */
210
226
  export type PermissionDecision = 'allowed' | 'denied' | 'cancelled';
@@ -219,7 +235,7 @@ export interface SessionEvent {
219
235
  text?: string;
220
236
  /**
221
237
  * Adapter-authenticated presentation phase for assistant text. Only the
222
- * exact Codex ACP marker is promoted; absence/unknown values stay unset.
238
+ * exact Codex phase marker is promoted; absence/unknown values stay unset.
223
239
  */
224
240
  messagePhase?: 'commentary' | 'final_answer';
225
241
  /** Stable adapter message/item id when supplied. */
@@ -261,11 +277,13 @@ export interface ConversationHandlePage {
261
277
  /**
262
278
  * The live, harness-neutral contract between Fleet and one agent session.
263
279
  * Harness adapters construct this handle; orchestration code must not depend
264
- * on the ACP transport which currently implements it.
280
+ * on any concrete transport which implements it.
265
281
  */
266
282
  export interface AgentSession {
267
283
  readonly backend: SessionBackendId;
268
284
  readonly pid: number;
285
+ /** Live capabilities; optional only for source compatibility with external adapters. */
286
+ readonly capabilities?: AgentSessionCapabilities;
269
287
  isAlive(): boolean;
270
288
  snapshot(): SessionSnapshot;
271
289
  conversationPage?(request: {
@@ -283,7 +301,7 @@ export interface AgentSession {
283
301
  queuePrompt(text: string, options?: SubmitPromptOptions): Promise<QueuedPrompt>;
284
302
  /** Queue a prompt and wait for its terminal result. */
285
303
  submitPrompt(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
286
- /** Monitor-only ACP safe-boundary delivery. Never implies human/control cancellation. */
304
+ /** Monitor-only safe-boundary delivery. Never implies human/control cancellation. */
287
305
  submitPromptAfterTool?(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
288
306
  /**
289
307
  * Cancel the active turn. Resolves when the cancellation has taken effect —
@@ -1,5 +1,7 @@
1
1
  /** Stable body-free reason shared by ACP recovery and durable ingress. */
2
2
  export const ACP_CANCEL_DEADLINE_EXCEEDED = 'ACP_CANCEL_DEADLINE_EXCEEDED';
3
+ /** Native Codex equivalent; kept distinct so diagnostics name the failing transport. */
4
+ export const CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED = 'CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED';
3
5
  export class SessionControlError extends Error {
4
6
  kind;
5
7
  reasonCode;
@@ -53,3 +55,29 @@ export function turnResult(accepted, outcome, detail, output, cancellationSource
53
55
  ...(outcome === 'cancelled' && cancellationSource ? { cancellationSource } : {}),
54
56
  };
55
57
  }
58
+ /** Conservative static capabilities used before a live session is reachable. */
59
+ export function sessionBackendCapabilities(backend, harness) {
60
+ if (backend === 'codex-app-server')
61
+ return {
62
+ streaming: true,
63
+ durableConversation: true,
64
+ promptInput: true,
65
+ interrupt: true,
66
+ steering: true,
67
+ permissions: true,
68
+ toolBoundaryDelivery: false,
69
+ messagePhases: true,
70
+ resume: true,
71
+ };
72
+ return {
73
+ streaming: true,
74
+ durableConversation: true,
75
+ promptInput: true,
76
+ interrupt: true,
77
+ steering: false,
78
+ permissions: true,
79
+ toolBoundaryDelivery: true,
80
+ messagePhases: harness === 'codex',
81
+ resume: true,
82
+ };
83
+ }
package/dist/spawn.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IsolationConfig } from './isolation/types.js';
2
+ import type { AgentLoopsConfig } from './loops/config.js';
2
3
  import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type MonitorConfig, type UnattendedMode, type RoomMemberStartup, type AgentDefinition, type AgentSelection } from './config.js';
3
4
  import { type OpsDeps } from './ops.js';
4
5
  import { type CreationDeps, type CreationProvenance } from './creation.js';
@@ -40,6 +41,12 @@ export interface SpawnOpts {
40
41
  * input in this release.
41
42
  */
42
43
  isolationFile?: string;
44
+ /** Trusted YAML containing exactly a top-level `loops:` map. Temporary launches only. */
45
+ loopsFile?: string;
46
+ /** Explicitly override any selected Agent Template loops with none. */
47
+ noLoops?: boolean;
48
+ /** Internal source label retained after room-member plan resolution. */
49
+ loopSource?: 'agent-template' | 'cli' | 'omitted';
43
50
  overseeInterval?: string;
44
51
  configPath?: string;
45
52
  dryRun?: boolean;
@@ -56,6 +63,8 @@ export declare function agentDefinitionFromSpawn(o: SpawnOpts): AgentDefinition;
56
63
  * must fail before any artifact exists.
57
64
  */
58
65
  export declare function readIsolationFile(path: string): IsolationConfig;
66
+ /** Read a private canonical temporary-loop override before any creation side effect. */
67
+ export declare function readLoopsFile(path: string): AgentLoopsConfig;
59
68
  export declare function validateSpawnOpts(o: SpawnOpts): void;
60
69
  /** The ours identity a spawn will bind: explicit, else the role name. */
61
70
  export declare const effectiveIdentity: (o: SpawnOpts) => string;
package/dist/spawn.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn as spawnChild } from 'node:child_process';
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { parse, stringify } from 'yaml';
5
5
  import { agentDir, defaultConfigPath } from './paths.js';
@@ -22,9 +22,12 @@ export function agentDefinitionFromSpawn(o) {
22
22
  if (o.agentDefinition) {
23
23
  if (o.brain !== undefined || o.role !== undefined || o.cwd !== undefined
24
24
  || o.coordinator !== undefined || o.approval !== undefined || o.filesystem !== undefined
25
- || o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined)
25
+ || o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined
26
+ || o.loopsFile !== undefined)
26
27
  throw new Error('canonical agentDefinition conflicts with separate Agent fields');
27
28
  const definition = structuredClone(o.agentDefinition);
29
+ if (o.noLoops === true && definition.loops !== undefined)
30
+ throw new Error('canonical agentDefinition loops conflict with explicit no-loops policy');
28
31
  if (o.identity)
29
32
  definition.identity = o.identity;
30
33
  return definition;
@@ -46,12 +49,21 @@ export function agentDefinitionFromSpawn(o) {
46
49
  ...(permissions ? { permissions } : {}),
47
50
  ...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
48
51
  ...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
52
+ ...(o.loopsFile ? { loops: readLoopsFile(o.loopsFile) } : {}),
49
53
  };
50
54
  }
51
55
  function resolvedSpawn(o) {
52
56
  const definition = agentDefinitionFromSpawn(o);
53
- const cfg = loadConfig(o.configPath, { additionalAgent: { id: o.name, definition } });
54
- return { definition, role: findRole(cfg, o.name) };
57
+ const cfg = loadConfig(o.configPath, {
58
+ additionalAgent: { id: o.name, definition, temporary: o.temp === true },
59
+ });
60
+ const role = findRole(cfg, o.name);
61
+ if (o.temp)
62
+ role.temporaryLoopSource = o.loopSource
63
+ ?? (role.temporaryLoops?.length ? 'agent-template' : 'omitted');
64
+ if (o.noLoops === true)
65
+ role.temporaryLoops = [];
66
+ return { definition, role };
55
67
  }
56
68
  /**
57
69
  * Read and validate an `--isolation-file`. The file is the existing
@@ -78,7 +90,37 @@ export function readIsolationFile(path) {
78
90
  throw new Error(`--isolation-file ${path}: ${problems.join('; ')}`);
79
91
  return cfg;
80
92
  }
93
+ /** Read a private canonical temporary-loop override before any creation side effect. */
94
+ export function readLoopsFile(path) {
95
+ const stat = lstatSync(path);
96
+ const uid = process.getuid?.();
97
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_000_000
98
+ || (uid !== undefined && stat.uid !== uid) || (stat.mode & 0o777) !== 0o600)
99
+ throw new Error(`${path}: loops file must be an owner-only regular file no larger than 1 MB`);
100
+ let raw;
101
+ try {
102
+ raw = parse(readFileSync(path, 'utf8'));
103
+ }
104
+ catch (error) {
105
+ throw new Error(`--loops-file ${path}: ${error.message}`);
106
+ }
107
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)
108
+ || !Object.hasOwn(raw, 'loops')
109
+ || Object.keys(raw).some(key => key !== 'loops'))
110
+ throw new Error(`${path}: expected exactly a top-level loops: mapping`);
111
+ const loops = raw.loops;
112
+ if (!loops || typeof loops !== 'object' || Array.isArray(loops)
113
+ || !Object.keys(loops).length)
114
+ throw new Error(`${path}: loops must be a non-empty mapping; use --no-loops to disable loops`);
115
+ return structuredClone(loops);
116
+ }
81
117
  export function validateSpawnOpts(o) {
118
+ if (o.loopsFile && o.noLoops === true)
119
+ throw new Error('--loops-file and --no-loops are mutually exclusive');
120
+ if ((o.loopsFile || o.noLoops === true || o.agentDefinition?.loops !== undefined) && !o.temp)
121
+ throw new Error('temporary Agent loops require --temp');
122
+ if (o.loopsFile)
123
+ readLoopsFile(o.loopsFile);
82
124
  if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
83
125
  throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
84
126
  if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
@@ -102,7 +144,15 @@ export function validateSpawnOpts(o) {
102
144
  * by the reservation rather than by this function.
103
145
  */
104
146
  function assertNameFree(o) {
105
- const cfg = loadConfig(o.configPath);
147
+ let cfg;
148
+ try {
149
+ cfg = loadConfig(o.configPath);
150
+ }
151
+ catch (error) {
152
+ if (error instanceof Error && error.message.includes(`resolve to duplicate identity '${effectiveIdentity(o)}'`))
153
+ throw new Error(`ours identity '${effectiveIdentity(o)}' is already taken or being created right now`);
154
+ throw error;
155
+ }
106
156
  if (cfg.roles.some(r => r.name === o.name))
107
157
  throw new Error(`role '${o.name}' already exists (${cfg.roles.find(r => r.name === o.name).sourceFile})`);
108
158
  if (existsSync(agentDir(o.name)) || existsSync(agentDir(o.name, true)))
@@ -141,7 +191,7 @@ export function spawnDryRun(o) {
141
191
  * `env`, `bio`, `persona` and `harness_options` are deliberately absent: the
142
192
  * record exists to be read, and must not become a place credentials collect.
143
193
  */
144
- function provenanceSettings(o, defaults) {
194
+ function provenanceSettings(o, defaults, resolvedLoops = []) {
145
195
  const perms = (defaults.permissions ?? {});
146
196
  const callerDefaults = new Set(o.inheritedFromCaller ?? []);
147
197
  const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
@@ -161,6 +211,16 @@ function provenanceSettings(o, defaults) {
161
211
  ? { value: 'declared via --isolation-file', source: 'cli' }
162
212
  : { value: defaults.isolation ? 'from fleet defaults' : undefined, source: defaults.isolation ? 'fleet-default' : 'built-in' },
163
213
  monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
214
+ loops: {
215
+ value: o.noLoops ? { enabled: false, loops: [] }
216
+ : resolvedLoops.length ? resolvedLoops.map(loop => ({
217
+ name: loop.name, enabled: loop.enabled, intervalMs: loop.intervalMs,
218
+ initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
219
+ prompt: { bytes: loop.promptBytes, sha256: loop.promptHash },
220
+ })) : 'omitted (legacy no temporary loops)',
221
+ source: o.loopSource === 'agent-template' ? 'agent-template'
222
+ : o.loopsFile || o.noLoops || o.loopSource === 'cli' ? 'cli' : 'built-in',
223
+ },
164
224
  };
165
225
  }
166
226
  /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
@@ -168,6 +228,9 @@ export async function spawnPermanent(o, deps, creation = {}) {
168
228
  validateSpawnOpts(o);
169
229
  if (o.isolationFile)
170
230
  readIsolationFile(o.isolationFile); // fail before reserving
231
+ // Preserve the detailed owner/source diagnostic for an existing identity.
232
+ // The check is repeated inside the reservation boundary to close races.
233
+ assertNameFree(o);
171
234
  const prepared = resolvedSpawn(o); // canonical validation before mutation
172
235
  // Reserve name and identity together before anything is written or started.
173
236
  // A loser of the race creates no config, state, or service.
@@ -221,7 +284,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
221
284
  // launch still records how it was asked for.
222
285
  const provenance = buildProvenance({
223
286
  role: o.name, lifetime: 'permanent', fleetVersion: VERSION,
224
- settings: provenanceSettings(o, cfg.defaults),
287
+ settings: provenanceSettings(o, cfg.defaults, prepared.role.temporaryLoops),
225
288
  surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
226
289
  });
227
290
  mkdirSync(agentDir(o.name), { recursive: true });
@@ -282,14 +345,19 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
282
345
  }
283
346
  async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
284
347
  const cfg = loadConfig(o.configPath);
348
+ const { temporaryLoops, ...launchRole } = preparedRole;
285
349
  const role = {
286
- ...preparedRole, sourceFile: '(temp)', loops: undefined, roomMemberStartup: o.roomMemberStartup,
350
+ ...launchRole, sourceFile: '(temp)',
351
+ ...(o.noLoops === true ? { loops: [] }
352
+ : temporaryLoops?.length ? { loops: temporaryLoops } : { loops: undefined }),
353
+ temporaryLoopSource: o.loopSource ?? (temporaryLoops?.length ? 'agent-template' : 'omitted'),
354
+ roomMemberStartup: o.roomMemberStartup,
287
355
  };
288
356
  onStage?.('writing_role');
289
357
  const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
290
358
  const provenance = buildProvenance({
291
359
  role: o.name, lifetime: 'temporary', fleetVersion: VERSION,
292
- settings: provenanceSettings(o, cfg.defaults),
360
+ settings: provenanceSettings(o, cfg.defaults, preparedRole.temporaryLoops),
293
361
  surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
294
362
  });
295
363
  writeProvenance(dir, provenance);
@@ -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', 'room_templates'];
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,7 @@ function readSources(manifest) {
176
176
  }
177
177
  trustedDirectory(dir);
178
178
  for (const name of readdirSync(dir).sort()) {
179
- if ((kind === 'agents' || kind === 'room_templates')
179
+ if ((kind === 'agents' || kind === 'agent_templates' || kind === 'room_templates')
180
180
  && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
181
181
  continue;
182
182
  documents.set(join(kind, name), trustedFile(join(dir, name)));
@@ -188,20 +188,26 @@ function modelFrom(sources) {
188
188
  const manifestKey = basename(sources.manifest);
189
189
  const manifest = parseFleetDocument(sources.manifest, sources.documents.get(manifestKey), 'strict').value;
190
190
  const agents = {};
191
+ const agent_templates = {};
191
192
  for (const [rel, source] of sources.documents)
192
193
  if (rel.startsWith(`agents/`)) {
193
194
  const id = basename(rel, extname(rel));
194
195
  agents[id] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
195
196
  }
196
- 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 };
197
203
  }
198
204
  function assertModel(value) {
199
205
  if (!value || typeof value !== 'object' || Array.isArray(value))
200
206
  throw new FleetError('invalid_request', 'model must be an object');
201
207
  const input = value;
202
- if (Object.keys(input).sort().join(',') !== 'agents,manifest')
203
- throw new FleetError('invalid_request', 'model allows exactly manifest and agents');
204
- 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'])
205
211
  if (!input[key] || typeof input[key] !== 'object' || Array.isArray(input[key]))
206
212
  throw new FleetError('invalid_request', `${key} must be an object`);
207
213
  for (const [id, doc] of Object.entries(input.agents)) {
@@ -210,6 +216,12 @@ function assertModel(value) {
210
216
  if (!doc || typeof doc !== 'object' || Array.isArray(doc))
211
217
  throw new FleetError('invalid_request', `Agent '${id}' must be an object`);
212
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
+ }
213
225
  return structuredClone(input);
214
226
  }
215
227
  function renderProposal(current, model) {
@@ -222,6 +234,12 @@ function renderProposal(current, model) {
222
234
  next.set(oldRel ?? join('agents', `${id}.yaml`), oldRel
223
235
  ? render(current.documents.get(oldRel), doc) : stringify(doc));
224
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
+ }
225
243
  for (const [rel, source] of current.documents)
226
244
  if (rel.startsWith('roles/') || rel.startsWith('brains/') || rel.startsWith('room_templates/'))
227
245
  next.set(rel, source);
@@ -425,7 +443,7 @@ function sourceSensitiveValues(sources) {
425
443
  continue;
426
444
  try {
427
445
  const parsed = parse(source);
428
- values.push(...sensitiveValues({ manifest: {}, agents: { source: parsed } }));
446
+ values.push(...sensitiveValues({ manifest: {}, agents: { source: parsed }, agent_templates: {} }));
429
447
  }
430
448
  catch { /* the authoritative parser reports malformed YAML */ }
431
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}.`,
217
+ };
218
+ });
192
219
  app.post('/api/v1/tasks', async (request, reply) => {
193
220
  auth.authenticate(request, true);
194
221
  const body = request.body;