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

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 +285 -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 +42 -3
  19. package/dist/config.js +202 -29
  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 +195 -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 +5 -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 +21 -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 +97 -47
  105. package/dist/session/acp.d.ts +27 -1
  106. package/dist/session/acp.js +307 -17
  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/stall-watchdog.d.ts +60 -0
  117. package/dist/session/stall-watchdog.js +210 -0
  118. package/dist/session/types.d.ts +29 -8
  119. package/dist/session/types.js +28 -0
  120. package/dist/spawn.d.ts +9 -0
  121. package/dist/spawn.js +77 -9
  122. package/dist/web/fleet-config-service.d.ts +1 -0
  123. package/dist/web/fleet-config-service.js +25 -7
  124. package/dist/web/runtime.js +11 -6
  125. package/dist/web/server.js +28 -1
  126. package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
  127. package/dist/web-app/index.html +1 -1
  128. package/package.json +4 -4
  129. package/presets/brain-catalog.json +17 -0
  130. package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
  131. package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
  132. package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
  133. package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
  134. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  135. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  136. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  137. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  138. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  139. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  140. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  141. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  142. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  143. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  144. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  145. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  146. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  147. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  148. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  149. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  150. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  151. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  152. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  153. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  154. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  155. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  156. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
  188. package/presets/fleet/roles/Coordinator.yaml +37 -0
  189. package/presets/fleet/roles/Critic.yaml +23 -1
  190. package/presets/fleet/roles/Developer.yaml +27 -5
  191. package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
  192. package/presets/fleet/room_templates/pair.yaml +9 -7
  193. package/presets/fleet/room_templates/single.yaml +5 -4
  194. package/presets/fleet/room_templates/team.yaml +12 -8
  195. package/presets/manifest.json +2 -1
  196. package/presets/fleet/agents/Architect.yaml +0 -3
  197. package/presets/fleet/agents/Secretary.yaml +0 -3
  198. package/presets/fleet/agents/Tester.yaml +0 -3
  199. package/presets/fleet/roles/Agent.yaml +0 -6
  200. package/presets/fleet/roles/Architect.yaml +0 -6
  201. package/presets/fleet/roles/Secretary.yaml +0 -6
  202. package/presets/fleet/roles/Tester.yaml +0 -6
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;