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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +231 -46
  2. package/dist/application/capabilities.js +18 -6
  3. package/dist/application/fleet-query-service.js +7 -4
  4. package/dist/application/role-command-service.js +9 -0
  5. package/dist/application/role-creation-service.d.ts +4 -4
  6. package/dist/application/role-creation-service.js +9 -2
  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 +579 -173
  13. package/dist/briefing.js +85 -28
  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 +130 -55
  18. package/dist/config.d.ts +38 -3
  19. package/dist/config.js +193 -28
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +7 -4
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +193 -55
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +40 -4
  26. package/dist/fleet-command-audit.d.ts +59 -9
  27. package/dist/fleet-command-audit.js +426 -63
  28. package/dist/fleet-proxy.d.ts +5 -2
  29. package/dist/harness/claude-code-session.js +2 -1
  30. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  31. package/dist/harness/codex-app-server-proxy.js +348 -14
  32. package/dist/harness/codex-session.d.ts +9 -2
  33. package/dist/harness/codex-session.js +18 -2
  34. package/dist/harness/codex.d.ts +5 -1
  35. package/dist/harness/codex.js +104 -10
  36. package/dist/harness/types.d.ts +1 -1
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +3 -2
  39. package/dist/init-guidance.d.ts +1 -1
  40. package/dist/init-guidance.js +1 -1
  41. package/dist/init-wizard.d.ts +98 -0
  42. package/dist/init-wizard.js +631 -0
  43. package/dist/lifecycle-summary.d.ts +134 -0
  44. package/dist/lifecycle-summary.js +179 -0
  45. package/dist/loops/config.d.ts +10 -1
  46. package/dist/loops/config.js +38 -5
  47. package/dist/model-recovery.d.ts +1 -1
  48. package/dist/ops.js +2 -17
  49. package/dist/owner-channel/channel.d.ts +21 -5
  50. package/dist/owner-channel/channel.js +312 -138
  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/owner-channel/state.d.ts +2 -0
  58. package/dist/owner-channel/state.js +8 -2
  59. package/dist/permissions.js +1 -1
  60. package/dist/preset-migration.d.ts +42 -0
  61. package/dist/preset-migration.js +450 -0
  62. package/dist/reports/artifact.d.ts +10 -0
  63. package/dist/reports/artifact.js +32 -0
  64. package/dist/reports/index.d.ts +5 -0
  65. package/dist/reports/index.js +5 -0
  66. package/dist/reports/render.d.ts +6 -0
  67. package/dist/reports/render.js +240 -0
  68. package/dist/reports/service.d.ts +42 -0
  69. package/dist/reports/service.js +57 -0
  70. package/dist/reports/tasks.d.ts +52 -0
  71. package/dist/reports/tasks.js +177 -0
  72. package/dist/reports/types.d.ts +315 -0
  73. package/dist/reports/types.js +6 -0
  74. package/dist/resolved-plan.js +13 -1
  75. package/dist/rooms-tasks/cli.d.ts +2 -0
  76. package/dist/rooms-tasks/cli.js +434 -279
  77. package/dist/rooms-tasks/close.d.ts +9 -1
  78. package/dist/rooms-tasks/close.js +23 -7
  79. package/dist/rooms-tasks/config.js +16 -25
  80. package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
  81. package/dist/rooms-tasks/cowork-adapter.js +13 -0
  82. package/dist/rooms-tasks/deletion.d.ts +51 -0
  83. package/dist/rooms-tasks/deletion.js +217 -0
  84. package/dist/rooms-tasks/external-worker.d.ts +8 -0
  85. package/dist/rooms-tasks/external-worker.js +38 -0
  86. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  87. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  88. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  89. package/dist/rooms-tasks/member-overrides.js +204 -0
  90. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  91. package/dist/rooms-tasks/member-startup.js +9 -2
  92. package/dist/rooms-tasks/provision.js +146 -56
  93. package/dist/rooms-tasks/room-state.d.ts +1 -0
  94. package/dist/rooms-tasks/room-state.js +12 -1
  95. package/dist/rooms-tasks/task-room-name.d.ts +12 -0
  96. package/dist/rooms-tasks/task-room-name.js +61 -0
  97. package/dist/rooms-tasks/task-state.d.ts +115 -3
  98. package/dist/rooms-tasks/task-state.js +361 -11
  99. package/dist/rooms-tasks/templates.d.ts +4 -1
  100. package/dist/rooms-tasks/templates.js +34 -3
  101. package/dist/rooms-tasks/terminal.d.ts +3 -0
  102. package/dist/rooms-tasks/terminal.js +10 -4
  103. package/dist/rooms-tasks/types.d.ts +92 -7
  104. package/dist/rooms-tasks/types.js +14 -1
  105. package/dist/runner.d.ts +3 -0
  106. package/dist/runner.js +92 -33
  107. package/dist/session/acp.d.ts +4 -1
  108. package/dist/session/acp.js +29 -13
  109. package/dist/session/arbiter.d.ts +2 -1
  110. package/dist/session/arbiter.js +2 -0
  111. package/dist/session/codex-app-server-transport.d.ts +53 -0
  112. package/dist/session/codex-app-server-transport.js +184 -0
  113. package/dist/session/codex-app-server.d.ts +122 -0
  114. package/dist/session/codex-app-server.js +1031 -0
  115. package/dist/session/control.d.ts +2 -2
  116. package/dist/session/control.js +18 -2
  117. package/dist/session/conversation-types.d.ts +4 -4
  118. package/dist/session/types.d.ts +24 -6
  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
@@ -1,14 +1,22 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { join } from 'node:path';
3
4
  import { parse } from 'yaml';
4
5
  import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
5
6
  import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
7
+ import { storedRoomLaunchPolicy } from './types.js';
6
8
  import { spawnTemp } from '../spawn.js';
7
- import { findRole, loadConfig } from '../config.js';
9
+ import { effectivePermissionMode } from '../permissions.js';
10
+ import { selectionOrigin, summarizeResolvedLaunch, } from '../lifecycle-summary.js';
11
+ import { canonicalJson } from '../canonical-json.js';
12
+ import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js';
8
13
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
9
14
  import { controlRequest } from '../session/control.js';
10
15
  import { SessionControlError } from '../session/types.js';
11
16
  import { closeManagedRoom } from './close.js';
17
+ import { withFileLock } from '../atomic-file.js';
18
+ import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
19
+ import { TaskStateError, taskDeletionState } from './task-state.js';
12
20
  import { buildRoomMemberTask, sha256Text } from './member-startup.js';
13
21
  import { agentDir } from '../paths.js';
14
22
  import { readProvenance } from '../creation.js';
@@ -21,23 +29,9 @@ export function getBinPath() {
21
29
  return process.argv[1];
22
30
  }
23
31
  }
24
- function canonicalJson(value) {
25
- if (Array.isArray(value))
26
- return `[${value.map(canonicalJson).join(',')}]`;
27
- if (value && typeof value === 'object')
28
- return `{${Object.entries(value)
29
- .sort(([a], [b]) => a.localeCompare(b))
30
- .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
31
- return JSON.stringify(value);
32
- }
33
32
  function launchDefinition(definition) {
34
- const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : { kind: 'inline' };
35
33
  return {
36
- projection: {
37
- brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
38
- operationalFields: Object.keys(definition)
39
- .filter(key => key !== 'brain' && key !== 'role').sort(),
40
- },
34
+ projection: redactLaunchDefinition(definition),
41
35
  fingerprint: createHash('sha256').update(canonicalJson(definition)).digest('hex'),
42
36
  };
43
37
  }
@@ -61,8 +55,27 @@ async function spawnRoomMember(options, binPath) {
61
55
  statePath: result.statePath,
62
56
  creationActionId: result.creationActionId,
63
57
  callerRole: result.caller,
58
+ ...(result.configuration ? { configuration: result.configuration } : {}),
64
59
  };
65
60
  }
61
+ /**
62
+ * Launch-boundary capture: the exact ResolvedRole the spawn persisted,
63
+ * whitelisted into the operator-facing presentation. Capture is required for
64
+ * every launched room member — launchMatches has already proved role.yaml
65
+ * readable, and effectivePermissionMode is total over registered harnesses —
66
+ * so a failure here is a capture defect and must surface, not degrade to the
67
+ * legacy rendering.
68
+ */
69
+ function presentationFromStatePath(statePath, settings, coworkRole) {
70
+ const resolved = parse(readFileSync(join(statePath, 'role.yaml'), 'utf8'));
71
+ return summarizeResolvedLaunch(resolved, {
72
+ role: selectionOrigin(settings.definition.role),
73
+ brain: selectionOrigin(settings.definition.brain),
74
+ template: settings.template,
75
+ permissionMode: effectivePermissionMode(resolved),
76
+ missionFallback: coworkRole,
77
+ });
78
+ }
66
79
  function shortId(id) { return id.slice(0, 8); }
67
80
  function expandMembers(template, prefix) {
68
81
  const result = [];
@@ -72,39 +85,41 @@ function expandMembers(template, prefix) {
72
85
  name: `${prefix}-${slot.slot}-${i}`,
73
86
  slot: slot.slot,
74
87
  coworkRole: slot.role,
75
- agent: structuredClone(slot.agent),
88
+ agentTemplate: slot.agent_template,
89
+ launchDefinitionId: slot.launch_definition_id ?? slot.agent_template,
90
+ agentTemplateHash: slot.agent_template_hash,
91
+ loopSource: slot.loop_source ?? 'omitted',
76
92
  });
77
93
  }
78
94
  }
79
95
  return result;
80
96
  }
81
- function settingsFor(member, cfg) {
82
- if (!('ref' in member.agent)) {
83
- const role = member.agent.role;
84
- if (cfg.files[0] && existsSync(cfg.files[0]) && statSync(cfg.files[0]).isFile())
85
- loadConfig(cfg.files[0], { additionalAgent: {
86
- id: `RoomPreflight${member.name.replace(/[^A-Za-z0-9_-]/g, '')}`.slice(0, 64),
87
- definition: member.agent,
88
- } });
89
- return { definition: structuredClone(member.agent),
90
- ...('inline' in role && typeof role.inline.persona === 'string'
91
- ? { persona: role.inline.persona } : {}) };
92
- }
93
- const refRole = findRole(cfg, member.agent.ref);
94
- const definition = cfg.agentDefinitions?.[member.agent.ref];
97
+ function settingsFor(member, cfg, sealed) {
98
+ const definition = sealed
99
+ ? sealed[member.launchDefinitionId]
100
+ : cfg.agentTemplates?.[member.agentTemplate];
95
101
  if (!definition)
96
- throw new Error(`Agent '${member.agent.ref}' has no canonical definition`);
102
+ throw new Error(sealed
103
+ ? `Sealed Agent definition '${member.launchDefinitionId}' not found`
104
+ : `Agent Template '${member.agentTemplate}' not found`);
105
+ const role = definition.role;
97
106
  return {
98
107
  definition: structuredClone(definition),
99
- persona: refRole.persona,
108
+ ...('inline' in role && typeof role.inline.persona === 'string'
109
+ ? { persona: role.inline.persona } : {}),
110
+ template: member.agentTemplate,
111
+ templateHash: member.agentTemplateHash
112
+ ?? createHash('sha256').update(canonicalJson(definition)).digest('hex'),
113
+ loopSource: member.loopSource,
100
114
  };
101
115
  }
102
- function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
116
+ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
103
117
  return buildRoomMemberTask({
104
118
  taskId: input.taskId,
105
119
  roomId: input.roomId,
106
120
  roomIdentityCid,
107
121
  ownerSeatCid,
122
+ anonymous,
108
123
  goal: input.goal,
109
124
  brief: input.brief,
110
125
  contract: input.template.contract,
@@ -119,7 +134,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
119
134
  })),
120
135
  });
121
136
  }
122
- function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId) {
137
+ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId, anonymous = false) {
123
138
  const provenance = readProvenance(dir);
124
139
  if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
125
140
  return false;
@@ -131,6 +146,7 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
131
146
  && startup.room_identity_cid === roomIdentityCid
132
147
  && startup.identity_name === member.name
133
148
  && startup.role === member.coworkRole
149
+ && (startup.anonymous ?? false) === anonymous
134
150
  && sha256Text(startup.task ?? '') === taskSha
135
151
  && (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
136
152
  && typeof startup.invite === 'string'
@@ -141,21 +157,22 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
141
157
  }
142
158
  }
143
159
  async function retainRunningLaunch(input) {
144
- const { provision, member, task, roomIdentityCid } = input;
160
+ const { provision, member, settings, task, roomIdentityCid } = input;
145
161
  let seat = getRoomRecord(provision.roomId).member_seats
146
162
  .find(candidate => candidate.role_name === member.name);
147
163
  const dir = agentDir(member.name, true);
148
164
  const taskSha = sha256Text(task);
165
+ const anonymous = storedRoomLaunchPolicy(getRoomRecord(provision.roomId)?.room_policy).anonymous;
149
166
  if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched'
150
167
  || seat.launch?.state === 'failed') && existsSync(dir)) {
151
- if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
168
+ if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
152
169
  const provenance = readProvenance(dir);
153
170
  const adoptable = (seat.launch.state === 'intent' || seat.launch.state === 'failed')
154
171
  && Boolean(seat.launch.caller_role)
155
172
  && provenance?.surface === 'agent'
156
173
  && provenance.callerRole === seat.launch.caller_role
157
174
  && typeof provenance.creationActionId === 'string'
158
- && launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id);
175
+ && launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous);
159
176
  if (!adoptable)
160
177
  throw new Error(`existing launch for ${member.name} does not match its durable intent`);
161
178
  updateMemberStartup(provision.roomId, member.name, { launch: {
@@ -173,9 +190,14 @@ async function retainRunningLaunch(input) {
173
190
  throw new Error(`existing launch for ${member.name} has unknown liveness`);
174
191
  const retainedLaunch = seat.launch;
175
192
  if (live === 'running') {
193
+ // A crash between spawn and the seat update (or a pre-upgrade launch)
194
+ // can retain a running member without a captured presentation; backfill
195
+ // it from the same persisted resolved role the match was proved against.
196
+ const presentation = retainedLaunch.presentation
197
+ ?? presentationFromStatePath(dir, settings, member.coworkRole);
176
198
  updateMemberStartup(provision.roomId, member.name, { launch: {
177
199
  ...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
178
- updated_at: new Date().toISOString(),
200
+ presentation, updated_at: new Date().toISOString(),
179
201
  } });
180
202
  return true;
181
203
  }
@@ -190,7 +212,7 @@ async function retainRunningLaunch(input) {
190
212
  if (!seat.launch.launch_id || !seat.launch.action_id)
191
213
  throw new Error(`missing durable launch identity for disappeared ${member.name}`);
192
214
  const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
193
- if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
215
+ if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
194
216
  throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
195
217
  }
196
218
  updateMemberStartup(provision.roomId, member.name, { launch: {
@@ -202,7 +224,7 @@ async function retainRunningLaunch(input) {
202
224
  if (!seat.launch.action_id)
203
225
  throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
204
226
  const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
205
- if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
227
+ if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
206
228
  throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
207
229
  }
208
230
  updateMemberStartup(provision.roomId, member.name, { launch: {
@@ -212,7 +234,25 @@ async function retainRunningLaunch(input) {
212
234
  }
213
235
  return false;
214
236
  }
237
+ /**
238
+ * Member-launch publication window: for a task-bound room, the durable launch
239
+ * intent, spawn, and launch record publish under the common task-operation
240
+ * lock so an accepted deletion linearizes strictly before (bounded abort — no
241
+ * spawn happens) or strictly after (the seat's launch record is visible to
242
+ * deletion retirement). The lock is held per member only, never across
243
+ * seat-wait loops.
244
+ */
215
245
  async function launchMember(input) {
246
+ const taskId = input.provision.taskId;
247
+ if (!taskId)
248
+ return launchMemberUnlocked(input);
249
+ return withFileLock(taskOperationLockPath(taskId), () => {
250
+ if (taskDeletionState(taskId) !== 'none')
251
+ throw new Error(`task ${taskId} is pending deletion; aborting member launch for ${input.member.name}`);
252
+ return launchMemberUnlocked(input);
253
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
254
+ }
255
+ async function launchMemberUnlocked(input) {
216
256
  const { provision, member, settings, startup } = input;
217
257
  const seat = getRoomRecord(provision.roomId).member_seats
218
258
  .find(candidate => candidate.role_name === member.name);
@@ -227,6 +267,7 @@ async function launchMember(input) {
227
267
  updateMemberStartup(provision.roomId, member.name, { launch: {
228
268
  state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
229
269
  agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
270
+ agent_template: settings.template, agent_template_hash: settings.templateHash,
230
271
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
231
272
  updated_at: new Date().toISOString(),
232
273
  } });
@@ -236,6 +277,9 @@ async function launchMember(input) {
236
277
  temp: true,
237
278
  identity: member.name,
238
279
  agentDefinition: settings.definition,
280
+ ...(settings.loopSource === 'cli' && settings.definition.loops === undefined
281
+ ? { noLoops: true } : {}),
282
+ loopSource: settings.loopSource,
239
283
  surface: 'agent',
240
284
  creationActionId: actionId,
241
285
  roomMemberStartup: startup,
@@ -246,17 +290,25 @@ async function launchMember(input) {
246
290
  updateMemberStartup(provision.roomId, member.name, { launch: {
247
291
  state: 'intent', attempt, action_id: launched.creationActionId,
248
292
  mission_sha256: taskSha, agent_definition: agentDefinition,
249
- agent_fingerprint: agentFingerprint, updated_at: new Date().toISOString(),
293
+ agent_fingerprint: agentFingerprint,
294
+ agent_template: settings.template, agent_template_hash: settings.templateHash,
295
+ updated_at: new Date().toISOString(),
250
296
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
251
297
  } });
252
298
  }
253
299
  const supervisor = readTempSupervisor(launchedDir);
254
- if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id)) {
300
+ if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id, startup.anonymous ?? false)) {
255
301
  throw new Error(`new launch for ${member.name} did not persist matching provenance`);
256
302
  }
303
+ const presentation = launched.configuration
304
+ ? { ...launched.configuration, template: settings.template,
305
+ ...(launched.configuration.mission ? {} : { mission: member.coworkRole }) }
306
+ : presentationFromStatePath(launchedDir, settings, member.coworkRole);
257
307
  updateMemberStartup(provision.roomId, member.name, { launch: {
258
308
  state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
259
309
  agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
310
+ agent_template: settings.template, agent_template_hash: settings.templateHash,
311
+ presentation,
260
312
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
261
313
  launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
262
314
  } });
@@ -265,6 +317,7 @@ async function launchMember(input) {
265
317
  updateMemberStartup(provision.roomId, member.name, { launch: {
266
318
  state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
267
319
  agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
320
+ agent_template: settings.template, agent_template_hash: settings.templateHash,
268
321
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
269
322
  updated_at: new Date().toISOString(),
270
323
  error: error instanceof Error ? error.message : String(error),
@@ -286,9 +339,10 @@ function exactCoworkSeat(observed, member, expectedInviteId) {
286
339
  }
287
340
  return seat;
288
341
  }
289
- function reconcileMemberSeats(roomId, members, observed) {
342
+ function reconcileMemberSeats(roomId, members, observed, ownerSeatCid) {
290
343
  const current = getRoomRecord(roomId);
291
- let complete = true;
344
+ let complete = !ownerSeatCid || observed.some(seat => seat.identity_cid.toLowerCase() === ownerSeatCid.toLowerCase()
345
+ && seat.seat_state === 'active');
292
346
  const seats = current.member_seats.map(seat => {
293
347
  const member = members.find(candidate => candidate.name === seat.role_name);
294
348
  const found = exactCoworkSeat(observed, member, seat.invite_id);
@@ -306,24 +360,35 @@ function reconcileMemberSeats(roomId, members, observed) {
306
360
  updateMemberSeats(roomId, seats);
307
361
  return { complete, seats };
308
362
  }
363
+ function assertCoworkRoomPolicy(room, expectedAnonymous) {
364
+ if ((room.anonymous ?? false) !== expectedAnonymous)
365
+ throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
366
+ }
309
367
  export async function provisionMembers(input) {
310
368
  const { cfg, cowork, roomId, taskId, template } = input;
369
+ // Deletion-epoch pre-check; each member launch re-checks under the lock.
370
+ if (taskId && taskDeletionState(taskId) !== 'none')
371
+ throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
311
372
  const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
312
373
  const members = expandMembers(template, prefix);
313
374
  // Resolve every Agent before persisting launch intent or touching Cowork membership.
314
- const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
375
+ const sealed = template.launch_snapshot_hash
376
+ ? readLaunchSnapshot(template.launch_snapshot_hash) : undefined;
377
+ const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg, sealed)]));
315
378
  const existing = getRoomRecord(roomId);
316
379
  if (!existing?.room_identity_cid)
317
380
  throw new Error(`room ${roomId} has no pinned room identity CID`);
318
381
  const roomIdentityCid = existing.room_identity_cid;
319
382
  const ownerSeatCid = existing.owner_seat_cid ?? null;
383
+ const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
320
384
  const persistedNames = new Set(existing.member_seats.map(seat => seat.role_name));
321
385
  const resuming = members.length > 0
322
386
  && members.every(member => persistedNames.has(member.name));
323
387
  if (!resuming) {
324
388
  advanceSaga(roomId, 'create_members', 3);
325
389
  updateMemberSeats(roomId, members.map(member => {
326
- const evidence = launchDefinition(settings.get(member.name).definition);
390
+ const memberSettings = settings.get(member.name);
391
+ const evidence = launchDefinition(memberSettings.definition);
327
392
  return ({
328
393
  role_name: member.name,
329
394
  slot: member.slot,
@@ -331,6 +396,8 @@ export async function provisionMembers(input) {
331
396
  seat_state: 'pending',
332
397
  launch: { state: 'pending', attempt: 0,
333
398
  agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
399
+ agent_template: memberSettings.template,
400
+ agent_template_hash: memberSettings.templateHash,
334
401
  updated_at: new Date().toISOString() },
335
402
  });
336
403
  }));
@@ -343,7 +410,7 @@ export async function provisionMembers(input) {
343
410
  throw new Error(`Agent definition drift for ${member.name}; durable launch intent does not match current configuration`);
344
411
  }
345
412
  }
346
- const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
413
+ const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid, roomPolicy.anonymous)]));
347
414
  const policy = {
348
415
  timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
349
416
  initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
@@ -354,20 +421,23 @@ export async function provisionMembers(input) {
354
421
  advanceSaga(roomId, 'join_role_groups', 4);
355
422
  try {
356
423
  const initialRoom = await cowork.recoverRoom(roomId);
357
- reconcileMemberSeats(roomId, members, initialRoom.seats);
424
+ assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
425
+ reconcileMemberSeats(roomId, members, initialRoom.seats, existing.owner_seat_cid);
358
426
  for (const member of members) {
359
427
  const task = tasks.get(member.name);
360
428
  const currentSeat = getRoomRecord(roomId).member_seats
361
429
  .find(seat => seat.role_name === member.name);
362
430
  if (currentSeat.seat_state === 'active') {
363
431
  if (!await retainRunningLaunch({
364
- provision: input, member, task, roomIdentityCid,
432
+ provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
365
433
  })) {
366
434
  throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
367
435
  }
368
436
  continue;
369
437
  }
370
- if (await retainRunningLaunch({ provision: input, member, task, roomIdentityCid }))
438
+ if (await retainRunningLaunch({
439
+ provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
440
+ }))
371
441
  continue;
372
442
  const issued = await cowork.issueInvite(roomId, {
373
443
  mode: 'one_time', role: member.coworkRole, min_accepts: 1,
@@ -388,6 +458,7 @@ export async function provisionMembers(input) {
388
458
  role: member.coworkRole,
389
459
  task,
390
460
  owner_seat_cid: ownerSeatCid,
461
+ anonymous: roomPolicy.anonymous,
391
462
  },
392
463
  });
393
464
  }
@@ -399,9 +470,18 @@ export async function provisionMembers(input) {
399
470
  }
400
471
  catch (error) {
401
472
  const reason = error instanceof Error ? error.message : String(error);
402
- setSagaError(roomId, reason, 'Member invite or launch failed. Retry with `task recover`.', 'member_failed');
403
- if (taskId)
404
- blockTask(taskId, reason);
473
+ setSagaError(roomId, reason, 'Member invite or launch failed. Inspect role logs, then retry with `task start`.', 'member_failed');
474
+ if (taskId) {
475
+ // A deletion-pending (or already-terminal) task rejects the block
476
+ // overlay; the original launch failure must still propagate.
477
+ try {
478
+ blockTask(taskId, reason);
479
+ }
480
+ catch (blockError) {
481
+ if (!(blockError instanceof TaskStateError))
482
+ throw blockError;
483
+ }
484
+ }
405
485
  throw error;
406
486
  }
407
487
  advanceSaga(roomId, 'wait_seats', 5);
@@ -409,7 +489,8 @@ export async function provisionMembers(input) {
409
489
  let delay = policy.initialDelayMs;
410
490
  for (;;) {
411
491
  const remote = await cowork.recoverRoom(roomId);
412
- const reconciled = reconcileMemberSeats(roomId, members, remote.seats);
492
+ assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
493
+ const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
413
494
  if (reconciled.complete)
414
495
  break;
415
496
  if (policy.now() >= deadline) {
@@ -420,6 +501,15 @@ export async function provisionMembers(input) {
420
501
  await policy.sleep(delay);
421
502
  delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
422
503
  }
504
+ // Admission alone is insufficient: every expected member must still have
505
+ // the exact live launch that owns its authenticated Cowork seat.
506
+ for (const member of members) {
507
+ if (!await retainRunningLaunch({
508
+ provision: input, member, settings: settings.get(member.name),
509
+ task: tasks.get(member.name), roomIdentityCid,
510
+ }))
511
+ throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
512
+ }
423
513
  if (taskId) {
424
514
  const taskMembers = getRoomRecord(roomId).member_seats.map(seat => {
425
515
  if (!seat.identity_cid)
@@ -9,6 +9,7 @@ export interface CreateRoomInput {
9
9
  room_identity_cid?: string;
10
10
  task_id?: string;
11
11
  template_snapshot?: import('./types.js').TemplateSnapshot;
12
+ room_policy?: import('./types.js').RoomLaunchPolicy;
12
13
  }
13
14
  export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
14
15
  export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:f
2
2
  import { join } from 'node:path';
3
3
  import { replaceFileAtomically } from '../atomic-file.js';
4
4
  import { stateRoot } from '../paths.js';
5
+ import { storedRoomLaunchPolicy } from './types.js';
6
+ import { releaseLaunchSnapshot } from './launch-snapshot.js';
5
7
  export const roomsDir = () => join(stateRoot(), 'rooms');
6
8
  function roomPath(id) { return join(roomsDir(), `${id}.json`); }
7
9
  export class RoomStateError extends Error {
@@ -18,8 +20,13 @@ function writeRoom(record) {
18
20
  }
19
21
  export function createRoomRecord(input) {
20
22
  const existing = getRoomRecord(input.room_id);
21
- if (existing)
23
+ if (existing) {
24
+ const before = storedRoomLaunchPolicy(existing.room_policy);
25
+ const requested = storedRoomLaunchPolicy(input.room_policy);
26
+ if (JSON.stringify(before) !== JSON.stringify(requested))
27
+ throw new RoomStateError(`room ${input.room_id} launch policy mismatch`);
22
28
  return existing;
29
+ }
23
30
  const record = {
24
31
  room_id: input.room_id,
25
32
  room_name: input.room_name,
@@ -27,6 +34,7 @@ export function createRoomRecord(input) {
27
34
  room_identity_cid: input.room_identity_cid,
28
35
  task_id: input.task_id,
29
36
  template_snapshot: input.template_snapshot,
37
+ room_policy: input.room_policy,
30
38
  saga: { phase: 'persist_intent', step_index: 0 },
31
39
  member_seats: [],
32
40
  state: 'provisioning',
@@ -46,8 +54,11 @@ export function getRoomRecord(id) {
46
54
  /** Remove a terminal orchestration record from the live room inventory. */
47
55
  export function deleteRoomRecord(id) {
48
56
  const path = roomPath(id);
57
+ const hash = getRoomRecord(id)?.template_snapshot?.launch_snapshot_hash;
49
58
  if (existsSync(path))
50
59
  rmSync(path);
60
+ if (hash)
61
+ releaseLaunchSnapshot(hash);
51
62
  }
52
63
  export function listRoomRecords(filter) {
53
64
  const dir = roomsDir();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Cowork room identities are named `ours-cowork:<room name>` and both the
3
+ * room and identity contracts count NFC-normalized Unicode code points.
4
+ * Keeping the complete task ID inside that shared budget makes every derived
5
+ * name stable, directly correlated, and unique even when the title is empty,
6
+ * sanitized, or truncated.
7
+ */
8
+ export declare const COWORK_ROOM_IDENTITY_PREFIX = "ours-cowork:";
9
+ export declare const COWORK_IDENTITY_NAME_MAX_CODE_POINTS = 64;
10
+ export declare const TASK_ROOM_NAME_MAX_CODE_POINTS: number;
11
+ /** Canonical Cowork room name for a Fleet task; never mutates the task title. */
12
+ export declare function deriveTaskRoomName(title: string, taskId: string): string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Cowork room identities are named `ours-cowork:<room name>` and both the
3
+ * room and identity contracts count NFC-normalized Unicode code points.
4
+ * Keeping the complete task ID inside that shared budget makes every derived
5
+ * name stable, directly correlated, and unique even when the title is empty,
6
+ * sanitized, or truncated.
7
+ */
8
+ export const COWORK_ROOM_IDENTITY_PREFIX = 'ours-cowork:';
9
+ export const COWORK_IDENTITY_NAME_MAX_CODE_POINTS = 64;
10
+ export const TASK_ROOM_NAME_MAX_CODE_POINTS = COWORK_IDENTITY_NAME_MAX_CODE_POINTS
11
+ - Array.from(COWORK_ROOM_IDENTITY_PREFIX).length;
12
+ const TASK_ID_PATTERN = /^[0-9a-z]{9}[0-9a-f]{8}$/;
13
+ const FORBIDDEN = /[\\/\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
14
+ const DASH_SEPARATOR = /\p{Pd}/u;
15
+ const WHITESPACE = /\s/u;
16
+ const FALLBACK_TITLE = 'Task';
17
+ function sanitizeTitle(title) {
18
+ const normalized = title.normalize('NFC');
19
+ let result = '';
20
+ let pending;
21
+ for (const character of normalized) {
22
+ if (FORBIDDEN.test(character) || DASH_SEPARATOR.test(character)) {
23
+ pending = 'separator';
24
+ }
25
+ else if (WHITESPACE.test(character)) {
26
+ if (pending !== 'separator')
27
+ pending = 'space';
28
+ }
29
+ else {
30
+ if (result && pending)
31
+ result += pending === 'separator' ? ' - ' : ' ';
32
+ result += character;
33
+ pending = undefined;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+ /** Truncate to a code-point budget without cutting an extended grapheme. */
39
+ function graphemeBound(value, maxCodePoints) {
40
+ const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
41
+ let result = '';
42
+ let length = 0;
43
+ for (const { segment } of segmenter.segment(value)) {
44
+ const segmentLength = Array.from(segment).length;
45
+ if (length + segmentLength > maxCodePoints)
46
+ break;
47
+ result += segment;
48
+ length += segmentLength;
49
+ }
50
+ return result.trimEnd();
51
+ }
52
+ /** Canonical Cowork room name for a Fleet task; never mutates the task title. */
53
+ export function deriveTaskRoomName(title, taskId) {
54
+ if (!TASK_ID_PATTERN.test(taskId))
55
+ throw new Error(`invalid canonical task ID: ${taskId}`);
56
+ const suffix = ` [${taskId}]`;
57
+ const titleBudget = TASK_ROOM_NAME_MAX_CODE_POINTS - Array.from(suffix).length;
58
+ const readable = sanitizeTitle(title);
59
+ const bounded = graphemeBound(readable || FALLBACK_TITLE, titleBudget) || FALLBACK_TITLE;
60
+ return `${bounded}${suffix}`;
61
+ }