@ours.network/fleet 0.19.0-nightly.1 → 0.19.0-nightly.11

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 (47) hide show
  1. package/README.md +30 -0
  2. package/dist/briefing.js +92 -6
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli.js +5 -0
  5. package/dist/config.d.ts +22 -0
  6. package/dist/config.js +44 -3
  7. package/dist/docs.d.ts +1 -1
  8. package/dist/docs.js +28 -0
  9. package/dist/doctor.js +125 -1
  10. package/dist/harness/claude-code.js +2 -0
  11. package/dist/harness/codex.js +2 -0
  12. package/dist/harness/types.d.ts +2 -0
  13. package/dist/owner-channel/channel.d.ts +7 -0
  14. package/dist/owner-channel/channel.js +47 -0
  15. package/dist/owner-channel/commands.d.ts +9 -0
  16. package/dist/owner-channel/commands.js +393 -0
  17. package/dist/rooms-tasks/cli.d.ts +4 -0
  18. package/dist/rooms-tasks/cli.js +1172 -0
  19. package/dist/rooms-tasks/close.d.ts +21 -0
  20. package/dist/rooms-tasks/close.js +159 -0
  21. package/dist/rooms-tasks/config.d.ts +15 -0
  22. package/dist/rooms-tasks/config.js +174 -0
  23. package/dist/rooms-tasks/cowork-adapter.d.ts +98 -0
  24. package/dist/rooms-tasks/cowork-adapter.js +385 -0
  25. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  26. package/dist/rooms-tasks/external-worker.js +40 -0
  27. package/dist/rooms-tasks/index.d.ts +10 -0
  28. package/dist/rooms-tasks/index.js +10 -0
  29. package/dist/rooms-tasks/member-startup.d.ts +51 -0
  30. package/dist/rooms-tasks/member-startup.js +151 -0
  31. package/dist/rooms-tasks/provision.d.ts +29 -0
  32. package/dist/rooms-tasks/provision.js +514 -0
  33. package/dist/rooms-tasks/room-state.d.ts +34 -0
  34. package/dist/rooms-tasks/room-state.js +263 -0
  35. package/dist/rooms-tasks/task-state.d.ts +43 -0
  36. package/dist/rooms-tasks/task-state.js +277 -0
  37. package/dist/rooms-tasks/templates.d.ts +6 -0
  38. package/dist/rooms-tasks/templates.js +94 -0
  39. package/dist/rooms-tasks/terminal.d.ts +23 -0
  40. package/dist/rooms-tasks/terminal.js +53 -0
  41. package/dist/rooms-tasks/types.d.ts +260 -0
  42. package/dist/rooms-tasks/types.js +20 -0
  43. package/dist/spawn.d.ts +3 -1
  44. package/dist/spawn.js +1 -0
  45. package/dist/temp-lifecycle.d.ts +14 -2
  46. package/dist/temp-lifecycle.js +90 -0
  47. package/package.json +1 -1
package/dist/doctor.js CHANGED
@@ -106,6 +106,126 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
106
106
  ok: true,
107
107
  detail: `warning: ${diagnostic.message}`,
108
108
  });
109
+ // ── Rooms/tasks checks (§5.3) ────────────────────────────────────────
110
+ if (loaded.ok && loaded.rooms) {
111
+ const rooms = loaded.rooms;
112
+ // Cowork socket reachability
113
+ const coworkConfig = rooms.cowork?.config;
114
+ try {
115
+ const { createCoworkAdapter } = await import('./rooms-tasks/cowork-adapter.js');
116
+ const adapter = createCoworkAdapter({ configPath: coworkConfig });
117
+ const reachable = await adapter.available();
118
+ checks.push({
119
+ name: 'cowork', ok: reachable,
120
+ detail: reachable
121
+ ? 'management socket reachable'
122
+ : `management socket unreachable${coworkConfig ? ` (config: ${coworkConfig})` : ''}`,
123
+ });
124
+ }
125
+ catch (e) {
126
+ checks.push({
127
+ name: 'cowork', ok: false,
128
+ detail: `management socket error${coworkConfig ? ` (config: ${coworkConfig})` : ''}: ${e?.message ?? e}`,
129
+ });
130
+ }
131
+ // Owner CID shape
132
+ const cidOk = /^[0-9a-fA-F]{64}$/.test(rooms.owner.expected_cid);
133
+ checks.push({
134
+ name: 'rooms: owner CID', ok: cidOk,
135
+ detail: cidOk ? 'valid 64-hex CID' : `invalid CID shape: ${rooms.owner.expected_cid.slice(0, 16)}...`,
136
+ });
137
+ // Invite presence (never content)
138
+ checks.push({
139
+ name: 'rooms: owner invite',
140
+ ok: !!loaded.ownerInviteFingerprint,
141
+ detail: loaded.ownerInviteFingerprint
142
+ ? `present (fingerprint: ${loaded.ownerInviteFingerprint.slice(0, 12)}...)`
143
+ : 'not configured — room owner attachment will use waiting_owner_invite',
144
+ });
145
+ // Template validity and referenced role validity
146
+ const { listTemplates, resolveTemplate } = await import('./rooms-tasks/templates.js');
147
+ const customs = loaded.roomTemplates ?? {};
148
+ const templates = listTemplates(customs);
149
+ const roleNames = new Set(roles.map(r => r.name));
150
+ for (const t of templates) {
151
+ const badRefs = t.members
152
+ .filter(m => m.role_ref && !roleNames.has(m.role_ref))
153
+ .map(m => m.role_ref);
154
+ if (badRefs.length) {
155
+ checks.push({
156
+ name: `template: ${t.name}@${t.version}`,
157
+ ok: true,
158
+ detail: `warning: role_ref(s) ${badRefs.join(', ')} not found in configured roles`,
159
+ });
160
+ }
161
+ }
162
+ // Default template validity
163
+ const defaultTemplate = loaded.rooms.defaults?.template
164
+ ?? loaded.tasks?.default_room_template;
165
+ if (defaultTemplate) {
166
+ const resolved = resolveTemplate(defaultTemplate, customs);
167
+ checks.push({
168
+ name: 'rooms: default template',
169
+ ok: !!resolved,
170
+ detail: resolved
171
+ ? `${resolved.name}@${resolved.version}`
172
+ : `template '${defaultTemplate}' not found`,
173
+ });
174
+ }
175
+ // Shared-daemon selection coherence
176
+ if (rooms.cowork?.config) {
177
+ const { existsSync: exists } = await import('node:fs');
178
+ const configOk = exists(rooms.cowork.config);
179
+ checks.push({
180
+ name: 'rooms: cowork config', ok: configOk,
181
+ detail: configOk
182
+ ? `cowork config at ${rooms.cowork.config}`
183
+ : `cowork config not found: ${rooms.cowork.config}`,
184
+ });
185
+ }
186
+ // Hard prerelease capability check
187
+ try {
188
+ const { createCoworkAdapter } = await import('./rooms-tasks/cowork-adapter.js');
189
+ const adapter = createCoworkAdapter({ configPath: coworkConfig });
190
+ const roomList = await adapter.listRooms();
191
+ checks.push({
192
+ name: 'rooms: capability', ok: true,
193
+ detail: `cowork room management operational (${roomList.length} room(s))`,
194
+ });
195
+ }
196
+ catch (e) {
197
+ const msg = e?.message ?? String(e);
198
+ const isProto = msg.includes('protocol') || msg.includes('invalid');
199
+ checks.push({
200
+ name: 'rooms: capability', ok: false,
201
+ detail: isProto
202
+ ? `cowork does not support room management protocol — upgrade ours-cowork to prerelease`
203
+ : `room management check failed: ${msg}`,
204
+ });
205
+ }
206
+ // Stale task/room warnings
207
+ try {
208
+ const { listTasks } = await import('./rooms-tasks/task-state.js');
209
+ const { listRoomRecords } = await import('./rooms-tasks/room-state.js');
210
+ const tasks = listTasks();
211
+ const roomRecords = listRoomRecords();
212
+ const roomIds = new Set(roomRecords.map(r => r.room_id));
213
+ const taskIds = new Set(tasks.map(t => t.task_id));
214
+ const orphanedTasks = tasks.filter(t => t.room_id && !roomIds.has(t.room_id));
215
+ const orphanedRooms = roomRecords.filter(r => r.task_id && !taskIds.has(r.task_id));
216
+ if (orphanedTasks.length)
217
+ checks.push({
218
+ name: 'rooms: stale tasks', ok: true,
219
+ detail: `warning: ${orphanedTasks.length} task(s) reference missing rooms: ${orphanedTasks.map(t => t.task_id).join(', ')}`,
220
+ });
221
+ if (orphanedRooms.length)
222
+ checks.push({
223
+ name: 'rooms: stale rooms', ok: true,
224
+ detail: `warning: ${orphanedRooms.length} room(s) reference missing tasks: ${orphanedRooms.map(r => r.room_id).join(', ')}`,
225
+ });
226
+ }
227
+ catch { /* state dir may not exist yet */ }
228
+ }
109
229
  if (roles.length === 0 || roles.some(role => (role.session ?? 'tmux') === 'tmux')) {
110
230
  const tmux = await exec('tmux', ['-V']);
111
231
  checks.push({
@@ -405,7 +525,11 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
405
525
  function loadConfigResult(configPath, yamlMode) {
406
526
  try {
407
527
  const cfg = loadConfig(configPath, { yamlMode });
408
- return { ok: true, roles: cfg.roles, files: cfg.files, diagnostics: cfg.diagnostics };
528
+ return {
529
+ ok: true, roles: cfg.roles, files: cfg.files, diagnostics: cfg.diagnostics,
530
+ rooms: cfg.rooms, roomTemplates: cfg.roomTemplates, tasks: cfg.tasks,
531
+ ownerInviteFingerprint: cfg.ownerInviteFingerprint,
532
+ };
409
533
  }
410
534
  catch (e) {
411
535
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
@@ -285,6 +285,8 @@ export function makeClaudeCodeAdapter(exec = realExec) {
285
285
  currentIdentityTool: 'current_identity',
286
286
  sendTool: 'send_message',
287
287
  getMessagesTool: 'get_messages',
288
+ listHistoryTool: 'list_history',
289
+ getHistoryItemTool: 'get_history_item',
288
290
  monitorInstruction: id => {
289
291
  const m = armMonitor(id);
290
292
  return `${m.charAt(0).toUpperCase()}${m.slice(1)}.`;
@@ -411,6 +411,8 @@ export function makeCodexAdapter(exec = realExec) {
411
411
  currentIdentityTool: 'current_identity',
412
412
  sendTool: 'send_message',
413
413
  getMessagesTool: 'get_messages',
414
+ listHistoryTool: 'list_history',
415
+ getHistoryItemTool: 'get_history_item',
414
416
  monitorInstruction: (id, configuredRole) => {
415
417
  const consented = configuredRole?.harness_options?.monitor === true;
416
418
  const consent = consented
@@ -67,6 +67,8 @@ export interface BriefingVocab {
67
67
  currentIdentityTool: string;
68
68
  sendTool: string;
69
69
  getMessagesTool: string;
70
+ listHistoryTool: string;
71
+ getHistoryItemTool: string;
70
72
  monitorInstruction(identity: string, role?: ResolvedRole): string;
71
73
  /** Wake-source wording for a role whose monitor is supervisor-owned (monitor.mode=fleet). */
72
74
  supervisedWakeNote(identity: string, role?: ResolvedRole): string;
@@ -217,6 +217,13 @@ export declare class OwnerChannel implements OwnerChannelHandle {
217
217
  * CLI is asked to bounce this very process; neither can happen afterwards.
218
218
  */
219
219
  private restartSelf;
220
+ /**
221
+ * The caller may itself be a room member. Persist and acknowledge acceptance
222
+ * before an external worker starts a saga that can retire this process.
223
+ */
224
+ private closeRoomFromOwner;
225
+ /** Carry a task terminal request through a worker that survives this role. */
226
+ private terminalTaskFromOwner;
220
227
  /** Code-point-safe tail of the worklog, or undefined when there is none. */
221
228
  private readWorklogTail;
222
229
  /** Bound harness-command output to a single outbound message. */
@@ -996,6 +996,8 @@ export class OwnerChannel {
996
996
  return this.commentsState();
997
997
  },
998
998
  fleetList: () => this.fleetOps.list(),
999
+ closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
1000
+ terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
999
1001
  recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
1000
1002
  readWorklogTail: maxChars => this.readWorklogTail(maxChars),
1001
1003
  reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
@@ -1046,6 +1048,51 @@ export class OwnerChannel {
1046
1048
  this.options.log(`[${this.options.role}] owner requested ${command}`);
1047
1049
  await this.fleetOps.restart(mode);
1048
1050
  }
1051
+ /**
1052
+ * The caller may itself be a room member. Persist and acknowledge acceptance
1053
+ * before an external worker starts a saga that can retire this process.
1054
+ */
1055
+ async closeRoomFromOwner(sender, roomId, wireId) {
1056
+ const { acceptManagedRoomClose, recordManagedRoomCloseError } = await import('../rooms-tasks/close.js');
1057
+ const accepted = await acceptManagedRoomClose(roomId);
1058
+ if (accepted.state === 'closed') {
1059
+ await this.send(sender.id, `🔒 Room ${roomId} is already closed`, wireId);
1060
+ this.state.remember(wireId);
1061
+ return;
1062
+ }
1063
+ await this.send(sender.id, `🔒 Room ${roomId} close accepted/pending`, wireId);
1064
+ this.state.remember(wireId);
1065
+ try {
1066
+ await this.fleetOps.closeRoom(roomId);
1067
+ }
1068
+ catch (error) {
1069
+ await recordManagedRoomCloseError(roomId, error instanceof Error ? error.message : String(error), `External close worker failed to start. Retry /room close ${roomId} ${roomId}.`);
1070
+ throw error;
1071
+ }
1072
+ }
1073
+ /** Carry a task terminal request through a worker that survives this role. */
1074
+ async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
1075
+ const { getTask } = await import('../rooms-tasks/task-state.js');
1076
+ const { acceptTaskTerminalIntent, recordTaskTerminalIntentError, } = await import('../rooms-tasks/terminal.js');
1077
+ const task = getTask(taskId);
1078
+ const accepted = await acceptTaskTerminalIntent({
1079
+ taskId, kind, roomId: task.room_id, outcome,
1080
+ });
1081
+ if (accepted.terminal_intent?.status === 'settled') {
1082
+ await this.send(sender.id, `✅ Task ${taskId} → ${accepted.state}`, wireId);
1083
+ this.state.remember(wireId);
1084
+ return;
1085
+ }
1086
+ await this.send(sender.id, `⏳ Task ${taskId} terminal intent accepted/pending`, wireId);
1087
+ this.state.remember(wireId);
1088
+ try {
1089
+ await this.fleetOps.settleTask(taskId);
1090
+ }
1091
+ catch (error) {
1092
+ await recordTaskTerminalIntentError(taskId, error instanceof Error ? error.message : String(error), `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`);
1093
+ throw error;
1094
+ }
1095
+ }
1049
1096
  /** Code-point-safe tail of the worklog, or undefined when there is none. */
1050
1097
  async readWorklogTail(maxChars) {
1051
1098
  try {
@@ -1,4 +1,5 @@
1
1
  import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
2
+ import type { TaskOutcome, TaskTerminalIntent } from '../rooms-tasks/types.js';
2
3
  import { type OwnerCommentsState } from './notices.js';
3
4
  /**
4
5
  * Fleet-level effects a deterministic owner command may trigger. Production
@@ -10,6 +11,10 @@ export interface OwnerFleetOps {
10
11
  restart(mode: 'keep' | 'fresh'): Promise<void>;
11
12
  /** `ours-fleet ls` output. */
12
13
  list(): Promise<string>;
14
+ /** Start a room-close worker outside the caller role's supervisor lifecycle. */
15
+ closeRoom(roomId: string): Promise<void>;
16
+ /** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
17
+ settleTask(taskId: string): Promise<void>;
13
18
  }
14
19
  /**
15
20
  * The narrow capability surface a command executor sees. Everything here is
@@ -40,6 +45,10 @@ export interface OwnerCommandContext {
40
45
  */
41
46
  setComments(enabled: boolean): OwnerCommentsState;
42
47
  fleetList(): Promise<string>;
48
+ /** Persist acceptance, acknowledge it, then launch the external close worker. */
49
+ closeRoom(roomId: string): Promise<void>;
50
+ /** Persist terminal intent, acknowledge it, then launch the external settle worker. */
51
+ terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
43
52
  recentEvents(limit: number): SessionEvent[];
44
53
  readWorklogTail(maxChars: number): Promise<string | undefined>;
45
54
  reply(text: string): Promise<void>;