@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
package/dist/spawn.js CHANGED
@@ -1,11 +1,10 @@
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';
6
6
  import { validateIsolationConfig } from './isolation/policy.js';
7
- import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolveOwnerChannelConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, splitRootFor, validateMonitorConfig, } from './config.js';
8
- import { resolveRoleModelEnv } from './model-env.js';
7
+ import { loadConfig, findRole, splitRootFor, validateMonitorConfig, } from './config.js';
9
8
  import { applyRole, up } from './ops.js';
10
9
  import { START_STAGGER_FILE } from './runner.js';
11
10
  import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
@@ -13,86 +12,58 @@ import { VERSION } from './version.js';
13
12
  import { recordGeneratedAgentSource } from './generated-agent-source.js';
14
13
  import './harness/claude-code.js';
15
14
  import './harness/codex.js';
16
- import { getAdapter } from './harness/registry.js';
17
15
  import { archiveTempState, makeTempSupervisorLauncher, prepareTempSupervisor, reclaimStaleTempState, } from './temp-lifecycle.js';
18
16
  /**
19
17
  * The provenance record written by the most recent spawn in this process, so
20
18
  * the CLI can print the same summary it persisted rather than rebuilding it.
21
19
  */
22
20
  export let lastProvenance;
23
- export function profileValues(o) {
24
- if (o.bio !== undefined && o.bioFile)
25
- throw new Error('bio and bioFile are mutually exclusive');
26
- if (o.persona !== undefined && o.personaFile)
27
- throw new Error('persona and personaFile are mutually exclusive');
21
+ export function agentDefinitionFromSpawn(o) {
22
+ if (o.agentDefinition) {
23
+ if (o.brain !== undefined || o.role !== undefined || o.cwd !== undefined
24
+ || o.coordinator !== undefined || o.approval !== undefined || o.filesystem !== undefined
25
+ || o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined
26
+ || o.loopsFile !== undefined)
27
+ throw new Error('canonical agentDefinition conflicts with separate Agent fields');
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');
31
+ if (o.identity)
32
+ definition.identity = o.identity;
33
+ return definition;
34
+ }
35
+ if (!o.role)
36
+ throw new Error('--role is required (declared ID or inline mapping)');
37
+ if (!o.brain)
38
+ throw new Error('--brain is required (declared ID or inline mapping)');
39
+ const permissions = o.approval || o.filesystem || o.unattended ? {
40
+ ...(o.approval ? { approval: o.approval } : {}),
41
+ ...(o.filesystem ? { filesystem: o.filesystem } : {}),
42
+ ...(o.unattended ? { unattended: o.unattended } : {}),
43
+ } : undefined;
28
44
  return {
29
- bio: o.bio !== undefined ? o.bio.trim()
30
- : o.bioFile ? readFileSync(o.bioFile, 'utf8').trim() : undefined,
31
- persona: o.persona !== undefined ? o.persona.trim()
32
- : o.personaFile ? readFileSync(o.personaFile, 'utf8').trim() : undefined,
45
+ role: structuredClone(o.role), brain: structuredClone(o.brain),
46
+ ...(o.identity ? { identity: o.identity } : {}),
47
+ ...(o.cwd ? { cwd: o.cwd } : {}),
48
+ ...(o.coordinator ? { coordinator: o.coordinator } : {}),
49
+ ...(permissions ? { permissions } : {}),
50
+ ...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
51
+ ...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
52
+ ...(o.loopsFile ? { loops: readLoopsFile(o.loopsFile) } : {}),
33
53
  };
34
54
  }
35
- /** Pure option-to-role mapping shared by CLI and application services. */
36
- export function buildRoleConfig(o, defaultHarness) {
37
- const r = {};
38
- const harness = o.harness ?? defaultHarness;
39
- if (harness)
40
- r.harness = harness;
41
- if (o.session)
42
- r.session = o.session;
43
- if (o.identity)
44
- r.identity = o.identity;
45
- if (o.cwd)
46
- r.cwd = o.cwd;
47
- if (o.coordinator)
48
- r.coordinator = o.coordinator;
49
- if (o.missionFile)
50
- r.mission = readMissionFile(o.missionFile);
51
- else if (o.mission !== undefined)
52
- r.mission = o.mission;
53
- if (o.model === null)
54
- r.model = null;
55
- else if (o.model?.trim())
56
- r.model = o.model.trim();
57
- if (o.reasoningEffort?.trim())
58
- r.effort = o.reasoningEffort.trim();
59
- const harnessOptions = {};
60
- if (o.permissionMode)
61
- harnessOptions[harness === 'claude-code' ? 'permission_mode' : 'approval'] = o.permissionMode;
62
- if (o.sandbox)
63
- harnessOptions.sandbox = o.sandbox;
64
- if (o.profile)
65
- harnessOptions.profile = o.profile;
66
- if (o.launcher)
67
- harnessOptions.launcher = o.launcher;
68
- if (o.search === true)
69
- harnessOptions.search = true;
70
- const codexConfig = { ...(o.codexConfig ?? {}) };
71
- if (Object.keys(codexConfig).length)
72
- harnessOptions.config = codexConfig;
73
- if (o.addDirs?.length)
74
- harnessOptions.add_dirs = o.addDirs;
75
- if (o.monitor === true)
76
- harnessOptions.monitor = true;
77
- if (Object.keys(harnessOptions).length)
78
- r.harness_options = harnessOptions;
79
- if (o.approval || o.filesystem || o.unattended) {
80
- r.permissions = {
81
- ...(o.approval ? { approval: o.approval } : {}),
82
- ...(o.filesystem ? { filesystem: o.filesystem } : {}),
83
- ...(o.unattended ? { unattended: o.unattended } : {}),
84
- };
85
- }
86
- const profile = profileValues(o);
87
- if (profile.bio)
88
- r.bio = profile.bio;
89
- if (profile.persona)
90
- r.persona = profile.persona;
91
- if (o.isolationFile)
92
- r.isolation = readIsolationFile(o.isolationFile);
93
- if (o.monitorConfig)
94
- r.monitor = { ...o.monitorConfig };
95
- return r;
55
+ function resolvedSpawn(o) {
56
+ const definition = agentDefinitionFromSpawn(o);
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 };
96
67
  }
97
68
  /**
98
69
  * Read and validate an `--isolation-file`. The file is the existing
@@ -119,13 +90,37 @@ export function readIsolationFile(path) {
119
90
  throw new Error(`--isolation-file ${path}: ${problems.join('; ')}`);
120
91
  return cfg;
121
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
+ }
122
117
  export function validateSpawnOpts(o) {
123
- if (o.mission !== undefined && o.missionFile)
124
- throw new Error('--mission and --mission-file are mutually exclusive');
125
- if (o.session === 'tmux')
126
- throw new Error("invalid --session 'tmux'; tmux is no longer supported; use --session acp");
127
- if (o.session && o.session !== 'acp')
128
- throw new Error(`invalid --session '${o.session}'; allowed: acp`);
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);
129
124
  if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
130
125
  throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
131
126
  if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
@@ -136,23 +131,12 @@ export function validateSpawnOpts(o) {
136
131
  throw new Error(`invalid role name '${o.name}'`);
137
132
  if (o.identity && !/^[A-Za-z0-9_-]+$/.test(o.identity))
138
133
  throw new Error(`invalid identity name '${o.identity}'`);
139
- if (o.model && (o.model.length > 128 || /[\0-\x1f\x7f]/.test(o.model)))
140
- throw new Error('model must be printable and at most 128 characters');
141
134
  if (o.monitorConfig) {
142
135
  const problems = validateMonitorConfig(o.monitorConfig);
143
136
  if (problems.length)
144
137
  throw new Error(problems.join('; '));
145
138
  }
146
139
  }
147
- /** Read mission text without trimming or newline rewriting. */
148
- export function readMissionFile(path) {
149
- try {
150
- return readFileSync(path, 'utf8');
151
- }
152
- catch (e) {
153
- throw new Error(`--mission-file ${path}: ${e.message}`);
154
- }
155
- }
156
140
  /**
157
141
  * Reject names that are already USED. This is a precondition, not a claim: it
158
142
  * runs INSIDE the creation transaction, after both names are reserved, so the
@@ -160,7 +144,15 @@ export function readMissionFile(path) {
160
144
  * by the reservation rather than by this function.
161
145
  */
162
146
  function assertNameFree(o) {
163
- 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
+ }
164
156
  if (cfg.roles.some(r => r.name === o.name))
165
157
  throw new Error(`role '${o.name}' already exists (${cfg.roles.find(r => r.name === o.name).sourceFile})`);
166
158
  if (existsSync(agentDir(o.name)) || existsSync(agentDir(o.name, true)))
@@ -173,23 +165,6 @@ function assertNameFree(o) {
173
165
  }
174
166
  /** The ours identity a spawn will bind: explicit, else the role name. */
175
167
  export const effectiveIdentity = (o) => o.identity ?? o.name;
176
- /** Bare Agent document written by v2 spawn: inline Role × inline Brain + operations. */
177
- export function buildAgentDocument(raw) {
178
- const role = Object.fromEntries(['mission', 'persona', 'bio', 'briefing_file']
179
- .filter(key => raw[key] !== undefined)
180
- .map(key => [key, raw[key]]));
181
- const brain = Object.fromEntries([
182
- 'harness', 'session', 'session_options', 'model', 'model_chain', 'max_tokens',
183
- 'autocompact_pct', 'harness_options', 'effort',
184
- ].filter(key => raw[key] !== undefined)
185
- .map(key => [key, raw[key]]));
186
- const operational = Object.fromEntries([
187
- 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee', 'isolation',
188
- 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
189
- ].filter(key => raw[key] !== undefined)
190
- .map(key => [key, raw[key]]));
191
- return { role: { inline: role }, brain: { inline: brain }, ...operational };
192
- }
193
168
  /**
194
169
  * Validate and resolve a spawn without reserving names, contacting the daemon,
195
170
  * or writing state. Collision checks are necessarily a point-in-time snapshot.
@@ -198,68 +173,13 @@ export function spawnDryRun(o) {
198
173
  validateSpawnOpts(o);
199
174
  if (o.isolationFile)
200
175
  readIsolationFile(o.isolationFile);
201
- if (o.missionFile)
202
- readMissionFile(o.missionFile);
203
176
  assertNameFree(o);
204
- const cfg = loadConfig(o.configPath);
205
- const raw = buildRoleConfig(o, cfg.defaults.harness ?? 'claude-code');
206
- const harnessOptions = {
207
- ...(cfg.defaults.harness_options ?? {}),
208
- ...(raw.harness_options ?? {}),
209
- };
210
- const harness = raw.harness ?? cfg.defaults.harness ?? 'claude-code';
211
- const defaultHarness = cfg.defaults.harness ?? 'claude-code';
212
- const inheritsModelDefaults = harness === defaultHarness && raw.model !== null;
213
- const authProxy = resolveAuthProxy(cfg.defaults.auth_proxy, raw.auth_proxy);
214
- // One resolution for the environment and the model it pins (src/model-env.ts).
215
- const modelEnv = resolveRoleModelEnv({
216
- harness,
217
- model: resolveRoleModel(raw.model, raw.harness, cfg.defaults),
218
- modelWasExplicit: raw.model !== undefined,
219
- defaultsEnv: (cfg.defaults.env ?? {}),
220
- roleEnv: raw.env,
221
- ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
222
- });
223
- const adapter = getAdapter(harness);
224
- const brain = adapter.agentSession.resolveBrain({
225
- model: modelEnv.model,
226
- effort: raw.effort,
227
- harnessOptions: Object.keys(harnessOptions).length ? harnessOptions : undefined,
228
- });
229
- const model = brain.model ?? undefined;
230
- const session = raw.session ?? cfg.defaults.session ?? 'acp';
231
- const resolvedRole = {
232
- ...raw,
233
- name: o.name,
234
- sourceFile: o.temp ? '(temp dry-run)' : join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents', `${o.name}.yaml`),
235
- harness,
236
- session,
237
- session_options: raw.session_options,
238
- permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
239
- permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
240
- identity: effectiveIdentity(o),
241
- effort: raw.effort,
242
- model,
243
- model_chain: resolveModelChain(model, raw.model_chain ?? (inheritsModelDefaults
244
- ? cfg.defaults.model_chain
245
- : undefined)),
246
- harness_options: brain.harnessOptions,
247
- isolation: raw.isolation ?? cfg.defaults.isolation,
248
- monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
249
- owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
250
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, raw.worklog),
251
- auth_proxy: authProxy,
252
- };
253
- resolvedRole.env = modelEnv.env;
254
- if (resolvedRole.auth_proxy && resolvedRole.harness !== 'claude-code')
255
- throw new Error('auth_proxy is supported only by claude-code');
256
- const optionProblems = adapter.validateOptions(resolvedRole.harness_options, resolvedRole);
257
- if (optionProblems.length)
258
- throw new Error(optionProblems.map(problem => `${problem.path}: ${problem.message}`).join('; '));
177
+ const resolved = resolvedSpawn(o);
178
+ const resolvedRole = resolved.role;
259
179
  return {
260
180
  schemaVersion: 1,
261
181
  warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
262
- roleDocument: buildAgentDocument(raw),
182
+ roleDocument: structuredClone(resolved.definition),
263
183
  resolvedRole,
264
184
  };
265
185
  }
@@ -271,26 +191,19 @@ export function spawnDryRun(o) {
271
191
  * `env`, `bio`, `persona` and `harness_options` are deliberately absent: the
272
192
  * record exists to be read, and must not become a place credentials collect.
273
193
  */
274
- function provenanceSettings(o, defaults) {
194
+ function provenanceSettings(o, defaults, resolvedLoops = []) {
275
195
  const perms = (defaults.permissions ?? {});
276
196
  const callerDefaults = new Set(o.inheritedFromCaller ?? []);
277
197
  const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
278
- const explicitModel = typeof o.model === 'string' ? o.model.trim() : undefined;
279
- const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
198
+ const selection = (value) => value && 'ref' in value ? `ref:${value.ref}` : value ? 'inline' : undefined;
280
199
  return {
281
- harness: tagged('harness', provenanceOf(o.harness, defaults.harness, 'claude-code')),
282
- session: tagged('session', provenanceOf(o.session, defaults.session, 'acp')),
200
+ brain: tagged('brain', provenanceOf(selection(o.brain), undefined, undefined)),
201
+ role: tagged('role', provenanceOf(selection(o.role), undefined, undefined)),
283
202
  identity: o.identity
284
203
  ? { value: o.identity, source: 'cli' }
285
204
  : { value: o.name, source: 'built-in' }, // defaults to the role name
286
205
  cwd: tagged('cwd', provenanceOf(o.cwd, undefined, undefined)),
287
- model: tagged('model', o.model === null
288
- ? { value: undefined, source: 'cli' }
289
- : explicitModel
290
- ? { value: explicitModel, source: 'cli' }
291
- : { value: inheritedModel, source: inheritedModel ? 'fleet-default' : 'built-in' }),
292
206
  coordinator: tagged('coordinator', provenanceOf(o.coordinator, undefined, undefined)),
293
- permission_mode: provenanceOf(o.permissionMode, undefined, undefined),
294
207
  approval: tagged('approval', provenanceOf(o.approval, perms.approval, 'ask')),
295
208
  filesystem: tagged('filesystem', provenanceOf(o.filesystem, perms.filesystem, 'workspace')),
296
209
  unattended: tagged('unattended', provenanceOf(o.unattended, perms.unattended, 'deny')),
@@ -298,6 +211,16 @@ function provenanceSettings(o, defaults) {
298
211
  ? { value: 'declared via --isolation-file', source: 'cli' }
299
212
  : { value: defaults.isolation ? 'from fleet defaults' : undefined, source: defaults.isolation ? 'fleet-default' : 'built-in' },
300
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
+ },
301
224
  };
302
225
  }
303
226
  /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
@@ -305,8 +228,10 @@ export async function spawnPermanent(o, deps, creation = {}) {
305
228
  validateSpawnOpts(o);
306
229
  if (o.isolationFile)
307
230
  readIsolationFile(o.isolationFile); // fail before reserving
308
- if (o.missionFile)
309
- readMissionFile(o.missionFile); // 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);
234
+ const prepared = resolvedSpawn(o); // canonical validation before mutation
310
235
  // Reserve name and identity together before anything is written or started.
311
236
  // A loser of the race creates no config, state, or service.
312
237
  creation.onStage?.('reserving');
@@ -316,7 +241,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
316
241
  // Establish the identity BEFORE the service is enabled, and record
317
242
  // what was actually guaranteed so the briefing can say something true.
318
243
  creation.onStage?.('checking_identity');
319
- const guarantee = await ensureIdentity(effectiveIdentity(o), profileValues(o), creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
244
+ const guarantee = await ensureIdentity(effectiveIdentity(o), { bio: prepared.role.bio, persona: prepared.role.persona }, creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
320
245
  creation.onStage?.('checking_identity', {
321
246
  result: guarantee.evidence, guarantee: guarantee.state,
322
247
  });
@@ -333,7 +258,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
333
258
  mkdirSync(agentRoot, { recursive: true });
334
259
  creation.onStage?.('writing_role');
335
260
  const file = join(agentRoot, `${o.name}.yaml`);
336
- writeRoleFile(tx, file, stringify(buildAgentDocument(buildRoleConfig(o, cfg.defaults.harness ?? 'claude-code'))));
261
+ writeRoleFile(tx, file, stringify(agentDefinitionFromSpawn(o)));
337
262
  // `up` materialises the state dir and registers the service. Journal the
338
263
  // dir before it exists so a failure leaves the name genuinely reusable
339
264
  // rather than blocked by a half-built directory.
@@ -359,7 +284,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
359
284
  // launch still records how it was asked for.
360
285
  const provenance = buildProvenance({
361
286
  role: o.name, lifetime: 'permanent', fleetVersion: VERSION,
362
- settings: provenanceSettings(o, cfg.defaults),
287
+ settings: provenanceSettings(o, cfg.defaults, prepared.role.temporaryLoops),
363
288
  surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
364
289
  });
365
290
  mkdirSync(agentDir(o.name), { recursive: true });
@@ -402,8 +327,7 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
402
327
  validateSpawnOpts(o);
403
328
  if (o.isolationFile)
404
329
  readIsolationFile(o.isolationFile); // fail before reserving
405
- if (o.missionFile)
406
- readMissionFile(o.missionFile); // fail before reserving
330
+ const prepared = resolvedSpawn(o); // canonical validation before mutation
407
331
  // Retire only supervisors whose recorded owner is definitively stopped. This
408
332
  // bounded pass keeps the active roster clean without deleting old evidence.
409
333
  await reclaimStaleTempState();
@@ -416,68 +340,24 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
416
340
  creation.onStage?.('checking_identity', {
417
341
  result: 'unknown', guarantee: 'unverified',
418
342
  });
419
- return spawnTempInner(o, binPath, launch, tx, creation.onStage);
343
+ return spawnTempInner(o, prepared.role, binPath, launch, tx, creation.onStage);
420
344
  }, creation);
421
345
  }
422
- async function spawnTempInner(o, binPath, launch, tx, onStage) {
346
+ async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
423
347
  const cfg = loadConfig(o.configPath);
424
- const defaultHarness = cfg.defaults.harness;
425
- const fromOpts = buildRoleConfig(o, defaultHarness);
426
- const mergedHarnessOptions = {
427
- ...(cfg.defaults.harness_options ?? {}),
428
- ...(fromOpts.harness_options ?? {}),
429
- };
430
- const harness = o.harness ?? defaultHarness ?? 'claude-code';
431
- const inheritsModelDefaults = harness === (defaultHarness ?? 'claude-code') && o.model !== null;
432
- const tempAuthProxy = resolveAuthProxy(cfg.defaults.auth_proxy, fromOpts.auth_proxy);
433
- // An explicitly requested --model must reach the child, not just the banner
434
- // (src/model-env.ts).
435
- const modelEnv = resolveRoleModelEnv({
436
- harness,
437
- model: resolveRoleModel(o.model, o.harness, cfg.defaults),
438
- modelWasExplicit: o.model !== undefined,
439
- defaultsEnv: (cfg.defaults.env ?? {}),
440
- roleEnv: fromOpts.env,
441
- ...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
442
- });
443
- const adapter = getAdapter(harness);
444
- const brain = adapter.agentSession.resolveBrain({
445
- model: modelEnv.model,
446
- effort: fromOpts.effort,
447
- harnessOptions: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
448
- });
449
- const model = brain.model ?? undefined;
450
- const session = o.session ?? cfg.defaults.session ?? 'acp';
348
+ const { temporaryLoops, ...launchRole } = preparedRole;
451
349
  const role = {
452
- ...fromOpts, // includes `isolation` when --isolation-file was given
453
- name: o.name,
454
- harness,
455
- session,
456
- identity: o.identity ?? o.name,
457
- effort: fromOpts.effort,
458
- model,
459
- model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
460
- ? cfg.defaults.model_chain
461
- : undefined)),
462
- harness_options: brain.harnessOptions,
463
- permissions: resolvePermissions(cfg.defaults.permissions, fromOpts.permissions),
464
- permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
465
- // Temp agents inherit the fleet-wide monitor defaults via the snapshot.
466
- monitor: resolveMonitorConfig(cfg.defaults.monitor, fromOpts.monitor),
467
- owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, fromOpts.owner_channel, session),
468
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, fromOpts.worklog),
469
- auth_proxy: tempAuthProxy,
470
- sourceFile: '(temp)',
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'),
471
354
  roomMemberStartup: o.roomMemberStartup,
472
355
  };
473
- role.env = modelEnv.env;
474
- if (role.auth_proxy && role.harness !== 'claude-code')
475
- throw new Error('auth_proxy is supported only by claude-code');
476
356
  onStage?.('writing_role');
477
357
  const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
478
358
  const provenance = buildProvenance({
479
359
  role: o.name, lifetime: 'temporary', fleetVersion: VERSION,
480
- settings: provenanceSettings(o, cfg.defaults),
360
+ settings: provenanceSettings(o, cfg.defaults, preparedRole.temporaryLoops),
481
361
  surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
482
362
  });
483
363
  writeProvenance(dir, provenance);
@@ -5,7 +5,7 @@ export declare const TEMP_STOP_REQUEST_FILE = ".temp-stop-request.json";
5
5
  export declare const TEMP_RECLAIM_BATCH = 32;
6
6
  export declare const TEMP_LAUNCH_GRACE_MS = 60000;
7
7
  export type TempSupervisorKind = 'systemd-transient' | 'launchd-transient' | 'detached';
8
- export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'startup-failure' | 'stale-supervisor';
8
+ export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'supervisor-recycle' | 'startup-failure' | 'stale-supervisor';
9
9
  export interface TempSupervisorRecord {
10
10
  version: 1;
11
11
  role: string;
@@ -1,19 +1,19 @@
1
- import type { FleetConfig, ResolvedRole, SessionBackendId } from '../config.js';
1
+ import type { AgentDefinition, FleetConfig, ResolvedRole } from '../config.js';
2
+ import type { SessionBackendId } from '../config.js';
2
3
  import type { IsolationConfig } from '../isolation/types.js';
3
4
  export interface WatchdogConfig {
4
5
  coordinator?: string;
5
6
  enabled?: boolean;
6
7
  interval?: string;
7
8
  watch?: string[];
8
- harness?: string;
9
- model?: string | null;
10
- session?: string;
9
+ agent?: {
10
+ ref: string;
11
+ } | AgentDefinition;
11
12
  identity?: string;
12
13
  timeout?: string;
13
14
  keep_reports?: number;
14
15
  alert_cooldown?: string;
15
16
  prompt_file?: string;
16
- isolation?: IsolationConfig;
17
17
  }
18
18
  export interface ResolvedWatchdog {
19
19
  name: string;
@@ -22,15 +22,18 @@ export interface ResolvedWatchdog {
22
22
  intervalMs: number;
23
23
  watch: string[];
24
24
  watchExplicit: boolean;
25
+ agentDefinition: AgentDefinition;
26
+ resolvedAgent: ResolvedRole;
27
+ /** Resolved/read-only runtime facts; never independent authoring fields. */
25
28
  harness: string;
26
29
  session: SessionBackendId;
27
30
  model?: string;
31
+ isolation?: IsolationConfig;
28
32
  identity: string;
29
33
  timeoutMs: number;
30
34
  keepReports: number;
31
35
  alertCooldownMs: number;
32
36
  promptFile?: string;
33
- isolation?: IsolationConfig;
34
37
  sourceFile: string;
35
38
  }
36
39
  export declare const WATCHDOG_DEFAULT_INTERVAL_MS = 600000;
@@ -38,7 +41,7 @@ export declare const WATCHDOG_MIN_INTERVAL_MS = 60000;
38
41
  export declare const WATCHDOG_DEFAULT_TIMEOUT_MS = 300000;
39
42
  export declare const WATCHDOG_DEFAULT_KEEP_REPORTS = 50;
40
43
  export declare const WATCHDOG_DEFAULT_COOLDOWN_MS = 3600000;
41
- export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, defaults: Record<string, unknown>): ResolvedWatchdog[];
44
+ export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, agentDefinitions: Record<string, AgentDefinition>, resolveInline: (name: string, definition: AgentDefinition) => ResolvedRole): ResolvedWatchdog[];
42
45
  /**
43
46
  * Split `restart`'s argument names into watchdogs (a release, handled
44
47
  * directly) and roles (handed to restartRoles). Watchdog names can't collide
@@ -1,11 +1,10 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { isAbsolute } from 'node:path';
3
3
  import { parseDuration } from '../duration.js';
4
- import { ConfigError, ROLE_NAME_RE, resolveRoleModel } from '../config.js';
5
- import { validateIsolationConfig } from '../isolation/policy.js';
4
+ import { ConfigError, ROLE_NAME_RE } from '../config.js';
6
5
  const WATCHDOG_KEYS = [
7
- 'coordinator', 'enabled', 'interval', 'watch', 'harness', 'model', 'session',
8
- 'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file', 'isolation',
6
+ 'coordinator', 'enabled', 'interval', 'watch', 'agent',
7
+ 'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file',
9
8
  ];
10
9
  export const WATCHDOG_DEFAULT_INTERVAL_MS = 600_000;
11
10
  export const WATCHDOG_MIN_INTERVAL_MS = 60_000;
@@ -21,7 +20,7 @@ function deepSub(v, vars) {
21
20
  return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deepSub(x, vars)]));
22
21
  return v;
23
22
  }
24
- export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
23
+ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, agentDefinitions, resolveInline) {
25
24
  const block = baseDoc.watchdogs;
26
25
  if (block === undefined || block === null)
27
26
  return [];
@@ -42,6 +41,9 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
42
41
  if (roleNames.has(name))
43
42
  throw new ConfigError(`${baseFile}: watchdog '${name}' collides with a role name`);
44
43
  const w = deepSub(rawEntry ?? {}, vars);
44
+ const legacy = ['harness', 'model', 'session', 'isolation'].filter(key => Object.hasOwn(w, key));
45
+ if (legacy.length)
46
+ throw new ConfigError(`${where}: E_LEGACY Brain/Agent-owned field(s) ${legacy.join(', ')} are unsupported; select a canonical Agent with agent`);
45
47
  const bad = Object.keys(w).filter(k => !WATCHDOG_KEYS.includes(k));
46
48
  if (bad.length)
47
49
  throw new ConfigError(`${where} has unknown key(s) ${bad.join(', ')}; allowed: ${WATCHDOG_KEYS.join(', ')}`);
@@ -60,23 +62,29 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
60
62
  if (collidingWatchdog !== undefined)
61
63
  throw new ConfigError(`${where}: identity '${identity}' collides with watchdog '${collidingWatchdog}'`);
62
64
  watchdogIdentities.set(identity, name);
63
- const harness = w.harness ?? defaults.harness ?? 'claude-code';
64
- const sessionRaw = w.session ?? defaults.session ?? 'acp';
65
- if (sessionRaw === 'tmux')
66
- throw new ConfigError(`${where}: session 'tmux' is no longer supported; use session: acp`);
67
- if (sessionRaw !== 'acp')
68
- throw new ConfigError(`${where}: session must be 'acp'`);
65
+ if (!w.agent || typeof w.agent !== 'object' || Array.isArray(w.agent))
66
+ throw new ConfigError(`${where}: agent is required (Agent ref or canonical Agent definition)`);
67
+ let agentDefinition;
68
+ let resolvedAgent;
69
+ if ('ref' in w.agent) {
70
+ const ref = w.agent.ref;
71
+ if (typeof ref !== 'string' || !ref)
72
+ throw new ConfigError(`${where}: agent.ref must be a declared Agent ID`);
73
+ agentDefinition = agentDefinitions[ref];
74
+ resolvedAgent = roles.find(role => role.name === ref);
75
+ if (!agentDefinition || !resolvedAgent)
76
+ throw new ConfigError(`${where}: Agent ref '${ref}' was not found`);
77
+ }
78
+ else {
79
+ agentDefinition = structuredClone(w.agent);
80
+ resolvedAgent = resolveInline(name, agentDefinition);
81
+ }
69
82
  if (w.prompt_file !== undefined) {
70
83
  if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
71
84
  throw new ConfigError(`${where}: prompt_file must be an absolute path`);
72
85
  if (!existsSync(w.prompt_file))
73
86
  throw new ConfigError(`${where}: prompt_file not found: ${w.prompt_file}`);
74
87
  }
75
- if (w.isolation !== undefined) {
76
- const problems = validateIsolationConfig(w.isolation);
77
- if (problems.length)
78
- throw new ConfigError(`${where} ${problems.join('; ')}`);
79
- }
80
88
  const dur = (v, key, fallback, minMs) => {
81
89
  if (v === undefined)
82
90
  return fallback;
@@ -96,13 +104,15 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
96
104
  name, coordinator: w.coordinator.trim(),
97
105
  enabled: w.enabled ?? true,
98
106
  intervalMs: dur(w.interval, 'interval', WATCHDOG_DEFAULT_INTERVAL_MS, WATCHDOG_MIN_INTERVAL_MS),
99
- watch, watchExplicit: w.watch !== undefined, harness, session: sessionRaw,
100
- model: resolveRoleModel(w.model, w.harness, defaults),
107
+ watch, watchExplicit: w.watch !== undefined,
108
+ agentDefinition: structuredClone(agentDefinition), resolvedAgent: structuredClone(resolvedAgent),
109
+ harness: resolvedAgent.harness, session: resolvedAgent.session,
110
+ model: resolvedAgent.model, isolation: resolvedAgent.isolation,
101
111
  identity,
102
112
  timeoutMs: dur(w.timeout, 'timeout', WATCHDOG_DEFAULT_TIMEOUT_MS),
103
113
  keepReports,
104
114
  alertCooldownMs: dur(w.alert_cooldown, 'alert_cooldown', WATCHDOG_DEFAULT_COOLDOWN_MS),
105
- promptFile: w.prompt_file, isolation: w.isolation, sourceFile: baseFile,
115
+ promptFile: w.prompt_file, sourceFile: baseFile,
106
116
  });
107
117
  }
108
118
  return out;