@ours.network/fleet 0.19.0-nightly.2 → 0.19.0-nightly.4
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.
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +12 -1
- package/dist/doctor.js +125 -1
- package/dist/owner-channel/commands.js +324 -25
- package/dist/rooms-tasks/cli.js +309 -74
- package/dist/rooms-tasks/cowork-adapter.d.ts +21 -24
- package/dist/rooms-tasks/cowork-adapter.js +257 -33
- package/dist/rooms-tasks/index.d.ts +1 -0
- package/dist/rooms-tasks/index.js +1 -0
- package/dist/rooms-tasks/provision.d.ts +21 -0
- package/dist/rooms-tasks/provision.js +229 -0
- package/dist/rooms-tasks/room-state.d.ts +2 -0
- package/dist/rooms-tasks/room-state.js +2 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +1 -0
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.19.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "0.19.0-nightly.4",
|
|
3
|
+
"buildId": "27ab2ac6d2c0",
|
|
4
|
+
"commit": "0213a2667a2d24e14297f7364f42880997451742",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-23T12:52:41.918Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/config.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export interface FleetConfig {
|
|
|
170
170
|
tasks?: import('./rooms-tasks/types.js').TasksConfig;
|
|
171
171
|
/** SHA-256 fingerprint of the resolved owner invite (never the invite itself). */
|
|
172
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;
|
|
173
179
|
}
|
|
174
180
|
export declare class ConfigError extends Error {
|
|
175
181
|
}
|
package/dist/config.js
CHANGED
|
@@ -276,6 +276,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
276
276
|
// top-level (room_templates merge by name, last writer wins).
|
|
277
277
|
let rooms;
|
|
278
278
|
let ownerInviteFingerprint;
|
|
279
|
+
let ownerInvite;
|
|
279
280
|
let roomTemplates;
|
|
280
281
|
let tasks;
|
|
281
282
|
for (const { file, doc } of docs) {
|
|
@@ -284,6 +285,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
284
285
|
throw new ConfigError(`rooms: defined in multiple files; last: ${file}`);
|
|
285
286
|
const validated = validateRoomsConfig(deepSub(doc.rooms, vars), vars, file);
|
|
286
287
|
ownerInviteFingerprint = validated._invite?.fingerprint;
|
|
288
|
+
ownerInvite = validated._invite?.value;
|
|
287
289
|
const { _invite: _, ...clean } = validated;
|
|
288
290
|
rooms = clean;
|
|
289
291
|
}
|
|
@@ -297,11 +299,20 @@ export function loadConfig(configPath, options = {}) {
|
|
|
297
299
|
tasks = validateTasksConfig(deepSub(doc.tasks, vars), file);
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
|
-
|
|
302
|
+
const result = {
|
|
301
303
|
roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
302
304
|
loops: resolvedLoops.loops,
|
|
303
305
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
304
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;
|
|
305
316
|
}
|
|
306
317
|
/**
|
|
307
318
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
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 {
|
|
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) };
|
|
@@ -159,14 +159,20 @@ export const ownerCommands = [
|
|
|
159
159
|
},
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
name: 'task',
|
|
163
|
-
|
|
162
|
+
name: 'task',
|
|
163
|
+
usage: '/task <create|list|show|start|block|unblock|review|done|cancel|recover> ...',
|
|
164
|
+
summary: 'task lifecycle subcommands',
|
|
164
165
|
execute: async (ctx, args) => {
|
|
165
166
|
if (!args)
|
|
166
|
-
throw new OwnerCommandUsageError('usage: /task <id>');
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
throw new OwnerCommandUsageError('usage: /task <subcommand> <id>');
|
|
168
|
+
const argLines = args.trim().split('\n');
|
|
169
|
+
const firstLineTokens = argLines[0].trim().split(/\s+/);
|
|
170
|
+
const sub = firstLineTokens[0];
|
|
171
|
+
const rest = firstLineTokens.slice(1);
|
|
172
|
+
const trailingLines = argLines.slice(1).join('\n').trim() || undefined;
|
|
173
|
+
const showTask = async (id) => {
|
|
174
|
+
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
175
|
+
const t = getTask(id);
|
|
170
176
|
const lines = [
|
|
171
177
|
`📋 Task: ${t.task_id}`,
|
|
172
178
|
`Title: ${t.title}`,
|
|
@@ -177,8 +183,157 @@ export const ownerCommands = [
|
|
|
177
183
|
`Created: ${t.created_at}`,
|
|
178
184
|
];
|
|
179
185
|
await ctx.reply(lines.join('\n'));
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
switch (sub) {
|
|
189
|
+
case 'create': {
|
|
190
|
+
if (!rest[0])
|
|
191
|
+
throw new OwnerCommandUsageError('usage: /task create [--backlog] [--template=<name>|--no-room] <title>');
|
|
192
|
+
const backlog = rest.includes('--backlog');
|
|
193
|
+
const tplFlag = rest.find(r => r.startsWith('--template='));
|
|
194
|
+
const template = tplFlag ? tplFlag.slice('--template='.length) : undefined;
|
|
195
|
+
const noRoom = rest.includes('--no-room');
|
|
196
|
+
const titleParts = rest.filter(r => r !== '--backlog' && !r.startsWith('--template=') && r !== '--no-room');
|
|
197
|
+
if (!titleParts.length)
|
|
198
|
+
throw new OwnerCommandUsageError('usage: /task create [--backlog] [--template=<name>|--no-room] <title>');
|
|
199
|
+
const { createTask } = await import('../rooms-tasks/task-state.js');
|
|
200
|
+
const t = createTask({
|
|
201
|
+
title: titleParts.join(' '),
|
|
202
|
+
origin: { type: 'owner_channel' },
|
|
203
|
+
start: !backlog,
|
|
204
|
+
...(template ? { template: { name: template, version: 1, content_hash: '' } } : {}),
|
|
205
|
+
...(noRoom ? { no_room: true } : {}),
|
|
206
|
+
...(trailingLines ? { brief: trailingLines } : {}),
|
|
207
|
+
});
|
|
208
|
+
const lines = [`Task ${t.task_id} created · ${t.state}`];
|
|
209
|
+
if (template)
|
|
210
|
+
lines.push(`Template: ${template}`);
|
|
211
|
+
if (noRoom)
|
|
212
|
+
lines.push('No room requested');
|
|
213
|
+
await ctx.reply(lines.join('\n'));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'list': {
|
|
217
|
+
const { listTasks } = await import('../rooms-tasks/task-state.js');
|
|
218
|
+
const filter = rest[0];
|
|
219
|
+
const stateMap = {
|
|
220
|
+
backlog: 'backlog', active: 'active', blocked: 'active',
|
|
221
|
+
done: 'done', all: ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'],
|
|
222
|
+
};
|
|
223
|
+
const stateFilter = filter && stateMap[filter]
|
|
224
|
+
? { state: stateMap[filter] }
|
|
225
|
+
: undefined;
|
|
226
|
+
const tasks = listTasks(stateFilter);
|
|
227
|
+
if (filter === 'blocked') {
|
|
228
|
+
const blocked = tasks.filter(t => t.blocked);
|
|
229
|
+
if (!blocked.length) {
|
|
230
|
+
await ctx.reply('📋 No blocked tasks.');
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
const lines = blocked.map(t => `${t.task_id} ${t.state} ${t.title} [BLOCKED: ${t.blocked.reason}]`);
|
|
234
|
+
await ctx.reply(tail(`📋 Blocked tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (!tasks.length) {
|
|
238
|
+
await ctx.reply('📋 No tasks.');
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
const lines = tasks.map(t => `${t.task_id} ${t.state} ${t.title}${t.blocked ? ` [BLOCKED]` : ''}`);
|
|
242
|
+
await ctx.reply(tail(`📋 Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'show': {
|
|
246
|
+
if (!rest[0])
|
|
247
|
+
throw new OwnerCommandUsageError('usage: /task show <id>');
|
|
248
|
+
await showTask(rest[0]);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case 'start': {
|
|
252
|
+
if (!rest[0])
|
|
253
|
+
throw new OwnerCommandUsageError('usage: /task start <id>');
|
|
254
|
+
const { startTask } = await import('../rooms-tasks/task-state.js');
|
|
255
|
+
const t = startTask(rest[0]);
|
|
256
|
+
await ctx.reply(`✅ Task ${t.task_id} → provisioning`);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case 'block': {
|
|
260
|
+
if (!rest[0] || !rest[1])
|
|
261
|
+
throw new OwnerCommandUsageError('usage: /task block <id> <reason>');
|
|
262
|
+
const { blockTask } = await import('../rooms-tasks/task-state.js');
|
|
263
|
+
const reason = rest.slice(1).join(' ');
|
|
264
|
+
const t = blockTask(rest[0], reason);
|
|
265
|
+
await ctx.reply(`🚧 Task ${t.task_id} blocked: ${reason}`);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case 'unblock': {
|
|
269
|
+
if (!rest[0])
|
|
270
|
+
throw new OwnerCommandUsageError('usage: /task unblock <id>');
|
|
271
|
+
const { unblockTask } = await import('../rooms-tasks/task-state.js');
|
|
272
|
+
const t = unblockTask(rest[0]);
|
|
273
|
+
await ctx.reply(`✅ Task ${t.task_id} unblocked`);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case 'review': {
|
|
277
|
+
if (!rest[0])
|
|
278
|
+
throw new OwnerCommandUsageError('usage: /task review <id>');
|
|
279
|
+
const { reviewTask } = await import('../rooms-tasks/task-state.js');
|
|
280
|
+
const t = reviewTask(rest[0]);
|
|
281
|
+
await ctx.reply(`📝 Task ${t.task_id} → review`);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case 'done': {
|
|
285
|
+
if (!rest[0])
|
|
286
|
+
throw new OwnerCommandUsageError('usage: /task done <id> [summary]');
|
|
287
|
+
const { completeTask } = await import('../rooms-tasks/task-state.js');
|
|
288
|
+
const summary = rest.slice(1).join(' ') || undefined;
|
|
289
|
+
const t = completeTask(rest[0], summary ? { summary } : undefined);
|
|
290
|
+
await ctx.reply(`✅ Task ${t.task_id} → done`);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'cancel': {
|
|
294
|
+
if (rest.length < 2 || rest[0] !== rest[1])
|
|
295
|
+
throw new OwnerCommandUsageError('destructive: /task cancel <id> <id> — provide the task ID twice');
|
|
296
|
+
const { cancelTask } = await import('../rooms-tasks/task-state.js');
|
|
297
|
+
const t = cancelTask(rest[0]);
|
|
298
|
+
await ctx.reply(`🗑️ Task ${t.task_id} → cancelled`);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 'recover': {
|
|
302
|
+
if (!rest[0])
|
|
303
|
+
throw new OwnerCommandUsageError('usage: /task recover <id>');
|
|
304
|
+
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
305
|
+
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
306
|
+
const t = getTask(rest[0]);
|
|
307
|
+
const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
308
|
+
const hints = [];
|
|
309
|
+
if (t.state === 'provisioning' && room) {
|
|
310
|
+
if (room.provisioning_detail === 'waiting_cowork')
|
|
311
|
+
hints.push('Cowork socket unreachable');
|
|
312
|
+
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
313
|
+
hints.push('Owner invite missing or invalid');
|
|
314
|
+
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
315
|
+
hints.push('Owner CID mismatch');
|
|
316
|
+
if (room.provisioning_detail === 'member_failed')
|
|
317
|
+
hints.push(`Member failed at step ${room.saga.step_index}`);
|
|
318
|
+
}
|
|
319
|
+
const lines = [`Task ${t.task_id} · ${t.state}`];
|
|
320
|
+
if (room)
|
|
321
|
+
lines.push(`Room ${room.room_id} · ${room.state} · saga: ${room.saga.phase}`);
|
|
322
|
+
if (hints.length)
|
|
323
|
+
lines.push(`Hints: ${hints.join('; ')}`);
|
|
324
|
+
else
|
|
325
|
+
lines.push('No automated recovery actions available.');
|
|
326
|
+
await ctx.reply(lines.join('\n'));
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
default:
|
|
330
|
+
// bare /task <id> → show
|
|
331
|
+
await showTask(sub);
|
|
332
|
+
}
|
|
180
333
|
}
|
|
181
334
|
catch (e) {
|
|
335
|
+
if (e instanceof OwnerCommandUsageError)
|
|
336
|
+
throw e;
|
|
182
337
|
await ctx.reply(`⚠️ ${e instanceof Error ? e.message : String(e)}`);
|
|
183
338
|
}
|
|
184
339
|
},
|
|
@@ -195,30 +350,125 @@ export const ownerCommands = [
|
|
|
195
350
|
}),
|
|
196
351
|
},
|
|
197
352
|
{
|
|
198
|
-
name: 'room',
|
|
199
|
-
|
|
353
|
+
name: 'room',
|
|
354
|
+
usage: '/room <create|list|show|close|recover> ...',
|
|
355
|
+
summary: 'room lifecycle subcommands',
|
|
200
356
|
execute: async (ctx, args) => {
|
|
201
357
|
if (!args)
|
|
202
|
-
throw new OwnerCommandUsageError('usage: /room <id>');
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
358
|
+
throw new OwnerCommandUsageError('usage: /room <subcommand> <id>');
|
|
359
|
+
const argLines = args.trim().split('\n');
|
|
360
|
+
const firstLineTokens = argLines[0].trim().split(/\s+/);
|
|
361
|
+
const sub = firstLineTokens[0];
|
|
362
|
+
const rest = firstLineTokens.slice(1);
|
|
363
|
+
const roomTrailingLines = argLines.slice(1).join('\n').trim() || undefined;
|
|
364
|
+
const showRoom = async (id) => {
|
|
365
|
+
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
366
|
+
const r = getRoomRecord(id);
|
|
367
|
+
if (!r)
|
|
368
|
+
return ctx.reply(`⚠️ room not found: ${id}`);
|
|
369
|
+
const lines = [
|
|
370
|
+
`🏠 Room: ${r.room_id}`,
|
|
371
|
+
`Name: ${r.room_name}`,
|
|
372
|
+
`State: ${r.state}`,
|
|
373
|
+
`Saga: ${r.saga.phase} (step ${r.saga.step_index})`,
|
|
374
|
+
...(r.task_id ? [`Task: ${r.task_id}`] : []),
|
|
375
|
+
...(r.provisioning_detail ? [`Detail: ${r.provisioning_detail}`] : []),
|
|
376
|
+
...(r.saga.error ? [`Error: ${r.saga.error}`] : []),
|
|
377
|
+
`Created: ${r.created_at}`,
|
|
378
|
+
];
|
|
379
|
+
await ctx.reply(lines.join('\n'));
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
switch (sub) {
|
|
383
|
+
case 'create': {
|
|
384
|
+
const tplFlag = rest.find(r => r.startsWith('--template='));
|
|
385
|
+
const templateName = tplFlag?.slice('--template='.length);
|
|
386
|
+
const nameParts = rest.filter(r => !r.startsWith('--'));
|
|
387
|
+
if (!nameParts.length)
|
|
388
|
+
throw new OwnerCommandUsageError('usage: /room create --template=<name> <room name>');
|
|
389
|
+
const { randomUUID } = await import('node:crypto');
|
|
390
|
+
const { createRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
391
|
+
const { resolveTemplate, snapshotTemplate } = await import('../rooms-tasks/templates.js');
|
|
392
|
+
let templateSnapshot;
|
|
393
|
+
if (templateName) {
|
|
394
|
+
const tpl = resolveTemplate(templateName, {});
|
|
395
|
+
if (!tpl)
|
|
396
|
+
throw new OwnerCommandUsageError(`unknown template: ${templateName}`);
|
|
397
|
+
templateSnapshot = snapshotTemplate(tpl);
|
|
398
|
+
}
|
|
399
|
+
const r = createRoomRecord({
|
|
400
|
+
room_id: randomUUID().replace(/-/g, '').slice(0, 16),
|
|
401
|
+
room_name: nameParts.join(' '),
|
|
402
|
+
...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
|
|
403
|
+
...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
|
|
404
|
+
});
|
|
405
|
+
const lines = [`🏠 Room ${r.room_id} created · ${r.state}`];
|
|
406
|
+
if (templateName)
|
|
407
|
+
lines.push(`Template: ${templateName}`);
|
|
408
|
+
await ctx.reply(lines.join('\n'));
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case 'list': {
|
|
412
|
+
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
413
|
+
const filter = rest[0];
|
|
414
|
+
let rooms = listRoomRecords();
|
|
415
|
+
if (filter === 'active')
|
|
416
|
+
rooms = rooms.filter(r => r.state === 'active');
|
|
417
|
+
if (!rooms.length) {
|
|
418
|
+
await ctx.reply('🏠 No rooms.');
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
422
|
+
await ctx.reply(tail(`🏠 Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case 'show': {
|
|
426
|
+
if (!rest[0])
|
|
427
|
+
throw new OwnerCommandUsageError('usage: /room show <id>');
|
|
428
|
+
await showRoom(rest[0]);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case 'close': {
|
|
432
|
+
if (rest.length < 2 || rest[0] !== rest[1])
|
|
433
|
+
throw new OwnerCommandUsageError('destructive: /room close <id> <id> — provide the room ID twice');
|
|
434
|
+
const { closeRoom } = await import('../rooms-tasks/room-state.js');
|
|
435
|
+
const r = closeRoom(rest[0]);
|
|
436
|
+
await ctx.reply(`🔒 Room ${r.room_id} → closed`);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case 'recover': {
|
|
440
|
+
if (!rest[0])
|
|
441
|
+
throw new OwnerCommandUsageError('usage: /room recover <id>');
|
|
442
|
+
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
443
|
+
const r = getRoomRecord(rest[0]);
|
|
444
|
+
if (!r) {
|
|
445
|
+
await ctx.reply(`⚠️ room not found: ${rest[0]}`);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
const lines = [`🏠 Room ${r.room_id} · ${r.state} · saga: ${r.saga.phase}`];
|
|
449
|
+
if (r.saga.error)
|
|
450
|
+
lines.push(`Error: ${r.saga.error}`);
|
|
451
|
+
if (r.provisioning_detail)
|
|
452
|
+
lines.push(`Detail: ${r.provisioning_detail}`);
|
|
453
|
+
lines.push(r.saga.error ? 'Run `ours-fleet room recover` from CLI for full recovery.' : 'No recovery needed.');
|
|
454
|
+
await ctx.reply(lines.join('\n'));
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
default:
|
|
458
|
+
// bare /room <id> → show
|
|
459
|
+
await showRoom(sub);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
if (e instanceof OwnerCommandUsageError)
|
|
464
|
+
throw e;
|
|
465
|
+
await ctx.reply(`⚠️ ${e instanceof Error ? e.message : String(e)}`);
|
|
466
|
+
}
|
|
218
467
|
},
|
|
219
468
|
},
|
|
220
469
|
{
|
|
221
|
-
name: 'templates',
|
|
470
|
+
name: 'templates', aliases: ['template-list'],
|
|
471
|
+
summary: 'list available room templates',
|
|
222
472
|
execute: noArgs('/templates', async (ctx) => {
|
|
223
473
|
const { listTemplates } = await import('../rooms-tasks/templates.js');
|
|
224
474
|
const templates = listTemplates({});
|
|
@@ -231,6 +481,55 @@ export const ownerCommands = [
|
|
|
231
481
|
await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
|
|
232
482
|
}),
|
|
233
483
|
},
|
|
484
|
+
{
|
|
485
|
+
name: 'template',
|
|
486
|
+
usage: '/template <show|list> <name[@version]>',
|
|
487
|
+
summary: 'template subcommands (show, list)',
|
|
488
|
+
execute: async (ctx, args) => {
|
|
489
|
+
if (!args)
|
|
490
|
+
throw new OwnerCommandUsageError('usage: /template show <name[@version]>');
|
|
491
|
+
const parts = args.trim().split(/\s+/);
|
|
492
|
+
const sub = parts[0];
|
|
493
|
+
const showTemplate = async (nameStr) => {
|
|
494
|
+
const { resolveTemplate } = await import('../rooms-tasks/templates.js');
|
|
495
|
+
const t = resolveTemplate(nameStr, {});
|
|
496
|
+
if (!t)
|
|
497
|
+
return ctx.reply(`⚠️ template not found: ${nameStr}`);
|
|
498
|
+
const lines = [
|
|
499
|
+
`📐 Template: ${t.name}@${t.version}`,
|
|
500
|
+
`Description: ${t.description}`,
|
|
501
|
+
...(t.builtin ? ['Source: built-in'] : []),
|
|
502
|
+
...(t.contract ? [`Contract: ${t.contract}`] : []),
|
|
503
|
+
'Members:',
|
|
504
|
+
...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} → role_ref: ${m.role_ref}`),
|
|
505
|
+
];
|
|
506
|
+
await ctx.reply(lines.join('\n'));
|
|
507
|
+
};
|
|
508
|
+
switch (sub) {
|
|
509
|
+
case 'show': {
|
|
510
|
+
if (!parts[1])
|
|
511
|
+
throw new OwnerCommandUsageError('usage: /template show <name[@version]>');
|
|
512
|
+
await showTemplate(parts[1]);
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case 'list': {
|
|
516
|
+
const { listTemplates } = await import('../rooms-tasks/templates.js');
|
|
517
|
+
const templates = listTemplates({});
|
|
518
|
+
if (!templates.length)
|
|
519
|
+
return ctx.reply('📐 No templates.');
|
|
520
|
+
const lines = templates.map(t => {
|
|
521
|
+
const tag = t.builtin ? ' (built-in)' : '';
|
|
522
|
+
return `${t.name}@${t.version}${tag} ${t.description}`;
|
|
523
|
+
});
|
|
524
|
+
await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
default:
|
|
528
|
+
// bare /template <name> → show
|
|
529
|
+
await showTemplate(sub);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
},
|
|
234
533
|
];
|
|
235
534
|
/** Trimmed slash-prefixed text is a command attempt and is never forwarded. */
|
|
236
535
|
export const isOwnerCommandText = (text) => text.trim().startsWith('/');
|