@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.20

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 (271) hide show
  1. package/README.md +152 -29
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/correspondence.d.ts +10 -0
  7. package/dist/agent-ours/correspondence.js +1 -0
  8. package/dist/agent-ours/file-rpc.d.ts +23 -0
  9. package/dist/agent-ours/file-rpc.js +123 -0
  10. package/dist/agent-ours/harness.d.ts +14 -0
  11. package/dist/agent-ours/harness.js +72 -0
  12. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  13. package/dist/agent-ours/mcp-endpoint.js +111 -0
  14. package/dist/agent-ours/runtime.d.ts +59 -0
  15. package/dist/agent-ours/runtime.js +283 -0
  16. package/dist/agent-ours/service.d.ts +23 -0
  17. package/dist/agent-ours/service.js +372 -0
  18. package/dist/agent-ours/state.d.ts +45 -0
  19. package/dist/agent-ours/state.js +95 -0
  20. package/dist/agent-ours/wire.d.ts +14 -0
  21. package/dist/agent-ours/wire.js +55 -0
  22. package/dist/agent-recovery-gate.js +1 -1
  23. package/dist/application/conversation-history.d.ts +6 -0
  24. package/dist/application/conversation-history.js +65 -0
  25. package/dist/application/fleet-query-service.d.ts +1 -1
  26. package/dist/application/fleet-query-service.js +2 -2
  27. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  28. package/dist/application/legacy-supervisor-identity.js +43 -0
  29. package/dist/application/model-catalog.d.ts +1 -1
  30. package/dist/application/model-catalog.js +5 -2
  31. package/dist/application/preset-provenance.d.ts +19 -0
  32. package/dist/application/preset-provenance.js +59 -0
  33. package/dist/application/room-layout-definitions.d.ts +45 -0
  34. package/dist/application/room-layout-definitions.js +130 -0
  35. package/dist/application/session-control.d.ts +8 -0
  36. package/dist/application/session-control.js +21 -3
  37. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  38. package/dist/application/supervisor-ours-tools.js +113 -0
  39. package/dist/application/task-layouts.d.ts +89 -0
  40. package/dist/application/task-layouts.js +248 -0
  41. package/dist/application/task-room-service.d.ts +99 -3
  42. package/dist/application/task-room-service.js +296 -31
  43. package/dist/atomic-file.d.ts +5 -0
  44. package/dist/atomic-file.js +62 -1
  45. package/dist/briefing.js +10 -59
  46. package/dist/build-info.json +5 -4
  47. package/dist/capabilities.d.ts +2 -1
  48. package/dist/capabilities.js +2 -0
  49. package/dist/cli.js +65 -2
  50. package/dist/client-profile.d.ts +5 -16
  51. package/dist/client-profile.js +4 -112
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +7 -0
  54. package/dist/creation.d.ts +1 -1
  55. package/dist/creation.js +5 -16
  56. package/dist/daemon-recovery.d.ts +1 -8
  57. package/dist/daemon-recovery.js +8 -113
  58. package/dist/docs.d.ts +3 -3
  59. package/dist/docs.js +43 -23
  60. package/dist/doctor.d.ts +1 -1
  61. package/dist/doctor.js +46 -59
  62. package/dist/erased-resources.d.ts +9 -0
  63. package/dist/erased-resources.js +87 -0
  64. package/dist/fleet-command-audit.d.ts +1 -0
  65. package/dist/fleet-command-audit.js +12 -7
  66. package/dist/harness/agent-session.d.ts +2 -0
  67. package/dist/harness/claude-code-session.js +4 -2
  68. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  69. package/dist/harness/codex-app-server-proxy.js +41 -5
  70. package/dist/harness/codex-session.js +9 -2
  71. package/dist/harness/codex.d.ts +3 -2
  72. package/dist/harness/codex.js +4 -3
  73. package/dist/harness/hermes-session.js +7 -1
  74. package/dist/init-wizard.js +19 -9
  75. package/dist/monitor.d.ts +1 -34
  76. package/dist/monitor.js +24 -151
  77. package/dist/owner-channel/channel.d.ts +3 -0
  78. package/dist/owner-channel/channel.js +34 -6
  79. package/dist/owner-channel/commands.d.ts +4 -0
  80. package/dist/owner-channel/commands.js +8 -0
  81. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  82. package/dist/owner-channel/ours-client.js +2 -17
  83. package/dist/preset-migration.js +15 -0
  84. package/dist/rooms-tasks/add-member.d.ts +48 -0
  85. package/dist/rooms-tasks/add-member.js +83 -0
  86. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  87. package/dist/rooms-tasks/archived-absence.js +48 -0
  88. package/dist/rooms-tasks/cli.js +34 -5
  89. package/dist/rooms-tasks/close.d.ts +5 -2
  90. package/dist/rooms-tasks/close.js +208 -28
  91. package/dist/rooms-tasks/config.js +3 -1
  92. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  93. package/dist/rooms-tasks/cowork-adapter.js +11 -117
  94. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  95. package/dist/rooms-tasks/cowork-http.js +70 -0
  96. package/dist/rooms-tasks/deletion.d.ts +1 -1
  97. package/dist/rooms-tasks/deletion.js +74 -5
  98. package/dist/rooms-tasks/erasure.d.ts +6 -0
  99. package/dist/rooms-tasks/erasure.js +169 -0
  100. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  101. package/dist/rooms-tasks/external-worker.js +3 -4
  102. package/dist/rooms-tasks/layout-cli.d.ts +2 -0
  103. package/dist/rooms-tasks/layout-cli.js +81 -0
  104. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  105. package/dist/rooms-tasks/layout-config.js +89 -0
  106. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  107. package/dist/rooms-tasks/layout-control.js +52 -0
  108. package/dist/rooms-tasks/layout-service.d.ts +39 -0
  109. package/dist/rooms-tasks/layout-service.js +173 -0
  110. package/dist/rooms-tasks/layout.d.ts +102 -0
  111. package/dist/rooms-tasks/layout.js +227 -0
  112. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  113. package/dist/rooms-tasks/live-readiness.js +85 -0
  114. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  115. package/dist/rooms-tasks/member-startup.js +2 -0
  116. package/dist/rooms-tasks/provision.d.ts +14 -0
  117. package/dist/rooms-tasks/provision.js +182 -48
  118. package/dist/rooms-tasks/room-state.d.ts +4 -2
  119. package/dist/rooms-tasks/room-state.js +53 -8
  120. package/dist/rooms-tasks/task-state.d.ts +42 -7
  121. package/dist/rooms-tasks/task-state.js +153 -23
  122. package/dist/rooms-tasks/types.d.ts +28 -0
  123. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  124. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  125. package/dist/rooms-tasks/workspace.d.ts +26 -0
  126. package/dist/rooms-tasks/workspace.js +195 -0
  127. package/dist/runner.d.ts +23 -3
  128. package/dist/runner.js +574 -494
  129. package/dist/session/acp.d.ts +3 -0
  130. package/dist/session/acp.js +7 -0
  131. package/dist/session/arbiter.d.ts +3 -0
  132. package/dist/session/arbiter.js +20 -2
  133. package/dist/session/codex-app-server-transport.js +1 -1
  134. package/dist/session/codex-app-server.d.ts +3 -0
  135. package/dist/session/codex-app-server.js +25 -1
  136. package/dist/session/control.d.ts +10 -1
  137. package/dist/session/control.js +36 -2
  138. package/dist/session/conversation-normalizer.d.ts +2 -1
  139. package/dist/session/conversation-normalizer.js +1 -1
  140. package/dist/session/conversation-types.d.ts +5 -3
  141. package/dist/session/task-notice.d.ts +14 -0
  142. package/dist/session/task-notice.js +13 -0
  143. package/dist/session/types.d.ts +3 -0
  144. package/dist/socket-path.d.ts +2 -0
  145. package/dist/socket-path.js +22 -0
  146. package/dist/spawn.js +19 -16
  147. package/dist/startup-readiness.d.ts +13 -0
  148. package/dist/startup-readiness.js +38 -0
  149. package/dist/supervisor/launchd.js +2 -1
  150. package/dist/supervisor/systemd.js +9 -23
  151. package/dist/temp-idle.d.ts +17 -0
  152. package/dist/temp-idle.js +42 -0
  153. package/dist/temp-lifecycle.d.ts +6 -1
  154. package/dist/temp-lifecycle.js +29 -13
  155. package/dist/temp-supervisor-recovery.d.ts +3 -0
  156. package/dist/temp-supervisor-recovery.js +46 -0
  157. package/dist/watchdog/briefing.js +3 -16
  158. package/dist/watchdog/run.js +19 -5
  159. package/dist/web/agent-attachments.d.ts +29 -0
  160. package/dist/web/agent-attachments.js +162 -0
  161. package/dist/web/auth.d.ts +4 -1
  162. package/dist/web/auth.js +23 -2
  163. package/dist/web/control.js +2 -1
  164. package/dist/web/fleet-config-service.d.ts +4 -1
  165. package/dist/web/fleet-config-service.js +46 -15
  166. package/dist/web/prefix-gateway.d.ts +20 -0
  167. package/dist/web/prefix-gateway.js +150 -0
  168. package/dist/web/runtime.d.ts +4 -0
  169. package/dist/web/runtime.js +19 -14
  170. package/dist/web/server.d.ts +7 -0
  171. package/dist/web/server.js +293 -25
  172. package/docs/room-layouts.md +92 -0
  173. package/docs/task-workspaces.md +130 -0
  174. package/examples/room-layouts/fresh.yaml +27 -0
  175. package/examples/room-layouts/shared.yaml +19 -0
  176. package/package.json +17 -13
  177. package/presets/README.md +66 -0
  178. package/presets/brain-catalog.json +7 -2
  179. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  180. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  181. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  182. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  183. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  184. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  185. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  186. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  187. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  188. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  189. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  190. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  191. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  192. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  193. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  194. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  195. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  196. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  197. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  198. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  199. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  200. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  201. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  202. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  203. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  204. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  205. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  206. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  207. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  208. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  209. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  210. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  211. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  212. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  213. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  214. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  215. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  216. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  217. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  218. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  219. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  220. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  221. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  222. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  223. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  224. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  225. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  226. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  227. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  228. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  229. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  230. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  231. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  232. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  233. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  234. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  235. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  236. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  237. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  238. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  239. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  240. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  241. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  242. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  243. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  244. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  245. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  246. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  247. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  248. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  249. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  250. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  251. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  252. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  253. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  254. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  255. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  256. package/presets/fleet/roles/Coordinator.yaml +22 -0
  257. package/presets/fleet/roles/Engineer.yaml +40 -0
  258. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  259. package/presets/fleet/room_layouts/pair.yaml +24 -0
  260. package/presets/fleet/room_layouts/single.yaml +17 -0
  261. package/presets/fleet/room_layouts/team.yaml +30 -0
  262. package/presets/fleet/room_templates/engineering.yaml +19 -0
  263. package/presets/manifest.json +2 -2
  264. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  265. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  266. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  267. package/dist/web-app/icons/ours-fleet.svg +0 -4
  268. package/dist/web-app/index.html +0 -17
  269. package/dist/web-app/manifest.webmanifest +0 -15
  270. package/dist/web-app/offline.html +0 -18
  271. package/dist/web-app/sw.js +0 -51
@@ -7,13 +7,18 @@ import { packagedPresetRoot } from './preset-bootstrap.js';
7
7
  import { listTemplates } from './rooms-tasks/templates.js';
8
8
  const WORK_KINDS = ['development', 'review', 'coordination'];
9
9
  const ROLE_WORK = {
10
- Developer: 'development', Critic: 'review',
10
+ Developer: 'development', Engineer: 'development', Critic: 'review',
11
11
  LocalCoordinator: 'coordination', Coordinator: 'coordination', FleetCoordinator: 'coordination',
12
12
  };
13
13
  const REASONING_EFFORT = {
14
14
  quick: 'low', balanced: 'medium', thorough: 'high',
15
15
  };
16
16
  const MODEL_LABELS = {
17
+ 'gpt-6-sol': 'GPT-6 Sol',
18
+ 'gpt-6-luna': 'GPT-6 Luna',
19
+ 'claude-fable-5-1': 'Claude Fable 5.1',
20
+ 'claude-opus-5-5': 'Claude Opus 5.5',
21
+ 'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
17
22
  'gpt-6-astra': 'GPT-6 Astra',
18
23
  'gpt-5.6-sol': 'GPT-5.6 Sol',
19
24
  'gpt-5.6-terra': 'GPT-5.6 Terra',
@@ -33,10 +38,10 @@ export function validateCatalog(value, path = 'supported model catalog') {
33
38
  for (const model of value.models) {
34
39
  if (!['codex', 'claude-code'].includes(model.harness) || model.session !== 'acp'
35
40
  || typeof model.model !== 'string' || !MODEL_LABELS[model.model]
36
- || !Array.isArray(model.efforts) || model.efforts.length === 0
41
+ || !Array.isArray(model.efforts)
37
42
  || new Set(model.efforts).size !== model.efforts.length
38
43
  || model.efforts.some(effort => !['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(effort))
39
- || !['low', 'medium', 'high'].every(effort => model.efforts.includes(effort)))
44
+ || (model.efforts.length > 0 && !['low', 'medium', 'high'].every(effort => model.efforts.includes(effort))))
40
45
  throw new Error(`unsupported model state in ${path}`);
41
46
  const tuple = `${model.harness}\0${model.session}\0${model.model}`;
42
47
  if (tuples.has(tuple))
@@ -90,7 +95,9 @@ export async function askInitQuestions(prompter, configuration) {
90
95
  return undefined;
91
96
  if (subscriptions.length === 0)
92
97
  throw new Error('Select at least one subscription.');
93
- const available = catalog().models.filter(model => subscriptions.includes(subscriptionFor(model)));
98
+ // The wizard assigns low/medium/high; no-effort models remain available as named presets.
99
+ const available = catalog().models.filter(model => subscriptions.includes(subscriptionFor(model))
100
+ && model.efforts.length > 0);
94
101
  if (!available.length)
95
102
  throw new Error('None of the selected subscriptions has a supported model.');
96
103
  prompter.note([
@@ -163,6 +170,7 @@ export function formatSetupSummary(answers, configuration) {
163
170
  ' One-agent work: one Developer',
164
171
  ' Reviewed pair: Developer + independent Critic',
165
172
  ' Team: LocalCoordinator + Developer + Critic',
173
+ ' Gated engineering: Engineer (gate pipeline) + independent Critic',
166
174
  ' FleetCoordinator: coordination model',
167
175
  '',
168
176
  'After the final Yes, Fleet performs host service setup before publishing the complete configuration.',
@@ -221,7 +229,7 @@ export function generateSetup(answers) {
221
229
  if (tuples.size !== 1)
222
230
  throw new Error('one-model assignment requires the same exact model for development, review, and coordination');
223
231
  }
224
- for (const role of ['Coordinator', 'LocalCoordinator', 'Developer', 'Critic']) {
232
+ for (const role of ['Coordinator', 'LocalCoordinator', 'Developer', 'Engineer', 'Critic']) {
225
233
  files.set(`roles/${role}.yaml`, preset(`roles/${role}.yaml`));
226
234
  if (role === 'Coordinator')
227
235
  continue;
@@ -236,8 +244,10 @@ export function generateSetup(answers) {
236
244
  }
237
245
  files.set('agents/FleetCoordinator.yaml', preset('agents/FleetCoordinator.yaml')
238
246
  .replace('brain: { ref: claude-default }', 'brain: { ref: coordination }'));
239
- for (const name of ['single', 'pair', 'team'])
247
+ for (const name of ['single', 'pair', 'team', 'engineering']) {
240
248
  files.set(`room_templates/${name}.yaml`, preset(`room_templates/${name}.yaml`));
249
+ files.set(`room_layouts/${name}.yaml`, preset(`room_layouts/${name}.yaml`));
250
+ }
241
251
  return { files, answers };
242
252
  }
243
253
  /** Run the mutation boundary only after the complete questionnaire is accepted. */
@@ -385,7 +395,7 @@ function writeStaged(stageManifest, setup) {
385
395
  replaceFileAtomically(target, contents, 0o600);
386
396
  fsyncPath(target);
387
397
  }
388
- for (const kind of ['agents', 'agent_templates', 'roles', 'brains', 'room_templates'])
398
+ for (const kind of ['agents', 'agent_templates', 'roles', 'brains', 'room_templates', 'room_layouts'])
389
399
  fsyncPath(join(stageRoot, kind));
390
400
  fsyncPath(stageRoot);
391
401
  fsyncPath(dirname(stageManifest));
@@ -394,8 +404,8 @@ function validateStaged(stageManifest) {
394
404
  const config = loadConfig(stageManifest, { yamlMode: 'strict' });
395
405
  const templates = listTemplates(config.roomTemplates ?? {});
396
406
  const names = new Set(templates.map(template => template.name));
397
- if (!['pair', 'single', 'team'].every(name => names.has(name)))
398
- throw new Error('generated setup did not resolve the default single, pair, and team experiences');
407
+ if (!['pair', 'single', 'team', 'engineering'].every(name => names.has(name)))
408
+ throw new Error('generated setup did not resolve the default single, pair, team, and engineering experiences');
399
409
  for (const template of templates)
400
410
  for (const member of template.members)
401
411
  if (!config.agentTemplates?.[member.agent_template])
package/dist/monitor.d.ts CHANGED
@@ -88,33 +88,6 @@ export interface MonitorDeps {
88
88
  * turns they trigger keep dying.
89
89
  */
90
90
  export type StatusCause = 'connectivity' | 'delivery' | 'modal' | 'offline' | 'turns-failing' | 'safe-boundary' | 'auth';
91
- /** Best-effort daemon config (issue #17): the fields the MCP client reads. */
92
- interface DaemonConfig {
93
- apiToken?: string;
94
- port?: number;
95
- stateDir?: string;
96
- }
97
- /**
98
- * Read the daemon config the way the MCP client does — best-effort. Any missing,
99
- * malformed, or unreadable config yields `{}` so token resolution falls through
100
- * (issue #17). Only the well-typed fields we consume are surfaced.
101
- */
102
- export declare function readDaemonConfig(env: NodeJS.ProcessEnv): DaemonConfig;
103
- /**
104
- * Resolve the daemon API token exactly like the MCP client (issue #17), a 3-step
105
- * chain: `OURS_API_TOKEN` (trimmed) → config `apiToken` (trimmed) → the 0600 owner
106
- * token at `<stateDir>/daemon-token`. Never generates a token; a failed read of
107
- * any source (missing/unreadable) silently falls through to the next.
108
- */
109
- export declare function resolveApiToken(env: NodeJS.ProcessEnv, file?: DaemonConfig): string | undefined;
110
- export interface DaemonEndpoint {
111
- origin: string;
112
- port: number;
113
- configPath: string;
114
- stateDir: string;
115
- url(name: string): string;
116
- headers: Record<string, string>;
117
- }
118
91
  export type IdentityPresence = {
119
92
  state: 'present';
120
93
  temporary: boolean;
@@ -125,16 +98,12 @@ export type IdentityPresence = {
125
98
  state: 'unknown';
126
99
  detail: string;
127
100
  };
128
- /** Resolve the daemon endpoint + auth header from env → config → defaults. */
129
- export declare function resolveEndpoint(env: NodeJS.ProcessEnv, includeToken?: boolean): DaemonEndpoint;
130
101
  /**
131
102
  * Ask the daemon's authoritative identity index. The notifications endpoint is
132
103
  * intentionally unsuitable for lifecycle: it serves an empty 200 page for a
133
104
  * valid but missing identity, which made a closed temp identity look healthy.
134
105
  */
135
106
  export declare function probeIdentityPresence(name: string, fetch: FetchLike, env: NodeJS.ProcessEnv, deps?: IdentityProbeDeps): Promise<IdentityPresence>;
136
- /** Actionable, secret-free description of every token source for this profile. */
137
- export declare function authResolutionHint(ep: DaemonEndpoint): string;
138
107
  /** Keep only the events whose type the role asked to wake on. */
139
108
  export declare function filterEvents(events: NotifyEvent[], wakeSources: string[]): NotifyEvent[];
140
109
  /**
@@ -196,8 +165,7 @@ export declare class Monitor {
196
165
  private readonly identity;
197
166
  private readonly cfg;
198
167
  private readonly deps;
199
- private readonly ep?;
200
- private readonly profile?;
168
+ private readonly profile;
201
169
  private readonly profileKey;
202
170
  private readonly monitorLeaseToken;
203
171
  private profileClientPromise?;
@@ -263,4 +231,3 @@ export declare class Monitor {
263
231
  private writeStatus;
264
232
  }
265
233
  export declare function createMonitor(o: MonitorOpts): Monitor;
266
- export {};
package/dist/monitor.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { homedir } from 'node:os';
4
3
  import { join } from 'node:path';
5
4
  import { attachOursClient } from '@ours.network/sdk/client';
6
5
  import { clientProfileKey, readClientProfile, } from './client-profile.js';
@@ -42,124 +41,28 @@ const TURN_OBSERVE_INTERVAL_MS = 1_500;
42
41
  const DEFAULT_TURN_FAIL_THRESHOLD = 3; // fallback when the resolved config omits it
43
42
  class AuthError extends Error {
44
43
  }
45
- /** Path to the daemon config the MCP client uses: OURS_CONFIG ?? real ~/.ours/config.json. */
46
- const daemonConfigPath = (env) => env.OURS_CONFIG ?? join(homedir(), '.ours', 'config.json');
47
- /** Preserve the daemon's legacy env integer semantics: parseInt, invalid → absent. */
48
- function envInt(env, name) {
49
- const raw = env[name];
50
- if (raw === undefined)
51
- return undefined;
52
- const n = parseInt(raw, 10);
53
- return Number.isNaN(n) ? undefined : n;
54
- }
55
- /**
56
- * Read the daemon config the way the MCP client does — best-effort. Any missing,
57
- * malformed, or unreadable config yields `{}` so token resolution falls through
58
- * (issue #17). Only the well-typed fields we consume are surfaced.
59
- */
60
- export function readDaemonConfig(env) {
61
- try {
62
- const p = JSON.parse(readFileSync(daemonConfigPath(env), 'utf8'));
63
- const o = {};
64
- if (typeof p.apiToken === 'string' && p.apiToken.trim())
65
- o.apiToken = p.apiToken.trim();
66
- if (typeof p.port === 'number' && Number.isFinite(p.port))
67
- o.port = p.port;
68
- if (typeof p.stateDir === 'string')
69
- o.stateDir = p.stateDir;
70
- return o;
71
- }
72
- catch {
73
- return {};
74
- }
75
- }
76
- /**
77
- * Resolve the daemon API token exactly like the MCP client (issue #17), a 3-step
78
- * chain: `OURS_API_TOKEN` (trimmed) → config `apiToken` (trimmed) → the 0600 owner
79
- * token at `<stateDir>/daemon-token`. Never generates a token; a failed read of
80
- * any source (missing/unreadable) silently falls through to the next.
81
- */
82
- export function resolveApiToken(env, file = readDaemonConfig(env)) {
83
- const e = env.OURS_API_TOKEN?.trim();
84
- if (e)
85
- return e;
86
- if (file.apiToken)
87
- return file.apiToken;
88
- const sd = env.OURS_STATE_DIR ?? file.stateDir ?? join(homedir(), '.ours');
89
- try {
90
- const t = readFileSync(join(sd, 'daemon-token'), 'utf8').trim();
91
- if (t)
92
- return t;
93
- }
94
- catch { /* missing/unreadable (e.g. cross-user 0600) → fall through */ }
95
- return undefined;
96
- }
97
- /** Resolve the daemon endpoint + auth header from env → config → defaults. */
98
- export function resolveEndpoint(env, includeToken = true) {
99
- const file = readDaemonConfig(env);
100
- const port = envInt(env, 'OURS_PORT') ?? file.port ?? DEFAULT_PORT;
101
- const configPath = daemonConfigPath(env);
102
- const stateDir = env.OURS_STATE_DIR ?? file.stateDir ?? join(homedir(), '.ours');
103
- const token = includeToken ? resolveApiToken(env, file) : undefined;
104
- const origin = `http://127.0.0.1:${port}`;
105
- return {
106
- origin,
107
- port,
108
- configPath,
109
- stateDir,
110
- url: (name) => `${origin}/identities/${encodeURIComponent(name)}/notifications`,
111
- headers: token ? { 'x-ours-api-token': token } : {},
112
- };
113
- }
114
44
  /**
115
45
  * Ask the daemon's authoritative identity index. The notifications endpoint is
116
46
  * intentionally unsuitable for lifecycle: it serves an empty 200 page for a
117
47
  * valid but missing identity, which made a closed temp identity look healthy.
118
48
  */
119
49
  export async function probeIdentityPresence(name, fetch, env, deps = {}) {
120
- const profile = readClientProfile(env);
121
- if (profile) {
122
- let client;
123
- try {
124
- client = await (deps.attachClient?.({
125
- endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
126
- credentialPath: profile.credentialPath,
127
- sessionMode: 'external', leaseToken: randomUUID(), env: {},
128
- }) ?? attachOursClient({
129
- endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
130
- credentialPath: profile.credentialPath,
131
- sessionMode: 'external', leaseToken: randomUUID(), env: {},
132
- }));
133
- return classifyIdentityPresence(name, await client.identities());
134
- }
135
- catch {
136
- return { state: 'unknown', detail: 'selected identity index is unavailable' };
137
- }
138
- finally {
139
- await client?.close().catch(() => undefined);
140
- }
141
- }
142
- const ep = resolveEndpoint(env);
143
- let response;
50
+ let client;
144
51
  try {
145
- response = await fetch(`${ep.origin}/identities`, { headers: ep.headers });
146
- }
147
- catch (error) {
148
- return { state: 'unknown', detail: `identity index unreachable (${msg(error)})` };
149
- }
150
- if (!response.ok)
151
- return {
152
- state: 'unknown',
153
- detail: response.status === 401
154
- ? `daemon rejected the API token (401) — ${authResolutionHint(ep)}`
155
- : `identity index returned HTTP ${response.status}`,
52
+ const profile = readClientProfile(env);
53
+ const options = {
54
+ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
55
+ credentialPath: profile.credentialPath,
56
+ sessionMode: 'external', leaseToken: randomUUID(), env: {},
156
57
  };
157
- try {
158
- const body = await response.json();
159
- return classifyIdentityPresence(name, body.identities);
58
+ client = await (deps.attachClient?.(options) ?? attachOursClient(options));
59
+ return classifyIdentityPresence(name, await client.identities());
160
60
  }
161
- catch (error) {
162
- return { state: 'unknown', detail: `identity index response is unreadable (${msg(error)})` };
61
+ catch {
62
+ return { state: 'unknown', detail: 'gateway identity index unavailable; check the shared client profile and issued credential' };
63
+ }
64
+ finally {
65
+ await client?.close().catch(() => undefined);
163
66
  }
164
67
  }
165
68
  function classifyIdentityPresence(name, identities) {
@@ -186,12 +89,6 @@ function classifyIdentityPresence(name, identities) {
186
89
  stale: found.stale === true,
187
90
  };
188
91
  }
189
- /** Actionable, secret-free description of every token source for this profile. */
190
- export function authResolutionHint(ep) {
191
- const tokenPath = join(ep.stateDir, 'daemon-token');
192
- return `set OURS_API_TOKEN, set apiToken in ${JSON.stringify(ep.configPath)}, or ensure ` +
193
- `${JSON.stringify(tokenPath)} is readable by the fleet supervisor`;
194
- }
195
92
  /** Keep only the events whose type the role asked to wake on. */
196
93
  export function filterEvents(events, wakeSources) {
197
94
  const set = new Set(wakeSources);
@@ -414,7 +311,6 @@ export class Monitor {
414
311
  identity;
415
312
  cfg;
416
313
  deps;
417
- ep;
418
314
  profile;
419
315
  profileKey;
420
316
  monitorLeaseToken = `ours-fleet-monitor-${process.pid}-${randomUUID()}`;
@@ -441,8 +337,7 @@ export class Monitor {
441
337
  this.cfg = o.cfg;
442
338
  this.deps = o.deps;
443
339
  this.profile = readClientProfile(o.deps.env);
444
- this.ep = this.profile ? undefined : resolveEndpoint(o.deps.env);
445
- this.profileKey = this.profile ? clientProfileKey(this.profile) : this.ep.origin;
340
+ this.profileKey = clientProfileKey(this.profile);
446
341
  this.statusPath = join(o.agentDir, '.monitor-status');
447
342
  this.cursorPath = join(o.agentDir, '.notify-cursor');
448
343
  this.statePath = join(o.agentDir, '.monitor-state.json');
@@ -470,14 +365,8 @@ export class Monitor {
470
365
  }
471
366
  catch (e) {
472
367
  if (e instanceof AuthError) {
473
- if (this.profile) {
474
- this.cursor = null;
475
- this.degrade('auth', e.message);
476
- }
477
- else {
478
- this.fatal = true;
479
- this.degrade('auth', e.message, 'failed');
480
- }
368
+ this.cursor = null;
369
+ this.degrade('auth', e.message);
481
370
  }
482
371
  else {
483
372
  this.cursor = null;
@@ -510,11 +399,6 @@ export class Monitor {
510
399
  catch (e) {
511
400
  if (this.stopped)
512
401
  return;
513
- if (e instanceof AuthError && !this.profile) {
514
- this.fatal = true;
515
- this.degrade('auth', e.message, 'failed');
516
- return;
517
- }
518
402
  backoff = Math.min(backoff + BACKOFF_STEP_MS, BACKOFF_MAX_MS);
519
403
  if (e instanceof AuthError)
520
404
  this.degrade('auth', e.message);
@@ -630,23 +514,19 @@ export class Monitor {
630
514
  this.currentAbort = ctrl;
631
515
  let timedOut = false;
632
516
  const timer = this.deps.timers.set(() => { timedOut = true; ctrl.abort(); }, timeoutMs);
633
- let resp;
634
517
  try {
635
- if (this.profile) {
636
- const client = await this.profileClient();
637
- const page = await client.readNotificationPage(this.identity, {
638
- since: since === 'tip' ? 'tip' : Number.parseInt(since, 10),
639
- signal: ctrl.signal,
640
- requestTimeoutMs: timeoutMs,
641
- });
642
- return { cursor: page.cursor, events: page.events };
643
- }
644
- resp = await this.deps.fetch(`${this.ep.url(this.identity)}?since=${since}`, { headers: this.ep.headers, signal: ctrl.signal });
518
+ const client = await this.profileClient();
519
+ const page = await client.readNotificationPage(this.identity, {
520
+ since: since === 'tip' ? 'tip' : Number.parseInt(since, 10),
521
+ signal: ctrl.signal,
522
+ requestTimeoutMs: timeoutMs,
523
+ });
524
+ return { cursor: page.cursor, events: page.events };
645
525
  }
646
526
  catch (error) {
647
527
  if (timedOut && timeoutKind === 'stall')
648
528
  throw new Error(`notification stream stalled for ${Math.round(timeoutMs / 1000)}s`);
649
- if (this.profile && /(?:HTTP\s*401|unauthori[sz]ed|API token)/i.test(msg(error)))
529
+ if (/(?:HTTP\s*401|unauthori[sz]ed|API token)/i.test(msg(error)))
650
530
  throw new AuthError(`daemon rejected the API token (401) — verify the credential file `
651
531
  + `${JSON.stringify(this.profile.credentialPath)} selected by `
652
532
  + `${JSON.stringify(this.profile.configPath)}`);
@@ -656,15 +536,8 @@ export class Monitor {
656
536
  this.deps.timers.clear(timer);
657
537
  this.currentAbort = null;
658
538
  }
659
- if (resp.status === 401)
660
- throw new AuthError(`daemon rejected the API token (401) — ${authResolutionHint(this.ep)}`);
661
- if (!resp.ok)
662
- throw new Error(`daemon returned HTTP ${resp.status}`);
663
- return resp.json();
664
539
  }
665
540
  profileClient() {
666
- if (!this.profile)
667
- throw new Error('explicit client profile is not selected');
668
541
  if (!this.profileClientPromise) {
669
542
  const { endpoint, expectedInstanceId, credentialPath } = this.profile;
670
543
  const options = {
@@ -14,6 +14,8 @@ export interface OwnerChannelOptions {
14
14
  harness: string;
15
15
  config: OwnerChannelConfig;
16
16
  session: AgentSession;
17
+ /** Ordinary owner input must not cancel the initial briefing turn. */
18
+ startupPending?: () => boolean;
17
19
  stateDir: string;
18
20
  env?: Record<string, string>;
19
21
  log(line: string): void;
@@ -269,6 +271,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
269
271
  private attachmentGroups;
270
272
  private handleAttachmentGroup;
271
273
  private handle;
274
+ private ownerPromptPolicy;
272
275
  /**
273
276
  * Deterministic command path: the message never becomes an agent prompt.
274
277
  * Authorization already happened — the managed-agent relay branch and the
@@ -883,8 +883,15 @@ export class OwnerChannel {
883
883
  const recovered = [];
884
884
  for (const claim of this.messageRecovery.list()) {
885
885
  const item = await this.client.getHistoryItem(claim.wireId);
886
- if (!item)
887
- throw new Error(`journaled owner message ${claim.wireId} is missing from persistent history`);
886
+ if (!item) {
887
+ // The daemon has definitively lost this body. Retrying the claim would
888
+ // block every later owner message, including after a supervisor restart.
889
+ // Keep transient lookup errors retryable; only an explicit absence skips.
890
+ this.messageRecovery.pruneHandled(wireId => wireId === claim.wireId);
891
+ this.options.log(`[${this.options.role}] skipped journaled owner message ${claim.wireId} `
892
+ + `(seq=${claim.seq}): missing from persistent history`);
893
+ continue;
894
+ }
888
895
  recovered.push(this.historyMessage(item, claim));
889
896
  }
890
897
  const listed = await this.client.listIncomingMessages();
@@ -1123,6 +1130,13 @@ export class OwnerChannel {
1123
1130
  const retrieved = unread.length
1124
1131
  ? parseRetrievedAttachments(await this.client.getFiles(unread.map(file => file.wireId)), unread)
1125
1132
  : [];
1133
+ // getFiles returns daemon-local paths, which need not exist in Fleet's
1134
+ // filesystem (for example with an HTTP daemon in a container). Fetch
1135
+ // through the bound client and keep the daemon's integrity metadata for
1136
+ // admission below; never trust or remap the returned filesystem path.
1137
+ for (const file of retrieved) {
1138
+ file.path = await writeRecoveredAttachment(requestDir, file.wireId, await this.client.fetchFile(file.wireId));
1139
+ }
1126
1140
  for (const file of historyRecovered) {
1127
1141
  if (!group.recovery)
1128
1142
  throw new Error('unexpected read attachment without recovery route');
@@ -1136,8 +1150,7 @@ export class OwnerChannel {
1136
1150
  await mkdir(outbox, { recursive: true, mode: 0o700 });
1137
1151
  const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
1138
1152
  const queued = await queueSessionPrompt(this.options.session, this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
1139
- interrupt: this.options.config.interrupt,
1140
- ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
1153
+ ...this.ownerPromptPolicy(),
1141
1154
  origin: { kind: 'owner', requestId,
1142
1155
  ...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
1143
1156
  });
@@ -1263,8 +1276,7 @@ export class OwnerChannel {
1263
1276
  const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
1264
1277
  try {
1265
1278
  queued = await queueSessionPrompt(this.options.session, this.ownerPrompt(sender, text, wireId), {
1266
- interrupt: this.options.config.interrupt,
1267
- ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
1279
+ ...this.ownerPromptPolicy(),
1268
1280
  origin: { kind: 'owner', requestId, displayText: text },
1269
1281
  });
1270
1282
  }
@@ -1309,6 +1321,14 @@ export class OwnerChannel {
1309
1321
  this.completionTasks.add(task);
1310
1322
  return true;
1311
1323
  }
1324
+ ownerPromptPolicy() {
1325
+ if (this.options.startupPending?.())
1326
+ return { interrupt: false, steer: true };
1327
+ return {
1328
+ interrupt: this.options.config.interrupt,
1329
+ ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
1330
+ };
1331
+ }
1312
1332
  /**
1313
1333
  * Deterministic command path: the message never becomes an agent prompt.
1314
1334
  * Authorization already happened — the managed-agent relay branch and the
@@ -1378,6 +1398,7 @@ export class OwnerChannel {
1378
1398
  await this.fleetOps.provisionTask(task.task_id);
1379
1399
  return outcome;
1380
1400
  }),
1401
+ observeTaskReadiness: taskId => new TaskRoomApplicationService(this.options.configPath).observeTaskReadiness(taskId),
1381
1402
  taskProvisioningOutcome: taskId => new TaskRoomApplicationService(this.options.configPath).taskProvisioningOutcome(taskId),
1382
1403
  listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
1383
1404
  groupedTasks: filter => new TaskRoomApplicationService(this.options.configPath).groupedTasks(filter),
@@ -1762,6 +1783,13 @@ export class OwnerChannel {
1762
1783
  const retrieved = unread.length
1763
1784
  ? parseRetrievedAttachments(await this.client.getFiles(unread.map(file => file.wireId)), unread)
1764
1785
  : [];
1786
+ // getFiles returns daemon-local paths, which need not exist in Fleet's
1787
+ // filesystem (for example with an HTTP daemon in a container). Fetch
1788
+ // through the bound client and keep the daemon's integrity metadata for
1789
+ // admission below; never trust or remap the returned filesystem path.
1790
+ for (const file of retrieved) {
1791
+ file.path = await writeRecoveredAttachment(requestDir, file.wireId, await this.client.fetchFile(file.wireId));
1792
+ }
1765
1793
  for (const file of historyRecovered) {
1766
1794
  if (!group.recovery)
1767
1795
  throw new Error('unexpected read attachment without recovery route');
@@ -1,3 +1,4 @@
1
+ import type { TaskReadinessIssue } from '../rooms-tasks/live-readiness.js';
1
2
  import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
2
3
  import type { RoomOrchestrationRecord, TaskOutcome, TaskRecord, TaskTerminalIntent, TaskListRecord } from '../rooms-tasks/types.js';
3
4
  import { type OwnerCommentsState } from './notices.js';
@@ -58,6 +59,9 @@ export interface OwnerCommandContext {
58
59
  terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
59
60
  createTask(input: Omit<CreateTaskRequest, 'actor'>): Promise<TaskRecord>;
60
61
  startTask(taskId: string): Promise<TaskProvisioningOutcome | TaskRecord>;
62
+ observeTaskReadiness?(taskId: string): Promise<{
63
+ state: 'ready';
64
+ } | TaskReadinessIssue>;
61
65
  taskProvisioningOutcome?(taskId: string): TaskProvisioningOutcome;
62
66
  listTasks(filter?: {
63
67
  state?: TaskState | TaskState[];
@@ -47,6 +47,11 @@ function isKnownOwnerRoomState(message) {
47
47
  ].some(pattern => pattern.test(message));
48
48
  }
49
49
  function ownerTaskFailure(error) {
50
+ if (error instanceof TaskRoomApplicationError && error.code === 'task_not_ready')
51
+ return {
52
+ kind: 'state', detail: error.message,
53
+ action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery.',
54
+ };
50
55
  if (error instanceof TaskListError)
51
56
  return {
52
57
  kind: error.code === 'list_not_found' ? 'not_found'
@@ -301,6 +306,7 @@ export const ownerCommands = [
301
306
  const trailingLines = argLines.slice(1).join('\n').trim() || undefined;
302
307
  const showTask = async (id) => {
303
308
  const { task: t } = ctx.getTask(id);
309
+ const readiness = t.state === 'active' ? await ctx.observeTaskReadiness?.(id) : undefined;
304
310
  await ctx.reply(renderMarkdownResult({
305
311
  icon: '📋', title: 'Task details',
306
312
  fields: [
@@ -308,6 +314,8 @@ export const ownerCommands = [
308
314
  { label: 'Title', value: t.title },
309
315
  { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
310
316
  { label: 'List', value: t.list_name ?? 'default', kind: 'code' },
317
+ ...(readiness ? [{ label: 'Readiness', value: readiness.state },
318
+ ...('reason' in readiness ? [{ label: 'Reason', value: readiness.reason }] : [])] : []),
311
319
  ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
312
320
  ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
313
321
  ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
@@ -1,6 +1,6 @@
1
+ import { redactErasedContent, writePrivacyFilteredLedger } from '../erased-resources.js';
1
2
  import { createHash } from 'node:crypto';
2
3
  import { chmodSync, existsSync, readFileSync } from 'node:fs';
3
- import { replaceFileAtomically } from '../atomic-file.js';
4
4
  import { lifecycleEventDigestBasis, validateFleetAuditPresentations, } from '../fleet-command-audit.js';
5
5
  const DIGEST = /^[a-f0-9]{64}$/u;
6
6
  const LIMIT = 5_000;
@@ -63,7 +63,7 @@ export class FleetLifecycleOutbox {
63
63
  }
64
64
  pending() {
65
65
  this.assertHealthy();
66
- return this.entries.filter(entry => entry.delivery === 'pending').map(entry => structuredClone(entry));
66
+ return redactErasedContent(this.entries).filter(entry => entry.delivery === 'pending').map(entry => structuredClone(entry));
67
67
  }
68
68
  finish(digest, delivery) {
69
69
  this.assertHealthy();
@@ -82,6 +82,7 @@ export class FleetLifecycleOutbox {
82
82
  throw new Error(this.corruptReason);
83
83
  }
84
84
  persist() {
85
- replaceFileAtomically(this.path, `${JSON.stringify({ version: 1, entries: this.entries })}\n`, 0o600);
85
+ this.entries = redactErasedContent(this.entries);
86
+ writePrivacyFilteredLedger(this.path, { version: 1, entries: this.entries });
86
87
  }
87
88
  }
@@ -1,7 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { extname, join } from 'node:path';
4
- import { readDaemonConfig, resolveEndpoint } from '../monitor.js';
3
+ import { extname } from 'node:path';
5
4
  import { readClientProfile } from '../client-profile.js';
6
5
  import { OursError, attachOursClient, } from '@ours.network/sdk/client';
7
6
  /** Any failure of a daemon operation. Never carries a message body or a token. */
@@ -131,24 +130,10 @@ export class OursSdkClient {
131
130
  return;
132
131
  const environment = { ...process.env, ...this.env };
133
132
  const profile = readClientProfile(environment);
134
- // Reuse existing connection configuration. Identity selection checks the
135
- // daemon capability before reading the client's protected token delivery file.
136
- const endpoint = !profile && environment.OURS_DAEMON_ID
137
- ? resolveEndpoint(environment, false) : undefined;
138
- const token = endpoint ? environment.OURS_API_TOKEN?.trim() || readDaemonConfig(environment).apiToken : undefined;
139
- const options = profile ? {
133
+ const options = {
140
134
  endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
141
135
  credentialPath: profile.credentialPath,
142
136
  sessionMode: 'external', leaseToken: this.leaseToken, env: {},
143
- } : endpoint ? {
144
- endpoint: endpoint.origin, expectedInstanceId: environment.OURS_DAEMON_ID,
145
- sessionMode: 'external', leaseToken: this.leaseToken,
146
- ...(token ? { token } : { credentialPath: join(endpoint.stateDir, 'daemon-token') }),
147
- } : {
148
- env: environment,
149
- leaseToken: this.leaseToken,
150
- clientPid: process.pid,
151
- fetch: notificationDeadlineFetch(this.deps.fetch ?? globalThis.fetch, this.deps.notificationRequestDeadlineMs ?? NOTIFICATION_REQUEST_DEADLINE_MS),
152
137
  };
153
138
  this.client = await (this.deps.attachClient?.(options) ?? attachOursClient(options));
154
139
  this.terminalReleasePending = true;
@@ -69,8 +69,11 @@ const V5_GENERATED_ROLE_DEFAULT_FINGERPRINTS = Object.freeze({
69
69
  });
70
70
  const CURRENT_ROLE_DEFAULTS = new Set([
71
71
  'roles/Coordinator.yaml', 'roles/LocalCoordinator.yaml', 'roles/Developer.yaml', 'roles/Critic.yaml',
72
+ 'roles/Engineer.yaml',
72
73
  'agent_templates/LocalCoordinator.yaml', 'agent_templates/Developer.yaml', 'agent_templates/Critic.yaml',
74
+ 'agent_templates/Engineer.yaml',
73
75
  'room_templates/single.yaml', 'room_templates/pair.yaml', 'room_templates/team.yaml',
76
+ 'room_templates/engineering.yaml',
74
77
  'agents/FleetCoordinator.yaml',
75
78
  ]);
76
79
  export const legacyStarterMigrationManifest = Object.freeze({
@@ -258,6 +261,18 @@ export function migratePackagedRoleDefaults(configuration, options = {}, hooks =
258
261
  unlinkSync(target);
259
262
  copyFileSync(join(source, relative), target, constants.COPYFILE_EXCL);
260
263
  chmodSync(target, 0o600);
264
+ // New Engineer seats use the existing Developer's selected brain. This
265
+ // preserves wizard/custom provider choices without copying permissions.
266
+ if (relative === 'agent_templates/Engineer.yaml' && classified.additions.includes(path)) {
267
+ const developer = join(root, 'agent_templates/Developer.yaml');
268
+ if (existsSync(developer)) {
269
+ const value = parseFleetDocument(developer, readFileSync(developer, 'utf8'), 'strict').value;
270
+ if (value.brain !== undefined && JSON.stringify(value.brain) !== JSON.stringify({ ref: 'claude-default' })) {
271
+ const bytes = readFileSync(target, 'utf8').replace('brain: { ref: claude-default }', () => `brain: ${JSON.stringify(value.brain)}`);
272
+ writeFileSync(target, bytes, { mode: 0o600 });
273
+ }
274
+ }
275
+ }
261
276
  fsyncPath(target);
262
277
  }
263
278
  for (const kind of ['roles', 'agent_templates', 'room_templates', 'agents'])