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

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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.19.0-nightly.1",
3
- "buildId": "6ce6f5e5dd7d",
4
- "commit": "d847df00e6f4a0146633f2533b4daf61004d9c55",
2
+ "version": "0.19.0-nightly.3",
3
+ "buildId": "c969df19c5f2",
4
+ "commit": "1cfdb94ec5bc493c83c72f62dd70c669c78b9e68",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-22T13:27:23.786Z",
6
+ "builtAt": "2026-08-23T10:19:42.625Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -37,6 +37,7 @@ import { WebAccessStore, passwordAccess, validatePublicOrigin } from './web/acce
37
37
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
38
38
  import './harness/claude-code.js'; // registers the claude-code adapter
39
39
  import './harness/codex.js'; // registers the codex adapter
40
+ import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
40
41
  // sudo/su shells lack XDG_RUNTIME_DIR, breaking every systemctl/journalctl
41
42
  // --user child (supervisor commands, logs, doctor). Derive it before dispatch. (#9)
42
43
  deriveXdgRuntimeDir();
@@ -1403,6 +1404,10 @@ function configureWebAccess(opts) {
1403
1404
  + 'or --pairing for protection, or --no-password only for intentional unprotected access');
1404
1405
  return undefined;
1405
1406
  }
1407
+ // ── rooms & tasks ──────────────────────────────────────────────────────────
1408
+ registerTemplateCommands(program, cOpt);
1409
+ registerTaskCommands(program, cOpt);
1410
+ registerRoomCommands(program, cOpt);
1406
1411
  program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
1407
1412
  .option('-c, --configuration <file>')
1408
1413
  .action(async (name, opts) => {
package/dist/config.d.ts CHANGED
@@ -165,6 +165,17 @@ export interface FleetConfig {
165
165
  diagnostics: ConfigDiagnostic[];
166
166
  watchdogs: ResolvedWatchdog[];
167
167
  loops: ResolvedLoop[];
168
+ rooms?: import('./rooms-tasks/types.js').RoomsConfig;
169
+ roomTemplates?: import('./rooms-tasks/types.js').RoomTemplatesConfig;
170
+ tasks?: import('./rooms-tasks/types.js').TasksConfig;
171
+ /** SHA-256 fingerprint of the resolved owner invite (never the invite itself). */
172
+ ownerInviteFingerprint?: string;
173
+ /**
174
+ * Resolved owner invite for immediate private-IPC use only. This property is
175
+ * deliberately non-enumerable so config dumps and JSON output cannot expose
176
+ * the bearer credential.
177
+ */
178
+ ownerInvite?: string;
168
179
  }
169
180
  export declare class ConfigError extends Error {
170
181
  }
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ import { harnessRuntimeDir, resolveIsolation, validateIsolationConfig, } from '.
6
6
  import { getAdapter } from './harness/registry.js';
7
7
  import { resolveWatchdogs } from './watchdog/config.js';
8
8
  import { resolveLoops } from './loops/config.js';
9
+ import { validateRoomsConfig, validateTasksConfig, validateRoomTemplatesConfig, } from './rooms-tasks/config.js';
9
10
  import { CAPABILITIES, CAP_MONITOR_INTERRUPT_AFTER_TOOL } from './capabilities.js';
10
11
  import { runningLabel } from './provenance.js';
11
12
  /** The 8 content-free event types the ours daemon appends to notifications.log. */
@@ -164,9 +165,10 @@ export function loadConfig(configPath, options = {}) {
164
165
  const parsed = parseFleetDocument(p, readFileSync(p, 'utf8'), options.yamlMode);
165
166
  const doc = parsed.value;
166
167
  diagnostics.push(...parsed.diagnostics);
167
- const extra = Object.keys(doc).filter(k => k !== 'roles');
168
+ const FLEET_D_ALLOWED = ['roles', 'rooms', 'room_templates', 'tasks'];
169
+ const extra = Object.keys(doc).filter(k => !FLEET_D_ALLOWED.includes(k));
168
170
  if (extra.length)
169
- throw new ConfigError(`${p}: fleet.d files may only define roles: (found: ${extra.join(', ')})`);
171
+ throw new ConfigError(`${p}: fleet.d files may only define ${FLEET_D_ALLOWED.join(', ')}: (found: ${extra.join(', ')})`);
170
172
  docs.push({ file: p, doc });
171
173
  files.push(p);
172
174
  }
@@ -268,10 +270,49 @@ export function loadConfig(configPath, options = {}) {
268
270
  const resolvedLoops = resolveLoops(baseDoc.loops, base, roles, vars);
269
271
  for (const role of roles)
270
272
  role.loops = resolvedLoops.byRole.get(role.name) ?? [];
271
- return {
273
+ // ── Rooms, room_templates, tasks (split-config merge) ──────────────
274
+ // These sections may appear in the base fleet.yaml and/or in fleet.d files.
275
+ // Base provides defaults; fleet.d extends. Only one source may define rooms/tasks
276
+ // top-level (room_templates merge by name, last writer wins).
277
+ let rooms;
278
+ let ownerInviteFingerprint;
279
+ let ownerInvite;
280
+ let roomTemplates;
281
+ let tasks;
282
+ for (const { file, doc } of docs) {
283
+ if (doc.rooms !== undefined) {
284
+ if (rooms)
285
+ throw new ConfigError(`rooms: defined in multiple files; last: ${file}`);
286
+ const validated = validateRoomsConfig(deepSub(doc.rooms, vars), vars, file);
287
+ ownerInviteFingerprint = validated._invite?.fingerprint;
288
+ ownerInvite = validated._invite?.value;
289
+ const { _invite: _, ...clean } = validated;
290
+ rooms = clean;
291
+ }
292
+ if (doc.room_templates !== undefined) {
293
+ const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
294
+ roomTemplates = { ...(roomTemplates ?? {}), ...validated };
295
+ }
296
+ if (doc.tasks !== undefined) {
297
+ if (tasks)
298
+ throw new ConfigError(`tasks: defined in multiple files; last: ${file}`);
299
+ tasks = validateTasksConfig(deepSub(doc.tasks, vars), file);
300
+ }
301
+ }
302
+ const result = {
272
303
  roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
273
304
  loops: resolvedLoops.loops,
305
+ rooms, roomTemplates, tasks, ownerInviteFingerprint,
274
306
  };
307
+ if (ownerInvite !== undefined) {
308
+ Object.defineProperty(result, 'ownerInvite', {
309
+ value: ownerInvite,
310
+ enumerable: false,
311
+ writable: false,
312
+ configurable: false,
313
+ });
314
+ }
315
+ return result;
275
316
  }
276
317
  /**
277
318
  * Canonical form of a 64-hex container ID for authorization decisions. Hex
@@ -142,6 +142,95 @@ export const ownerCommands = [
142
142
  name: 'version', summary: 'report the fleet version',
143
143
  execute: noArgs('/version', async (ctx) => ctx.reply(`ℹ️ ours-fleet ${ctx.version}`)),
144
144
  },
145
+ {
146
+ name: 'tasks', usage: '/tasks [state]',
147
+ summary: 'list tasks (optionally filter by state)',
148
+ execute: async (ctx, args) => {
149
+ const { listTasks } = await import('../rooms-tasks/task-state.js');
150
+ const stateFilter = args?.trim() || undefined;
151
+ const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
152
+ if (!tasks.length)
153
+ return ctx.reply('📋 No tasks.');
154
+ const lines = tasks.map(t => {
155
+ const blocked = t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : '';
156
+ return `${t.task_id} ${t.state}${blocked} ${t.title}`;
157
+ });
158
+ await ctx.reply(tail(`📋 Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
159
+ },
160
+ },
161
+ {
162
+ name: 'task', usage: '/task <id>',
163
+ summary: 'show task details',
164
+ execute: async (ctx, args) => {
165
+ if (!args)
166
+ throw new OwnerCommandUsageError('usage: /task <id>');
167
+ const { getTask } = await import('../rooms-tasks/task-state.js');
168
+ try {
169
+ const t = getTask(args.trim());
170
+ const lines = [
171
+ `📋 Task: ${t.task_id}`,
172
+ `Title: ${t.title}`,
173
+ `State: ${t.state}${t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : ''}`,
174
+ ...(t.template ? [`Template: ${t.template.name}@${t.template.version}`] : []),
175
+ ...(t.room_id ? [`Room: ${t.room_id}`] : []),
176
+ `Origin: ${t.origin.type}`,
177
+ `Created: ${t.created_at}`,
178
+ ];
179
+ await ctx.reply(lines.join('\n'));
180
+ }
181
+ catch (e) {
182
+ await ctx.reply(`⚠️ ${e instanceof Error ? e.message : String(e)}`);
183
+ }
184
+ },
185
+ },
186
+ {
187
+ name: 'rooms', summary: 'list rooms',
188
+ execute: noArgs('/rooms', async (ctx) => {
189
+ const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
190
+ const rooms = listRoomRecords();
191
+ if (!rooms.length)
192
+ return ctx.reply('🏠 No rooms.');
193
+ const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
194
+ await ctx.reply(tail(`🏠 Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
195
+ }),
196
+ },
197
+ {
198
+ name: 'room', usage: '/room <id>',
199
+ summary: 'show room details',
200
+ execute: async (ctx, args) => {
201
+ if (!args)
202
+ throw new OwnerCommandUsageError('usage: /room <id>');
203
+ const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
204
+ const r = getRoomRecord(args.trim());
205
+ if (!r)
206
+ return ctx.reply(`⚠️ room not found: ${args.trim()}`);
207
+ const lines = [
208
+ `🏠 Room: ${r.room_id}`,
209
+ `Name: ${r.room_name}`,
210
+ `State: ${r.state}`,
211
+ `Saga: ${r.saga.phase} (step ${r.saga.step_index})`,
212
+ ...(r.task_id ? [`Task: ${r.task_id}`] : []),
213
+ ...(r.provisioning_detail ? [`Detail: ${r.provisioning_detail}`] : []),
214
+ ...(r.saga.error ? [`Error: ${r.saga.error}`] : []),
215
+ `Created: ${r.created_at}`,
216
+ ];
217
+ await ctx.reply(lines.join('\n'));
218
+ },
219
+ },
220
+ {
221
+ name: 'templates', summary: 'list available room templates',
222
+ execute: noArgs('/templates', async (ctx) => {
223
+ const { listTemplates } = await import('../rooms-tasks/templates.js');
224
+ const templates = listTemplates({});
225
+ if (!templates.length)
226
+ return ctx.reply('📐 No templates.');
227
+ const lines = templates.map(t => {
228
+ const tag = t.builtin ? ' (built-in)' : '';
229
+ return `${t.name}@${t.version}${tag} ${t.description}`;
230
+ });
231
+ await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
232
+ }),
233
+ },
145
234
  ];
146
235
  /** Trimmed slash-prefixed text is a command attempt and is never forwarded. */
147
236
  export const isOwnerCommandText = (text) => text.trim().startsWith('/');
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerTemplateCommands(parent: Command, cOpt: (cmd: Command) => Command): void;
3
+ export declare function registerTaskCommands(parent: Command, cOpt: (cmd: Command) => Command): void;
4
+ export declare function registerRoomCommands(parent: Command, cOpt: (cmd: Command) => Command): void;