@ours.network/fleet 1.0.5 → 1.1.0-nightly.10

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 (159) hide show
  1. package/README.md +122 -133
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.d.ts +0 -1
  5. package/dist/application/capabilities.js +3 -13
  6. package/dist/application/fleet-query-service.d.ts +0 -3
  7. package/dist/application/fleet-query-service.js +14 -25
  8. package/dist/application/role-creation-service.d.ts +5 -24
  9. package/dist/application/role-creation-service.js +27 -67
  10. package/dist/application/role-removal-service.js +6 -3
  11. package/dist/application/role-repository.d.ts +1 -2
  12. package/dist/application/role-repository.js +7 -7
  13. package/dist/application/session-control.d.ts +0 -12
  14. package/dist/application/session-control.js +0 -42
  15. package/dist/application/session-mutations.d.ts +5 -5
  16. package/dist/application/task-room-service.d.ts +1 -1
  17. package/dist/application/task-room-service.js +154 -16
  18. package/dist/application/types.d.ts +1 -8
  19. package/dist/briefing.js +12 -11
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +143 -63
  22. package/dist/config-yaml.d.ts +1 -1
  23. package/dist/config.d.ts +66 -6
  24. package/dist/config.js +398 -43
  25. package/dist/creation.d.ts +1 -1
  26. package/dist/creation.js +2 -2
  27. package/dist/daemon-recovery.d.ts +93 -0
  28. package/dist/daemon-recovery.js +328 -0
  29. package/dist/docs.d.ts +1 -1
  30. package/dist/docs.js +103 -127
  31. package/dist/doctor.js +18 -11
  32. package/dist/fleet-command-audit.d.ts +160 -0
  33. package/dist/fleet-command-audit.js +433 -0
  34. package/dist/fleet-proxy.d.ts +3 -1
  35. package/dist/fleet-proxy.js +30 -8
  36. package/dist/generated-agent-source.d.ts +9 -0
  37. package/dist/generated-agent-source.js +53 -0
  38. package/dist/harness/acp-session-transport.d.ts +8 -0
  39. package/dist/harness/acp-session-transport.js +3 -0
  40. package/dist/harness/agent-session.d.ts +51 -0
  41. package/dist/harness/agent-session.js +1 -0
  42. package/dist/harness/claude-code-session.d.ts +24 -0
  43. package/dist/harness/claude-code-session.js +36 -0
  44. package/dist/harness/claude-code.d.ts +2 -1
  45. package/dist/harness/claude-code.js +41 -81
  46. package/dist/harness/codex-session.d.ts +24 -0
  47. package/dist/harness/codex-session.js +36 -0
  48. package/dist/harness/codex.d.ts +2 -1
  49. package/dist/harness/codex.js +55 -45
  50. package/dist/harness/registry.js +4 -0
  51. package/dist/harness/types.d.ts +6 -38
  52. package/dist/index.d.ts +4 -6
  53. package/dist/index.js +2 -4
  54. package/dist/init-guidance.d.ts +1 -0
  55. package/dist/init-guidance.js +6 -0
  56. package/dist/isolation/resources.d.ts +3 -7
  57. package/dist/isolation/resources.js +3 -7
  58. package/dist/model-env.d.ts +4 -6
  59. package/dist/model-env.js +5 -8
  60. package/dist/monitor.d.ts +1 -24
  61. package/dist/monitor.js +5 -140
  62. package/dist/ops.js +4 -2
  63. package/dist/owner-channel/channel.d.ts +58 -2
  64. package/dist/owner-channel/channel.js +269 -29
  65. package/dist/owner-channel/commands.d.ts +2 -0
  66. package/dist/owner-channel/commands.js +31 -13
  67. package/dist/owner-channel/state.d.ts +2 -0
  68. package/dist/owner-channel/state.js +8 -2
  69. package/dist/preset-bootstrap.d.ts +10 -0
  70. package/dist/preset-bootstrap.js +85 -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.d.ts +3 -2
  84. package/dist/resolved-plan.js +21 -5
  85. package/dist/rooms-tasks/cli.js +225 -40
  86. package/dist/rooms-tasks/config.js +24 -13
  87. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  88. package/dist/rooms-tasks/external-worker.js +14 -9
  89. package/dist/rooms-tasks/provision.js +98 -36
  90. package/dist/rooms-tasks/templates.d.ts +0 -1
  91. package/dist/rooms-tasks/templates.js +9 -76
  92. package/dist/rooms-tasks/types.d.ts +10 -16
  93. package/dist/rooms-tasks/types.js +1 -4
  94. package/dist/runner.d.ts +13 -16
  95. package/dist/runner.js +110 -122
  96. package/dist/sensitive-config.d.ts +2 -0
  97. package/dist/sensitive-config.js +4 -0
  98. package/dist/session/acp.d.ts +7 -2
  99. package/dist/session/acp.js +29 -6
  100. package/dist/session/activity.d.ts +2 -3
  101. package/dist/session/activity.js +2 -3
  102. package/dist/session/arbiter.d.ts +4 -4
  103. package/dist/session/control.d.ts +17 -4
  104. package/dist/session/control.js +35 -0
  105. package/dist/session/types.d.ts +11 -5
  106. package/dist/spawn.d.ts +8 -33
  107. package/dist/spawn.js +53 -213
  108. package/dist/supervisor/none.d.ts +1 -1
  109. package/dist/supervisor/none.js +2 -2
  110. package/dist/temp-lifecycle.d.ts +1 -1
  111. package/dist/tmux.d.ts +1 -14
  112. package/dist/tmux.js +1 -51
  113. package/dist/watchdog/config.d.ts +10 -7
  114. package/dist/watchdog/config.js +30 -18
  115. package/dist/watchdog/run.js +12 -36
  116. package/dist/web/auth.d.ts +1 -1
  117. package/dist/web/fleet-config-service.d.ts +11 -8
  118. package/dist/web/fleet-config-service.js +384 -183
  119. package/dist/web/runtime.js +4 -15
  120. package/dist/web/server.d.ts +1 -3
  121. package/dist/web/server.js +3 -14
  122. package/dist/web/topology-promote.js +26 -19
  123. package/dist/web/topology.js +2 -2
  124. package/dist/web/yaml-document-edit.js +2 -0
  125. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  126. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  127. package/dist/web-app/index.html +2 -2
  128. package/examples/fleet/agents/Alice.yaml +11 -0
  129. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  130. package/examples/fleet/brains/claude-default.yaml +5 -0
  131. package/examples/fleet/roles/coordinator.yaml +5 -0
  132. package/examples/fleet/roles/developer.yaml +3 -0
  133. package/examples/fleet.yaml +74 -0
  134. package/package.json +5 -8
  135. package/presets/fleet/agents/Agent.yaml +3 -0
  136. package/presets/fleet/agents/Architect.yaml +3 -0
  137. package/presets/fleet/agents/Critic.yaml +3 -0
  138. package/presets/fleet/agents/Developer.yaml +3 -0
  139. package/presets/fleet/agents/Secretary.yaml +3 -0
  140. package/presets/fleet/agents/Tester.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/roles/Agent.yaml +6 -0
  143. package/presets/fleet/roles/Architect.yaml +6 -0
  144. package/presets/fleet/roles/Critic.yaml +6 -0
  145. package/presets/fleet/roles/Developer.yaml +6 -0
  146. package/presets/fleet/roles/Secretary.yaml +6 -0
  147. package/presets/fleet/roles/Tester.yaml +6 -0
  148. package/presets/fleet/room_templates/pair.yaml +11 -0
  149. package/presets/fleet/room_templates/single.yaml +8 -0
  150. package/presets/fleet/room_templates/team.yaml +12 -0
  151. package/presets/fleet.yaml +7 -0
  152. package/presets/manifest.json +5 -0
  153. package/dist/session/tmux.d.ts +0 -28
  154. package/dist/session/tmux.js +0 -80
  155. package/dist/web/terminal/bridge.d.ts +0 -27
  156. package/dist/web/terminal/bridge.js +0 -317
  157. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  158. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  159. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -1,10 +1,10 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
3
  import { parse } from 'yaml';
4
4
  import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
5
5
  import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
6
6
  import { spawnTemp } from '../spawn.js';
7
- import { findRole } from '../config.js';
7
+ import { findRole, loadConfig } from '../config.js';
8
8
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
9
9
  import { controlRequest } from '../session/control.js';
10
10
  import { SessionControlError } from '../session/types.js';
@@ -13,6 +13,7 @@ import { buildRoomMemberTask, sha256Text } from './member-startup.js';
13
13
  import { agentDir } from '../paths.js';
14
14
  import { readProvenance } from '../creation.js';
15
15
  import { readTempSupervisor, secureStoppedTempArchive, tempArchiveForCreationAction, tempSupervisorLiveness, } from '../temp-lifecycle.js';
16
+ import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
16
17
  export function getBinPath() {
17
18
  try {
18
19
  return realpathSync(process.argv[1]);
@@ -21,6 +22,45 @@ export function getBinPath() {
21
22
  return process.argv[1];
22
23
  }
23
24
  }
25
+ function canonicalJson(value) {
26
+ if (Array.isArray(value))
27
+ return `[${value.map(canonicalJson).join(',')}]`;
28
+ if (value && typeof value === 'object')
29
+ return `{${Object.entries(value)
30
+ .sort(([a], [b]) => a.localeCompare(b))
31
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
32
+ return JSON.stringify(value);
33
+ }
34
+ function selectionSummary(selection) {
35
+ if ('ref' in selection)
36
+ return `ref:${selection.ref} (reference)`;
37
+ return `inline:sha256:${createHash('sha256').update(canonicalJson(selection.inline)).digest('hex').slice(0, 16)} (inline)`;
38
+ }
39
+ function permissionSummary(definition) {
40
+ const permissions = definition.permissions;
41
+ if (!permissions)
42
+ return undefined;
43
+ return `approval=${permissions.approval},filesystem=${permissions.filesystem},unattended=${permissions.unattended}`;
44
+ }
45
+ function launchDefinition(definition) {
46
+ const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : {
47
+ kind: 'inline', fingerprint: `sha256:${createHash('sha256')
48
+ .update(canonicalJson(selection.inline)).digest('hex').slice(0, 16)}`,
49
+ };
50
+ return {
51
+ projection: {
52
+ brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
53
+ ...(definition.permissions ? { permissions: {
54
+ approval: definition.permissions.approval,
55
+ filesystem: definition.permissions.filesystem,
56
+ unattended: definition.permissions.unattended,
57
+ } } : {}),
58
+ operationalFields: Object.keys(definition)
59
+ .filter(key => key !== 'brain' && key !== 'role').sort(),
60
+ },
61
+ fingerprint: createHash('sha256').update(canonicalJson(definition)).digest('hex'),
62
+ };
63
+ }
24
64
  async function spawnRoomMember(options, binPath) {
25
65
  const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
26
66
  if (!stateDir)
@@ -52,28 +92,31 @@ function expandMembers(template, prefix) {
52
92
  name: `${prefix}-${slot.slot}-${i}`,
53
93
  slot: slot.slot,
54
94
  coworkRole: slot.role,
55
- roleRef: slot.role_ref,
56
- overrides: slot.overrides,
95
+ agent: structuredClone(slot.agent),
57
96
  });
58
97
  }
59
98
  }
60
99
  return result;
61
100
  }
62
101
  function settingsFor(member, cfg) {
63
- let refRole;
64
- try {
65
- refRole = findRole(cfg, member.roleRef);
102
+ if (!('ref' in member.agent)) {
103
+ const role = member.agent.role;
104
+ if (cfg.files[0] && existsSync(cfg.files[0]) && statSync(cfg.files[0]).isFile())
105
+ loadConfig(cfg.files[0], { additionalAgent: {
106
+ id: `RoomPreflight${member.name.replace(/[^A-Za-z0-9_-]/g, '')}`.slice(0, 64),
107
+ definition: member.agent,
108
+ } });
109
+ return { definition: structuredClone(member.agent),
110
+ ...('inline' in role && typeof role.inline.persona === 'string'
111
+ ? { persona: role.inline.persona } : {}) };
66
112
  }
67
- catch { /* no ref role */ }
68
- const permissions = member.overrides?.permissions;
113
+ const refRole = findRole(cfg, member.agent.ref);
114
+ const definition = cfg.agentDefinitions?.[member.agent.ref];
115
+ if (!definition)
116
+ throw new Error(`Agent '${member.agent.ref}' has no canonical definition`);
69
117
  return {
70
- model: member.overrides?.model ?? refRole?.model,
71
- harness: member.overrides?.harness ?? refRole?.harness,
72
- cwd: member.overrides?.cwd ?? refRole?.cwd,
73
- persona: member.overrides?.persona ?? refRole?.persona,
74
- approval: permissions?.approval,
75
- filesystem: permissions?.filesystem,
76
- unattended: permissions?.unattended,
118
+ definition: structuredClone(definition),
119
+ persona: refRole.persona,
77
120
  };
78
121
  }
79
122
  function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
@@ -104,13 +147,11 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
104
147
  const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
105
148
  const startup = role.roomMemberStartup;
106
149
  return role.identity === member.name
107
- && typeof role.mission === 'string'
108
- && sha256Text(role.mission) === taskSha
109
150
  && startup?.room_id === roomId
110
151
  && startup.room_identity_cid === roomIdentityCid
111
152
  && startup.identity_name === member.name
112
153
  && startup.role === member.coworkRole
113
- && startup.task === role.mission
154
+ && sha256Text(startup.task ?? '') === taskSha
114
155
  && (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
115
156
  && typeof startup.invite === 'string'
116
157
  && startup.invite.length > 0;
@@ -199,10 +240,13 @@ async function launchMember(input) {
199
240
  let effectiveActionId = actionId;
200
241
  const attempt = (seat.launch?.attempt ?? 0) + 1;
201
242
  const taskSha = sha256Text(startup.task);
243
+ const effectiveAgentDefinition = structuredClone(settings.definition);
244
+ const { projection: agentDefinition, fingerprint: agentFingerprint } = launchDefinition(effectiveAgentDefinition);
202
245
  const proxyCaller = process.env[FLEET_PROXY_STATE_DIR_ENV]
203
246
  ? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
204
247
  updateMemberStartup(provision.roomId, member.name, { launch: {
205
248
  state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
249
+ agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
206
250
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
207
251
  updated_at: new Date().toISOString(),
208
252
  } });
@@ -211,13 +255,7 @@ async function launchMember(input) {
211
255
  name: member.name,
212
256
  temp: true,
213
257
  identity: member.name,
214
- mission: startup.task,
215
- model: settings.model,
216
- harness: settings.harness,
217
- cwd: settings.cwd,
218
- approval: settings.approval,
219
- filesystem: settings.filesystem,
220
- unattended: settings.unattended,
258
+ agentDefinition: settings.definition,
221
259
  surface: 'agent',
222
260
  creationActionId: actionId,
223
261
  roomMemberStartup: startup,
@@ -227,7 +265,8 @@ async function launchMember(input) {
227
265
  if (launched.creationActionId !== actionId) {
228
266
  updateMemberStartup(provision.roomId, member.name, { launch: {
229
267
  state: 'intent', attempt, action_id: launched.creationActionId,
230
- mission_sha256: taskSha, updated_at: new Date().toISOString(),
268
+ mission_sha256: taskSha, agent_definition: agentDefinition,
269
+ agent_fingerprint: agentFingerprint, updated_at: new Date().toISOString(),
231
270
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
232
271
  } });
233
272
  }
@@ -237,17 +276,26 @@ async function launchMember(input) {
237
276
  }
238
277
  updateMemberStartup(provision.roomId, member.name, { launch: {
239
278
  state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
279
+ agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
240
280
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
241
281
  launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
242
282
  } });
283
+ recordFleetAuditPresentation({ kind: 'agent_started', id: member.name, name: member.name,
284
+ lifetime: 'temporary', brain: selectionSummary(settings.definition.brain),
285
+ role: selectionSummary(settings.definition.role), harness: 'resolved', session: 'acp',
286
+ permissions: permissionSummary(settings.definition), parent: provision.roomId,
287
+ actionId: launched.creationActionId, inherited: [] });
243
288
  }
244
289
  catch (error) {
245
290
  updateMemberStartup(provision.roomId, member.name, { launch: {
246
291
  state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
292
+ agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
247
293
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
248
294
  updated_at: new Date().toISOString(),
249
295
  error: error instanceof Error ? error.message : String(error),
250
296
  } });
297
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Agent', id: member.name,
298
+ state: 'failed', category: 'readiness_failed', eventId: effectiveActionId });
251
299
  throw error;
252
300
  }
253
301
  }
@@ -289,6 +337,8 @@ export async function provisionMembers(input) {
289
337
  const { cfg, cowork, roomId, taskId, template } = input;
290
338
  const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
291
339
  const members = expandMembers(template, prefix);
340
+ // Resolve every Agent before persisting launch intent or touching Cowork membership.
341
+ const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
292
342
  const existing = getRoomRecord(roomId);
293
343
  if (!existing?.room_identity_cid)
294
344
  throw new Error(`room ${roomId} has no pinned room identity CID`);
@@ -299,15 +349,27 @@ export async function provisionMembers(input) {
299
349
  && members.every(member => persistedNames.has(member.name));
300
350
  if (!resuming) {
301
351
  advanceSaga(roomId, 'create_members', 3);
302
- updateMemberSeats(roomId, members.map(member => ({
303
- role_name: member.name,
304
- slot: member.slot,
305
- cowork_role: member.coworkRole,
306
- seat_state: 'pending',
307
- launch: { state: 'pending', attempt: 0, updated_at: new Date().toISOString() },
308
- })));
352
+ updateMemberSeats(roomId, members.map(member => {
353
+ const evidence = launchDefinition(settings.get(member.name).definition);
354
+ return ({
355
+ role_name: member.name,
356
+ slot: member.slot,
357
+ cowork_role: member.coworkRole,
358
+ seat_state: 'pending',
359
+ launch: { state: 'pending', attempt: 0,
360
+ agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
361
+ updated_at: new Date().toISOString() },
362
+ });
363
+ }));
364
+ }
365
+ else {
366
+ for (const member of members) {
367
+ const seat = existing.member_seats.find(candidate => candidate.role_name === member.name);
368
+ const evidence = launchDefinition(settings.get(member.name).definition);
369
+ if (!seat.launch?.agent_fingerprint || seat.launch.agent_fingerprint !== evidence.fingerprint)
370
+ throw new Error(`Agent definition drift for ${member.name}; durable launch intent does not match current configuration`);
371
+ }
309
372
  }
310
- const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
311
373
  const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
312
374
  const policy = {
313
375
  timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
@@ -1,5 +1,4 @@
1
1
  import type { TemplateDefinition, TemplateSnapshot } from './types.js';
2
- export declare const BUILTIN_TEMPLATES: readonly TemplateDefinition[];
3
2
  export declare function hashTemplate(t: TemplateDefinition): string;
4
3
  export declare function snapshotTemplate(t: TemplateDefinition): TemplateSnapshot;
5
4
  export declare function resolveTemplate(name: string, customTemplates: Record<string, TemplateDefinition>): TemplateDefinition | undefined;
@@ -1,54 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- const TEAM = {
3
- name: 'team',
4
- version: 1,
5
- builtin: true,
6
- description: 'Phased task pipeline: Architect specifies, Developer implements, Tester verifies',
7
- room: { quiet_membership: false, anonymous: false },
8
- contract: [
9
- 'Architect produces a spec and posts it to the room. Work pauses for owner approval.',
10
- 'Developer implements per approved spec.',
11
- 'Tester verifies spec conformance and test coverage.',
12
- 'Completion requires tester sign-off.',
13
- ].join('\n'),
14
- members: [
15
- { slot: 'architect', role: 'Architect', count: 1, role_ref: 'Architect' },
16
- { slot: 'developer', role: 'Developer', count: 1, role_ref: 'Developer' },
17
- { slot: 'tester', role: 'Tester', count: 1, role_ref: 'Tester' },
18
- ],
19
- };
20
- const PAIR = {
21
- name: 'pair',
22
- version: 1,
23
- builtin: true,
24
- description: 'Deliberation pair: Secretary writes code, Critic reviews — every decision deliberated together',
25
- room: { quiet_membership: false, anonymous: false },
26
- contract: [
27
- 'Secretary and Critic deliberate every decision together before acting.',
28
- 'Secretary writes code; Critic reviews and challenges.',
29
- 'No material change lands without both agreeing.',
30
- 'Completion requires joint sign-off.',
31
- ].join('\n'),
32
- members: [
33
- { slot: 'secretary', role: 'Secretary', count: 1, role_ref: 'Secretary' },
34
- { slot: 'critic', role: 'Critic', count: 1, role_ref: 'Critic' },
35
- ],
36
- };
37
- const SINGLE = {
38
- name: 'single',
39
- version: 1,
40
- builtin: true,
41
- description: 'Solo agent: one general-purpose agent works the task with owner oversight',
42
- room: { quiet_membership: false, anonymous: false },
43
- contract: [
44
- 'Agent works the task autonomously, consulting the owner when blocked.',
45
- 'Owner reviews and approves completion.',
46
- ].join('\n'),
47
- members: [
48
- { slot: 'agent', role: 'Agent', count: 1, role_ref: 'Agent' },
49
- ],
50
- };
51
- export const BUILTIN_TEMPLATES = [TEAM, PAIR, SINGLE];
52
2
  export function hashTemplate(t) {
53
3
  const canonical = JSON.stringify({
54
4
  name: t.name, version: t.version, description: t.description,
@@ -57,38 +7,21 @@ export function hashTemplate(t) {
57
7
  return createHash('sha256').update(canonical).digest('hex');
58
8
  }
59
9
  export function snapshotTemplate(t) {
60
- return { ...t, content_hash: hashTemplate(t) };
10
+ const { sourceFile: _, ...semantic } = t;
11
+ return { ...semantic, content_hash: hashTemplate(t) };
61
12
  }
62
13
  export function resolveTemplate(name, customTemplates) {
63
14
  const atIdx = name.lastIndexOf('@');
64
15
  const baseName = atIdx > 0 ? name.slice(0, atIdx) : name;
65
16
  const versionStr = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
66
17
  const version = versionStr ? parseInt(versionStr, 10) : undefined;
67
- const custom = customTemplates[baseName];
68
- if (custom) {
69
- if (version !== undefined && custom.version !== version)
70
- return undefined;
71
- return custom;
72
- }
73
- const builtin = BUILTIN_TEMPLATES.find(b => b.name === baseName);
74
- if (builtin) {
75
- if (version !== undefined && builtin.version !== version)
76
- return undefined;
77
- return builtin;
78
- }
79
- return undefined;
18
+ const template = customTemplates[baseName];
19
+ if (!template || (version !== undefined && template.version !== version))
20
+ return undefined;
21
+ return template;
80
22
  }
81
23
  export function listTemplates(customTemplates) {
82
- const result = [];
83
- const overridden = new Set();
84
- for (const [name, t] of Object.entries(customTemplates)) {
85
- result.push({ ...t, name });
86
- if (BUILTIN_TEMPLATES.some(b => b.name === name))
87
- overridden.add(name);
88
- }
89
- for (const b of BUILTIN_TEMPLATES) {
90
- if (!overridden.has(b.name))
91
- result.push(b);
92
- }
93
- return result.sort((a, b) => a.name.localeCompare(b.name));
24
+ return Object.entries(customTemplates)
25
+ .map(([name, template]) => ({ ...template, name }))
26
+ .sort((a, b) => a.name.localeCompare(b.name));
94
27
  }
@@ -100,6 +100,9 @@ export interface RoomMemberLaunchState {
100
100
  /** Expected authenticated proxy caller while adopting a post-spawn crash. */
101
101
  caller_role?: string;
102
102
  mission_sha256?: string;
103
+ /** Redacted, deterministic effective Agent launch definition retained for retry inspection. */
104
+ agent_definition?: Record<string, unknown>;
105
+ agent_fingerprint?: string;
103
106
  launch_id?: string;
104
107
  updated_at: string;
105
108
  error?: string;
@@ -189,19 +192,10 @@ export interface TemplateMemberSlot {
189
192
  slot: string;
190
193
  role: string;
191
194
  count: number;
192
- role_ref: string;
193
- overrides?: TemplateRoleOverrides;
194
- }
195
- export interface TemplateRoleOverrides {
196
- harness?: string;
197
- model?: string;
198
- model_chain?: string[];
199
- permissions?: Record<string, unknown>;
200
- isolation?: Record<string, unknown>;
201
- cwd?: string;
202
- env?: Record<string, string>;
203
- persona?: string;
204
- mission?: string;
195
+ /** Canonical Agent definition, or stable reference to a declared Agent. */
196
+ agent: import('../config.js').AgentDefinition | {
197
+ ref: string;
198
+ };
205
199
  }
206
200
  export interface TemplateRoomConfig {
207
201
  quiet_membership?: boolean;
@@ -211,7 +205,8 @@ export interface TemplateDefinition {
211
205
  name: string;
212
206
  version: number;
213
207
  description: string;
214
- builtin?: boolean;
208
+ /** Authoring provenance only; excluded from snapshots and semantic hashes. */
209
+ sourceFile?: string;
215
210
  room?: TemplateRoomConfig;
216
211
  contract?: string;
217
212
  members: TemplateMemberSlot[];
@@ -252,5 +247,4 @@ export declare const ROOMS_COWORK_KEYS: readonly ["config"];
252
247
  export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
253
248
  export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
254
249
  export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
255
- export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "role_ref", "overrides"];
256
- export declare const TEMPLATE_OVERRIDE_KEYS: readonly ["harness", "model", "model_chain", "permissions", "isolation", "cwd", "env", "persona", "mission"];
250
+ export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "agent"];
@@ -14,7 +14,4 @@ export const ROOMS_COWORK_KEYS = ['config'];
14
14
  export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
15
15
  export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
16
16
  export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
17
- export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'role_ref', 'overrides'];
18
- export const TEMPLATE_OVERRIDE_KEYS = [
19
- 'harness', 'model', 'model_chain', 'permissions', 'isolation', 'cwd', 'env', 'persona', 'mission',
20
- ];
17
+ export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'agent'];
package/dist/runner.d.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  import { type ResolvedRole } from './config.js';
2
- import type { Launch } from './harness/types.js';
3
- import { Tmux } from './tmux.js';
4
2
  import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
3
+ import { type DaemonGenerationProbe } from './daemon-recovery.js';
5
4
  import { type Exec } from './exec.js';
6
- import { type AcpSessionOptions } from './session/acp.js';
7
5
  import { RoleControlServer } from './session/control.js';
8
- import type { ExitRecord, SessionHandle, TurnResult } from './session/types.js';
6
+ import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
7
+ import type { AgentSessionAdapter, AgentSessionStartOptions } from './harness/agent-session.js';
9
8
  import { type OwnerChannelHandle, type OwnerChannelOptions } from './owner-channel/channel.js';
10
9
  import { type OwnerBinderLease } from './owner-channel/binder.js';
11
10
  export interface RunnerDeps {
12
- tmux: Tmux;
13
11
  exec: Exec;
14
12
  cpuDelegated(): boolean;
15
13
  isAlive(pid: number): boolean;
@@ -18,14 +16,16 @@ export interface RunnerDeps {
18
16
  log(line: string): void;
19
17
  /** HTTP transport for the monitor's daemon long-poll (injectable for tests). */
20
18
  fetch: FetchLike;
19
+ probeGeneration(env: NodeJS.ProcessEnv): Promise<DaemonGenerationProbe>;
21
20
  /** Construct the supervisor mail monitor (injectable so tests stub it out). */
22
21
  createMonitor(opts: MonitorOpts): MonitorHandle;
23
22
  /** Construct trusted owner ingress (injectable for lifecycle tests). */
24
23
  createOwnerChannel(opts: OwnerChannelOptions): OwnerChannelHandle;
25
24
  /** Start the ACP transport (injectable for deterministic runner lifecycle tests). */
26
- startAcpSession(opts: AcpSessionOptions): Promise<SessionHandle>;
25
+ /** Neutral construction seam for deterministic runner lifecycle tests. */
26
+ startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
27
27
  /** Construct the authenticated role control route (injectable where sockets are unavailable). */
28
- createControlServer(stateDir: string, session: SessionHandle, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
28
+ createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
29
29
  /** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
30
30
  acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
31
31
  /** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
@@ -33,6 +33,11 @@ export interface RunnerDeps {
33
33
  /** Lets a test (or a shutdown path) end the supervised restart loop. */
34
34
  shouldStop?(): boolean;
35
35
  }
36
+ export declare const SUPERVISOR_RECYCLE_REQUIRED = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
37
+ export declare class SupervisorRecycleRequiredError extends Error {
38
+ readonly code = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
39
+ constructor();
40
+ }
36
41
  /** Environment injected only into the managed harness process. */
37
42
  export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
38
43
  /**
@@ -51,14 +56,6 @@ export declare function harnessChildEnv(role: ResolvedRole, launchEnv: Record<st
51
56
  * responsible for.
52
57
  */
53
58
  export declare function recordMonitorOwner(dir: string, owner: 'fleet' | 'native'): boolean;
54
- /**
55
- * Compose the tmux pane shell command: env prefix + argv + exit-status capture.
56
- * `paneArgv` defaults to `launch.argv`; when isolation is active the caller passes
57
- * the sandbox-wrapped argv (e.g. `bwrap … -- claude …`). The `env` prefix and the
58
- * `echo $? > exitfile` capture stay host-side, outside the sandbox, so the runner
59
- * still sees the real exit code.
60
- */
61
- export declare function buildPaneCommand(launch: Launch, roleEnv: Record<string, string> | undefined, exitStatusPath: string, paneArgv?: string[]): string;
62
59
  /**
63
60
  * Read the pane's `.exit-status`. Three shapes are accepted: the structured
64
61
  * record written above, a bare number left by a pre-upgrade pane (so an
@@ -186,4 +183,4 @@ export declare function runSupervised(name: string, opts?: {
186
183
  allowResumeRotation?: boolean;
187
184
  }, d: Partial<RunnerDeps>) => Promise<AttemptResult>): Promise<RestartLedger>;
188
185
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
189
- export declare function runTemp(name: string, deps?: Partial<RunnerDeps>): Promise<void>;
186
+ export declare function runTemp(name: string, deps?: Partial<RunnerDeps>, attempt?: typeof runOnce): Promise<void>;