@ours.network/fleet 2.2.6 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/task-room-service.d.ts +11 -0
- package/dist/application/task-room-service.js +37 -14
- package/dist/atomic-file.d.ts +2 -0
- package/dist/atomic-file.js +2 -1
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +6 -3
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +37 -0
- package/dist/owner-channel/commands.js +25 -12
- package/dist/reports/tasks.js +4 -3
- package/dist/rooms-tasks/add-member.js +17 -6
- package/dist/rooms-tasks/cli.js +79 -5
- package/dist/rooms-tasks/config.js +11 -0
- package/dist/rooms-tasks/layout-member-retirement.d.ts +15 -0
- package/dist/rooms-tasks/layout-member-retirement.js +26 -1
- package/dist/rooms-tasks/layout-service.d.ts +2 -1
- package/dist/rooms-tasks/layout-service.js +10 -4
- package/dist/rooms-tasks/layout.d.ts +4 -2
- package/dist/rooms-tasks/layout.js +21 -4
- package/dist/rooms-tasks/member-naming.d.ts +43 -0
- package/dist/rooms-tasks/member-naming.js +186 -0
- package/dist/rooms-tasks/member-overrides.js +1 -0
- package/dist/rooms-tasks/mission-keys.d.ts +83 -0
- package/dist/rooms-tasks/mission-keys.js +250 -0
- package/dist/rooms-tasks/provision.d.ts +4 -0
- package/dist/rooms-tasks/provision.js +37 -3
- package/dist/rooms-tasks/task-lists.d.ts +18 -2
- package/dist/rooms-tasks/task-lists.js +60 -5
- package/dist/rooms-tasks/task-room-name.d.ts +7 -2
- package/dist/rooms-tasks/task-room-name.js +13 -4
- package/dist/rooms-tasks/task-state.d.ts +40 -0
- package/dist/rooms-tasks/task-state.js +288 -45
- package/dist/rooms-tasks/types.d.ts +35 -1
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/web/runtime.js +4 -1
- package/dist/web/server.js +62 -27
- package/package.json +1 -1
|
@@ -5,13 +5,13 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
6
6
|
import { canonicalJson } from '../canonical-json.js';
|
|
7
7
|
import { stateRoot } from '../paths.js';
|
|
8
|
-
import { getTask, TaskStateError } from './task-state.js';
|
|
8
|
+
import { allocateTaskMemberNames, getTask, TaskStateError } from './task-state.js';
|
|
9
9
|
import { getRoomRecord } from './room-state.js';
|
|
10
10
|
import { prepareExecutionPlan } from './member-overrides.js';
|
|
11
11
|
import { sealTemplateSnapshot } from './templates.js';
|
|
12
|
-
import { provisionMembers } from './provision.js';
|
|
12
|
+
import { assertMemberNameAvailable, provisionMembers } from './provision.js';
|
|
13
13
|
import { EMPTY_TASK_LAYOUT, EMPTY_TASK_ROOM } from './empty-layout.js';
|
|
14
|
-
import {
|
|
14
|
+
import { allocateLayoutMemberName } from './layout-member-retirement.js';
|
|
15
15
|
import { validLayoutKey } from './layout-config.js';
|
|
16
16
|
export class MemberAdditionRejected extends TaskStateError {
|
|
17
17
|
accepted = false;
|
|
@@ -71,7 +71,11 @@ export async function requestMemberAddition(input) {
|
|
|
71
71
|
}
|
|
72
72
|
})();
|
|
73
73
|
const receipt = { taskId, requestId: r.requestId,
|
|
74
|
-
roleId:
|
|
74
|
+
roleId: allocateLayoutMemberName(task.layout.run_id, r.slot, [
|
|
75
|
+
{ label: r.role, source: 'role_override' },
|
|
76
|
+
{ label: input.cfg.agentTemplateRoleRefs?.[r.agentTemplate], source: 'template_ref' },
|
|
77
|
+
{ label: r.slot, source: 'participant_key' }
|
|
78
|
+
]), hash, state: 'running' };
|
|
75
79
|
const save = () => { replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600); publishFleetResourceCommit(path); };
|
|
76
80
|
save();
|
|
77
81
|
running.add(path);
|
|
@@ -89,7 +93,14 @@ export async function requestMemberAddition(input) {
|
|
|
89
93
|
if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || room.state !== 'active'
|
|
90
94
|
|| room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
|
|
91
95
|
throw new MemberAdditionRejected('Adding an agent requires an active task room');
|
|
92
|
-
|
|
96
|
+
// Keyed tasks take the next durable role ordinal; unkeyed tasks keep the legacy one-per-slot name.
|
|
97
|
+
const allocationKey = `add:${r.requestId}`;
|
|
98
|
+
const roleId = allocateTaskMemberNames(taskId, [{ allocationKey, candidates: [
|
|
99
|
+
{ label: r.role, source: 'role_override' },
|
|
100
|
+
{ label: input.cfg.agentTemplateRoleRefs?.[r.agentTemplate], source: 'template_ref' },
|
|
101
|
+
{ label: r.role, source: 'room_role' }, { label: r.slot, source: 'slot' }
|
|
102
|
+
] }], assertMemberNameAvailable)?.[0]
|
|
103
|
+
?? `${taskId.slice(0, 8)}-${r.slot}-1`;
|
|
93
104
|
if (room.member_seats.some(s => s.role_name === roleId))
|
|
94
105
|
throw new MemberAdditionRejected('This member slot already exists');
|
|
95
106
|
const plan = (() => {
|
|
@@ -107,7 +118,7 @@ export async function requestMemberAddition(input) {
|
|
|
107
118
|
save();
|
|
108
119
|
running.add(path);
|
|
109
120
|
void (input.provision ?? provisionMembers)({ cfg: input.cfg, cowork: input.cowork, configPath: input.configPath, binPath: input.binPath,
|
|
110
|
-
taskId, roomId: room.room_id, template, goal: task.title, brief: task.brief, append: true }).then(result => {
|
|
121
|
+
taskId, roomId: room.room_id, template, goal: task.title, brief: task.brief, append: true, allocationKey }).then(result => {
|
|
111
122
|
const seat = result.member_seats.find(s => s.role_name === roleId);
|
|
112
123
|
receipt.state = seat?.seat_state === 'active' && getTask(taskId).member_roles.some(m => m.name === roleId) ? 'succeeded' : 'attention';
|
|
113
124
|
if (receipt.state === 'attention')
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ function cliAnonymousOverride(argv) {
|
|
|
39
39
|
throw new Error('--anonymous and --no-anonymous are mutually exclusive');
|
|
40
40
|
return enabled ? true : disabled ? false : undefined;
|
|
41
41
|
}
|
|
42
|
-
import { getTask, getDeletingTask, TaskStateError, reviewRevision, } from './task-state.js';
|
|
42
|
+
import { getTask, getDeletingTask, resolveTaskRef, TaskStateError, reviewRevision, } from './task-state.js';
|
|
43
43
|
import { getRoomRecord, RoomStateError, } from './room-state.js';
|
|
44
44
|
import { createCoworkAdapter } from './cowork-adapter.js';
|
|
45
45
|
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
|
|
@@ -212,6 +212,17 @@ function isKnownRoomStateMessage(message) {
|
|
|
212
212
|
new RegExp(`^room ${SAFE_ID_WORD} close cannot move backward to [A-Za-z_]+$`, 'u'),
|
|
213
213
|
].some(pattern => pattern.test(message));
|
|
214
214
|
}
|
|
215
|
+
/** Canonical ID for a CLI task argument holding an ID or a mission key. */
|
|
216
|
+
function cliTaskRef(ref, json) {
|
|
217
|
+
try {
|
|
218
|
+
return resolveTaskRef(ref);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (json)
|
|
222
|
+
die(error);
|
|
223
|
+
dieTaskRoom(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
215
226
|
function dieTaskRoom(e) {
|
|
216
227
|
if (e instanceof TaskRoomApplicationError)
|
|
217
228
|
e = taskRoomPublicError(e.code, e.fields);
|
|
@@ -282,7 +293,7 @@ function dieTaskRoom(e) {
|
|
|
282
293
|
}
|
|
283
294
|
const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
|
|
284
295
|
icon: '📋', title, empty: 'No tasks found.',
|
|
285
|
-
records: tasks.map(t => `${taskStatus(t.state)} ${markdownCode(t.task_id)} — ${markdownProse(t.title)}`
|
|
296
|
+
records: tasks.map(t => `${taskStatus(t.state)} ${t.key ? `${markdownCode(t.key)} ` : ''}${markdownCode(t.task_id)} — ${markdownProse(t.title)}`
|
|
286
297
|
+ ` — List ${markdownCode(t.list_name ?? 'default')}`
|
|
287
298
|
+ (t.deletion?.status === 'pending' ? ' — Deletion pending'
|
|
288
299
|
+ (t.deletion.error ? `: ${markdownProse(t.deletion.error)}` : '') : '')
|
|
@@ -295,6 +306,7 @@ const taskActionMarkdown = (title, task, fields = []) => {
|
|
|
295
306
|
return renderMarkdownResult({
|
|
296
307
|
icon: '📋', title,
|
|
297
308
|
fields: [
|
|
309
|
+
...(task.key ? [{ label: 'Key', value: task.key, kind: 'code' }] : []),
|
|
298
310
|
{ label: 'ID', value: task.task_id, kind: 'code' },
|
|
299
311
|
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
300
312
|
{ label: 'List', value: task.list_name ?? 'default', kind: 'code' },
|
|
@@ -800,16 +812,55 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
800
812
|
}
|
|
801
813
|
});
|
|
802
814
|
taskCmd.command('list-create <name>')
|
|
803
|
-
.description('create a named task list')
|
|
815
|
+
.description('create a named task list; its mission keys read <PREFIX>-1, <PREFIX>-2, ...')
|
|
816
|
+
.option('--prefix <prefix>', 'mission key prefix: 1-6 Latin letters or digits starting with a letter (default: suggested from the name)')
|
|
804
817
|
.option('--json', 'JSON output')
|
|
805
818
|
.action(async (name, opts) => {
|
|
806
819
|
try {
|
|
807
|
-
const list = await taskRoomService().createTaskList({ actor: { kind: 'local_control', surface: 'cli' }, name });
|
|
820
|
+
const list = await taskRoomService().createTaskList({ actor: { kind: 'local_control', surface: 'cli' }, name, prefix: opts.prefix });
|
|
821
|
+
if (opts.json) {
|
|
822
|
+
console.log(JSON.stringify({ schema_version: 1, list }, null, 2));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
console.log(renderMarkdownResult({ icon: '📚', title: 'Task list created', fields: [{ label: 'Name', value: list.name },
|
|
826
|
+
{ label: 'Prefix', value: list.prefix ?? '', kind: 'code' }] }));
|
|
827
|
+
}
|
|
828
|
+
catch (e) {
|
|
829
|
+
if (opts.json)
|
|
830
|
+
die(e);
|
|
831
|
+
dieTaskRoom(e);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
taskCmd.command('migrate-keys')
|
|
835
|
+
.description('assign mission keys and frozen agent names to tasks created before mission keys (idempotent)')
|
|
836
|
+
.option('--json', 'JSON output')
|
|
837
|
+
.action(async (opts) => {
|
|
838
|
+
try {
|
|
839
|
+
await taskRoomService().prepareMissionKeys();
|
|
840
|
+
if (opts.json) {
|
|
841
|
+
console.log(JSON.stringify({ schema_version: 1, migrated: true }, null, 2));
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
console.log(renderMarkdownResult({ icon: '🔑', title: 'Mission keys are up to date', fields: [] }));
|
|
845
|
+
}
|
|
846
|
+
catch (e) {
|
|
847
|
+
if (opts.json)
|
|
848
|
+
die(e);
|
|
849
|
+
dieTaskRoom(e);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
taskCmd.command('list-prefix <name> <prefix>')
|
|
853
|
+
.description('change the prefix for new mission keys of a list; issued keys never change')
|
|
854
|
+
.option('--json', 'JSON output')
|
|
855
|
+
.action(async (name, prefix, opts) => {
|
|
856
|
+
try {
|
|
857
|
+
const list = await taskRoomService().setTaskListPrefix({ actor: { kind: 'local_control', surface: 'cli' }, name, prefix });
|
|
808
858
|
if (opts.json) {
|
|
809
859
|
console.log(JSON.stringify({ schema_version: 1, list }, null, 2));
|
|
810
860
|
return;
|
|
811
861
|
}
|
|
812
|
-
console.log(renderMarkdownResult({ icon: '📚', title: 'Task list
|
|
862
|
+
console.log(renderMarkdownResult({ icon: '📚', title: 'Task list prefix changed', fields: [{ label: 'Name', value: list.name },
|
|
863
|
+
{ label: 'Prefix', value: list.prefix ?? '', kind: 'code' }] }));
|
|
813
864
|
}
|
|
814
865
|
catch (e) {
|
|
815
866
|
if (opts.json)
|
|
@@ -859,6 +910,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
859
910
|
.requiredOption('--list <name>', 'destination task list')
|
|
860
911
|
.option('--json', 'JSON output')
|
|
861
912
|
.action(async (id, opts) => {
|
|
913
|
+
id = cliTaskRef(id, opts.json);
|
|
862
914
|
try {
|
|
863
915
|
const task = await taskRoomService().moveTask({ actor: { kind: 'local_control', surface: 'cli' }, taskId: id, list: opts.list });
|
|
864
916
|
if (opts.json) {
|
|
@@ -877,6 +929,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
877
929
|
.description('show task details')
|
|
878
930
|
.option('--json', 'JSON output')
|
|
879
931
|
.action(async (id, opts) => {
|
|
932
|
+
id = cliTaskRef(id, opts.json);
|
|
880
933
|
try {
|
|
881
934
|
const service = taskRoomService(opts.configuration);
|
|
882
935
|
const { task: t, orchestration: room } = service.getTask(id);
|
|
@@ -892,6 +945,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
892
945
|
console.log(renderMarkdownResult({
|
|
893
946
|
icon: '📋', title: 'Task details',
|
|
894
947
|
fields: [
|
|
948
|
+
...(t.key ? [{ label: 'Key', value: t.key, kind: 'code' }] : []),
|
|
895
949
|
{ label: 'ID', value: t.task_id, kind: 'code' },
|
|
896
950
|
{ label: 'Title', value: t.title },
|
|
897
951
|
{ label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
|
|
@@ -932,6 +986,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
932
986
|
.description('resume proven stopped task members in-place; never create identities or replay invites')
|
|
933
987
|
.option('--json', 'JSON output')
|
|
934
988
|
.action(async (id, opts) => {
|
|
989
|
+
id = cliTaskRef(id, opts.json);
|
|
935
990
|
try {
|
|
936
991
|
const { recoverTaskMembers } = await import('./recovery.js');
|
|
937
992
|
const task = getTask(id), cfg = loadConfig(opts.configuration);
|
|
@@ -969,6 +1024,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
969
1024
|
.option('--no-loops', 'disable loops for current member')
|
|
970
1025
|
.option('--json', 'JSON output')
|
|
971
1026
|
.action(async (id, opts, command) => {
|
|
1027
|
+
id = cliTaskRef(id, opts.json);
|
|
972
1028
|
try {
|
|
973
1029
|
const service = taskRoomService(opts.configuration);
|
|
974
1030
|
const t = await service.startTask({
|
|
@@ -1002,6 +1058,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1002
1058
|
.requiredOption('--reason <reason>', 'blocking reason')
|
|
1003
1059
|
.option('--json', 'JSON output')
|
|
1004
1060
|
.action((id, opts) => {
|
|
1061
|
+
id = cliTaskRef(id, opts.json);
|
|
1005
1062
|
try {
|
|
1006
1063
|
const previous = getTask(id).state;
|
|
1007
1064
|
const t = taskRoomService().blockTask({
|
|
@@ -1024,6 +1081,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1024
1081
|
.description('unblock a task')
|
|
1025
1082
|
.option('--json', 'JSON output')
|
|
1026
1083
|
.action((id, opts) => {
|
|
1084
|
+
id = cliTaskRef(id, opts.json);
|
|
1027
1085
|
try {
|
|
1028
1086
|
const prior = getTask(id);
|
|
1029
1087
|
const previous = prior.state;
|
|
@@ -1047,6 +1105,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1047
1105
|
.description('move a task to review')
|
|
1048
1106
|
.option('--json', 'JSON output')
|
|
1049
1107
|
.action((id, opts) => {
|
|
1108
|
+
id = cliTaskRef(id, opts.json);
|
|
1050
1109
|
try {
|
|
1051
1110
|
const previous = getTask(id).state;
|
|
1052
1111
|
const t = taskRoomService().reviewTask({
|
|
@@ -1072,6 +1131,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1072
1131
|
.option('--request-key <key>', 'retry key: repeating the same key and text returns the original entry')
|
|
1073
1132
|
.option('--json', 'JSON output')
|
|
1074
1133
|
.action((id, opts) => {
|
|
1134
|
+
id = cliTaskRef(id, opts.json);
|
|
1075
1135
|
try {
|
|
1076
1136
|
if (opts.text !== undefined && opts.textFile !== undefined)
|
|
1077
1137
|
throw new Error('use --text or --text-file, not both');
|
|
@@ -1112,6 +1172,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1112
1172
|
.option('--expected-revision <n>', 'package revision you last read; --clear is refused if it changed since')
|
|
1113
1173
|
.option('--json', 'JSON output')
|
|
1114
1174
|
.action(async (id, opts) => {
|
|
1175
|
+
id = cliTaskRef(id, opts.json);
|
|
1115
1176
|
try {
|
|
1116
1177
|
if (opts.markdown !== undefined && opts.markdownFile !== undefined)
|
|
1117
1178
|
throw new Error('use --markdown or --markdown-file, not both');
|
|
@@ -1165,6 +1226,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1165
1226
|
.option('--summary-file <path>', 'completion summary from file')
|
|
1166
1227
|
.option('--json', 'JSON output')
|
|
1167
1228
|
.action(async (id, opts) => {
|
|
1229
|
+
id = cliTaskRef(id, opts.json);
|
|
1168
1230
|
try {
|
|
1169
1231
|
const previous = getTask(id).state;
|
|
1170
1232
|
let summary = opts.summary;
|
|
@@ -1208,6 +1270,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1208
1270
|
try {
|
|
1209
1271
|
if (id !== confirmId)
|
|
1210
1272
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
1273
|
+
id = resolveTaskRef(id);
|
|
1211
1274
|
const previous = getTask(id).state;
|
|
1212
1275
|
const plan = await taskRoomService(opts.configuration).cancelTask({
|
|
1213
1276
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
@@ -1246,6 +1309,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1246
1309
|
try {
|
|
1247
1310
|
if (id !== confirmId)
|
|
1248
1311
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
1312
|
+
// A deleted key still maps to its canonical ID, so a repeated delete stays idempotent.
|
|
1313
|
+
id = resolveTaskRef(id, { allowDeleted: true });
|
|
1249
1314
|
const accepted = await taskRoomService(opts.configuration).requestTaskDeletion({
|
|
1250
1315
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
1251
1316
|
});
|
|
@@ -1293,6 +1358,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1293
1358
|
.description('internal: settle a previously accepted task terminal intent')
|
|
1294
1359
|
.option('--json', 'JSON output')
|
|
1295
1360
|
.action(async (id, opts) => {
|
|
1361
|
+
id = cliTaskRef(id, opts.json);
|
|
1296
1362
|
try {
|
|
1297
1363
|
const app = taskRoomService(opts.configuration);
|
|
1298
1364
|
const t = await app.settleTask({
|
|
@@ -1318,6 +1384,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1318
1384
|
.description('internal: continue an accepted room-provisioning operation')
|
|
1319
1385
|
.option('--json', 'JSON output')
|
|
1320
1386
|
.action(async (id, opts) => {
|
|
1387
|
+
id = cliTaskRef(id, opts.json);
|
|
1321
1388
|
try {
|
|
1322
1389
|
await withFileLock(taskProvisioningWorkerLockPath(id), async () => {
|
|
1323
1390
|
const app = taskRoomService(opts.configuration);
|
|
@@ -1352,6 +1419,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1352
1419
|
.option('--json', 'JSON output')
|
|
1353
1420
|
.option('--operation-id <id>', 'accepted operation ID; status updates apply only while it is current')
|
|
1354
1421
|
.action(async (id, operation, room, opts) => {
|
|
1422
|
+
id = cliTaskRef(id, opts.json);
|
|
1355
1423
|
try {
|
|
1356
1424
|
if (!['start', 'open', 'close-room', 'close'].includes(operation))
|
|
1357
1425
|
throw new Error(`unknown layout operation: ${operation}`);
|
|
@@ -1371,6 +1439,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1371
1439
|
.description('internal: settle a previously accepted task deletion')
|
|
1372
1440
|
.option('--json', 'JSON output')
|
|
1373
1441
|
.action(async (id, opts) => {
|
|
1442
|
+
id = cliTaskRef(id, opts.json);
|
|
1374
1443
|
try {
|
|
1375
1444
|
const app = taskRoomService(opts.configuration);
|
|
1376
1445
|
const result = await app.settleTaskDeletion({
|
|
@@ -1416,6 +1485,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1416
1485
|
.option('--no-loops', 'disable loops for current member')
|
|
1417
1486
|
.option('--json', 'JSON output')
|
|
1418
1487
|
.action(async (id, opts, command) => {
|
|
1488
|
+
id = cliTaskRef(id, opts.json);
|
|
1419
1489
|
try {
|
|
1420
1490
|
const previous = getTask(id).state;
|
|
1421
1491
|
console.error('warning: `fleet task work` is deprecated; use `fleet task start`');
|
|
@@ -1463,6 +1533,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1463
1533
|
.option('--summary-file <path>', 'completion summary from file')
|
|
1464
1534
|
.option('--json', 'JSON output')
|
|
1465
1535
|
.action(async (id, opts) => {
|
|
1536
|
+
id = cliTaskRef(id, opts.json);
|
|
1466
1537
|
try {
|
|
1467
1538
|
const previous = getTask(id).state;
|
|
1468
1539
|
let summary = opts.summary;
|
|
@@ -1581,6 +1652,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1581
1652
|
.description('show room details')
|
|
1582
1653
|
.option('--json', 'JSON output')
|
|
1583
1654
|
.action(async (id, opts) => {
|
|
1655
|
+
id = cliTaskRef(id, opts.json);
|
|
1584
1656
|
try {
|
|
1585
1657
|
const { room: cowork, orchestration: r } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1586
1658
|
if (opts.json) {
|
|
@@ -1620,6 +1692,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1620
1692
|
.description('open room in Cowork local console')
|
|
1621
1693
|
.option('--json', 'JSON output')
|
|
1622
1694
|
.action(async (id, opts) => {
|
|
1695
|
+
id = cliTaskRef(id, opts.json);
|
|
1623
1696
|
try {
|
|
1624
1697
|
const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1625
1698
|
const url = `http://localhost:4460/room/${id}`;
|
|
@@ -1646,6 +1719,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1646
1719
|
.description('show room members')
|
|
1647
1720
|
.option('--json', 'JSON output')
|
|
1648
1721
|
.action(async (id, opts) => {
|
|
1722
|
+
id = cliTaskRef(id, opts.json);
|
|
1649
1723
|
try {
|
|
1650
1724
|
const { orchestration: r, members } = await taskRoomService(opts.configuration).getRoomMembers(id);
|
|
1651
1725
|
if (opts.json) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeMissionPrefix } from './mission-keys.js';
|
|
1
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { isAbsolute } from 'node:path';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
@@ -105,7 +106,17 @@ export function validateTasksConfig(raw, path) {
|
|
|
105
106
|
throw new RoomsTasksConfigError(path, 'tasks.close_room_on_done: must be a boolean');
|
|
106
107
|
if (raw.retain_completed_for !== undefined && typeof raw.retain_completed_for !== 'string')
|
|
107
108
|
throw new RoomsTasksConfigError(path, 'tasks.retain_completed_for: must be a duration string');
|
|
109
|
+
let defaultPrefix;
|
|
110
|
+
if (raw.default_prefix !== undefined) {
|
|
111
|
+
try {
|
|
112
|
+
defaultPrefix = normalizeMissionPrefix(raw.default_prefix);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new RoomsTasksConfigError(path, `tasks.default_prefix: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
108
118
|
return {
|
|
119
|
+
...(defaultPrefix ? { default_prefix: defaultPrefix } : {}),
|
|
109
120
|
default_room_template: raw.default_room_template,
|
|
110
121
|
create_mode: createMode,
|
|
111
122
|
close_room_on_done: raw.close_room_on_done,
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
2
|
+
import type { LabelCandidate } from './member-naming.js';
|
|
2
3
|
import type { LayoutInstance, RoomLayoutState } from './layout.js';
|
|
3
4
|
import type { RoomMemberSeat } from './types.js';
|
|
5
|
+
/** Name a pre-mission-key layout run derived for every participant; still the recovery fallback. */
|
|
6
|
+
export declare function legacyLayoutMemberName(runId: string, key: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* The owned name for a layout participant: the durable allocation recorded on
|
|
9
|
+
* its task (fresh or frozen), else the legacy hash. This is the ownership
|
|
10
|
+
* proof retirement checks before its launch, CID and provenance fences.
|
|
11
|
+
*/
|
|
4
12
|
export declare function layoutOwnedMemberName(runId: string, key: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Allocate (or reuse) the participant's durable role-scoped name before it is
|
|
15
|
+
* spawned. Every launch goes through the task allocator, which confirms a
|
|
16
|
+
* reused allocation is durable before returning it; retirement uses the
|
|
17
|
+
* side-effect-free layoutOwnedMemberName instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare function allocateLayoutMemberName(runId: string, key: string, candidates: LabelCandidate[]): string;
|
|
5
20
|
/** Retire a local layout factory result, never a borrowed binding. The runtime
|
|
6
21
|
* instance and CID fence replacement supervisors even when already stopped.
|
|
7
22
|
*/
|
|
@@ -9,6 +9,8 @@ import { agentDir, stateRoot } from '../paths.js';
|
|
|
9
9
|
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempArchiveForCreationAction } from '../temp-lifecycle.js';
|
|
10
10
|
import { assertMemberIdentityAbsent, removeExactMemberIdentity, waitForLivenessAbsent } from './close.js';
|
|
11
11
|
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
12
|
+
import { allocateTaskMemberNames, recordedMemberName } from './task-state.js';
|
|
13
|
+
import { assertMemberNameAvailable } from './provision.js';
|
|
12
14
|
import { retireTaskMemberService } from './task-service-retirement.js';
|
|
13
15
|
function proof(path) {
|
|
14
16
|
const stat = lstatSync(path);
|
|
@@ -16,9 +18,32 @@ function proof(path) {
|
|
|
16
18
|
throw Error('LAYOUT_MEMBER_UNSAFE_PROOF');
|
|
17
19
|
return readFileSync(path, 'utf8');
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
/** Name a pre-mission-key layout run derived for every participant; still the recovery fallback. */
|
|
22
|
+
export function legacyLayoutMemberName(runId, key) {
|
|
20
23
|
return `layout-${createHash('sha256').update(runId + ':' + key).digest('hex').slice(0, 16)}`;
|
|
21
24
|
}
|
|
25
|
+
const taskOfRun = (runId) => runId.startsWith('task-') && /^task-[0-9a-z]{9}[0-9a-f]{8}$/.test(runId) ? runId.slice(5) : undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The owned name for a layout participant: the durable allocation recorded on
|
|
28
|
+
* its task (fresh or frozen), else the legacy hash. This is the ownership
|
|
29
|
+
* proof retirement checks before its launch, CID and provenance fences.
|
|
30
|
+
*/
|
|
31
|
+
export function layoutOwnedMemberName(runId, key) {
|
|
32
|
+
const taskId = taskOfRun(runId);
|
|
33
|
+
return (taskId && recordedMemberName(taskId, `layout:${runId}:${key}`)) || legacyLayoutMemberName(runId, key);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Allocate (or reuse) the participant's durable role-scoped name before it is
|
|
37
|
+
* spawned. Every launch goes through the task allocator, which confirms a
|
|
38
|
+
* reused allocation is durable before returning it; retirement uses the
|
|
39
|
+
* side-effect-free layoutOwnedMemberName instead.
|
|
40
|
+
*/
|
|
41
|
+
export function allocateLayoutMemberName(runId, key, candidates) {
|
|
42
|
+
const taskId = taskOfRun(runId);
|
|
43
|
+
if (!taskId)
|
|
44
|
+
return legacyLayoutMemberName(runId, key);
|
|
45
|
+
return allocateTaskMemberNames(taskId, [{ allocationKey: `layout:${runId}:${key}`, candidates }], assertMemberNameAvailable)?.[0] ?? legacyLayoutMemberName(runId, key);
|
|
46
|
+
}
|
|
22
47
|
function assertOwned(instance, runId, key) {
|
|
23
48
|
if (instance.remote || !instance.agent || instance.temporary !== true
|
|
24
49
|
|| instance.agent !== layoutOwnedMemberName(runId, key) || instance.supervisor !== stateRoot())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AgentTemplateDefinition } from '../config.js';
|
|
2
2
|
import { RoomLayout, type LayoutInstance, type LayoutSupervisor, type RoomLayoutState } from './layout.js';
|
|
3
3
|
import { type RoomLayoutDefinition } from './layout-config.js';
|
|
4
|
+
import type { LabelCandidate } from './member-naming.js';
|
|
4
5
|
export declare class NativeLayoutSupervisor implements LayoutSupervisor {
|
|
5
6
|
private configPath;
|
|
6
7
|
private runId;
|
|
@@ -12,7 +13,7 @@ export declare class NativeLayoutSupervisor implements LayoutSupervisor {
|
|
|
12
13
|
inspect(agent: string, temporary: boolean): Promise<LayoutInstance>;
|
|
13
14
|
private call;
|
|
14
15
|
verify(i: LayoutInstance): Promise<void>;
|
|
15
|
-
spawn(key: string, template: string, snapshot?: AgentTemplateDefinition): Promise<LayoutInstance>;
|
|
16
|
+
spawn(key: string, template: string, snapshot?: AgentTemplateDefinition, labels?: LabelCandidate[]): Promise<LayoutInstance>;
|
|
16
17
|
join(i: LayoutInstance, invite: string, roomCid: string, membership?: {
|
|
17
18
|
roomId: string;
|
|
18
19
|
role: string;
|
|
@@ -13,7 +13,7 @@ import { createCoworkAdapter } from './cowork-adapter.js';
|
|
|
13
13
|
import { callRemoteLayoutBinding } from './layout-binding-client.js';
|
|
14
14
|
import { readClientProfile } from '../client-profile.js';
|
|
15
15
|
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
16
|
-
import {
|
|
16
|
+
import { allocateLayoutMemberName, retireOwnedLayoutMember, } from './layout-member-retirement.js';
|
|
17
17
|
import { EMPTY_TASK_LAYOUT, emptyTaskLayout } from './empty-layout.js';
|
|
18
18
|
export class NativeLayoutSupervisor {
|
|
19
19
|
configPath;
|
|
@@ -48,11 +48,13 @@ export class NativeLayoutSupervisor {
|
|
|
48
48
|
return this.request(i.agent, i.temporary, { ...request, instance: i });
|
|
49
49
|
}
|
|
50
50
|
async verify(i) { await this.call(i, { action: 'verify' }); }
|
|
51
|
-
async spawn(key, template, snapshot) {
|
|
51
|
+
async spawn(key, template, snapshot, labels = []) {
|
|
52
52
|
const definition = snapshot ?? this.templates[template];
|
|
53
53
|
if (!definition)
|
|
54
54
|
throw Error(`agent template not found in run snapshot: ${template}`);
|
|
55
|
-
|
|
55
|
+
// The first allocation is durable on the task; recovery reuses it once its durability is confirmed.
|
|
56
|
+
// Labels come from the run snapshot only.
|
|
57
|
+
const name = allocateLayoutMemberName(this.runId, key, labels);
|
|
56
58
|
const taskId = this.runId.startsWith('task-') ? this.runId.slice(5) : undefined;
|
|
57
59
|
const task = taskId ? (await import('./task-state.js')).getTask(taskId) : undefined;
|
|
58
60
|
if (task && task.layout?.run_id !== this.runId)
|
|
@@ -196,6 +198,7 @@ export class RoomLayoutService {
|
|
|
196
198
|
const source = scope.source ?? this.definition(name);
|
|
197
199
|
const definition = scope.definition ? scope.definition(structuredClone(source)) : source;
|
|
198
200
|
const templates = {};
|
|
201
|
+
const roleLabels = {};
|
|
199
202
|
for (const [key, participant] of Object.entries(definition.participants)) {
|
|
200
203
|
if (bindings[key] || !participant.agent_template)
|
|
201
204
|
continue;
|
|
@@ -203,9 +206,12 @@ export class RoomLayoutService {
|
|
|
203
206
|
if (!template)
|
|
204
207
|
throw Error(`agent template not found: ${participant.agent_template}`);
|
|
205
208
|
templates[participant.agent_template] = { ...structuredClone(template), ...(scope.cwd ? { cwd: scope.cwd } : {}) };
|
|
209
|
+
const roleRef = cfg.agentTemplateRoleRefs?.[participant.agent_template];
|
|
210
|
+
if (roleRef)
|
|
211
|
+
roleLabels[participant.agent_template] = roleRef;
|
|
206
212
|
}
|
|
207
213
|
const engine = this.engine(id);
|
|
208
|
-
await engine.create(definition, layoutSupervisorId(), bindings, templates);
|
|
214
|
+
await engine.create(definition, layoutSupervisorId(), bindings, templates, roleLabels);
|
|
209
215
|
return { id, state: engine.snapshot() };
|
|
210
216
|
}
|
|
211
217
|
}
|
|
@@ -53,7 +53,7 @@ export interface LayoutSupervisor {
|
|
|
53
53
|
id: string;
|
|
54
54
|
/** Must verify current launch, CID, live session and standalone ownership. */
|
|
55
55
|
verify(instance: LayoutInstance): Promise<void>;
|
|
56
|
-
spawn(key: string, template: string, definition?: import('../config.js').AgentTemplateDefinition): Promise<LayoutInstance>;
|
|
56
|
+
spawn(key: string, template: string, definition?: import('../config.js').AgentTemplateDefinition, labels?: import('./member-naming.js').LabelCandidate[]): Promise<LayoutInstance>;
|
|
57
57
|
join(instance: LayoutInstance, invite: string, roomCid: string, membership?: {
|
|
58
58
|
roomId: string;
|
|
59
59
|
role: string;
|
|
@@ -84,6 +84,8 @@ export interface RoomLayoutState {
|
|
|
84
84
|
agent_templates?: Record<string, import('../config.js').AgentTemplateDefinition>;
|
|
85
85
|
definition: LayoutDefinition;
|
|
86
86
|
controller: string;
|
|
87
|
+
/** Named Role of each source Agent Template, sealed at run creation; later config edits never rename members. */
|
|
88
|
+
role_labels?: Record<string, string>;
|
|
87
89
|
participants: Record<string, ParticipantState>;
|
|
88
90
|
rooms: Record<string, RoomState>;
|
|
89
91
|
/** Runtime additions leave the pinned source definition unchanged. */
|
|
@@ -115,7 +117,7 @@ export declare class RoomLayout {
|
|
|
115
117
|
snapshot(): RoomLayoutState;
|
|
116
118
|
private save;
|
|
117
119
|
private verify;
|
|
118
|
-
create(definition: LayoutDefinition, controller: string, bindings?: Record<string, LayoutInstance>, agentTemplates?: RoomLayoutState['agent_templates']): Promise<void>;
|
|
120
|
+
create(definition: LayoutDefinition, controller: string, bindings?: Record<string, LayoutInstance>, agentTemplates?: RoomLayoutState['agent_templates'], roleLabels?: RoomLayoutState['role_labels']): Promise<void>;
|
|
119
121
|
private checkOwnerRoles;
|
|
120
122
|
private operation;
|
|
121
123
|
private mutation;
|
|
@@ -16,6 +16,21 @@ function need(ok, message) { if (!ok)
|
|
|
16
16
|
function instanceKey(definition, participant, room) {
|
|
17
17
|
return definition.participants[participant]?.instance_scope === 'room' ? `${room}:${participant}` : participant;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Naming labels for a participant, after its template's named Role (added by
|
|
21
|
+
* the supervisor): a runtime-added member's room role, else its room role when
|
|
22
|
+
* every room it joins agrees (or its own room when room-scoped), else its key.
|
|
23
|
+
*/
|
|
24
|
+
function participantLabels(s, participant, template, room) {
|
|
25
|
+
const added = s.additional_members?.[participant];
|
|
26
|
+
// Runs created before sealing have no role_labels: fall back to room roles, never to current config.
|
|
27
|
+
const sealed = s.role_labels && own(s.role_labels, template) ? s.role_labels[template] : undefined;
|
|
28
|
+
const roles = Object.entries(s.definition.rooms)
|
|
29
|
+
.filter(([key, spec]) => spec.members.includes(participant) && (!room || room === key))
|
|
30
|
+
.map(([, spec]) => spec.roles?.[participant] ?? participant);
|
|
31
|
+
const agreed = added?.role ?? (new Set(roles).size === 1 ? roles[0] : undefined);
|
|
32
|
+
return [{ label: sealed, source: 'template_ref' }, { label: agreed, source: 'room_role' }, { label: participant, source: 'participant_key' }];
|
|
33
|
+
}
|
|
19
34
|
export function validateLayout(def) {
|
|
20
35
|
need(Object.keys(def.rooms).length, 'rooms required');
|
|
21
36
|
const empty = Object.keys(def.participants).length === 0;
|
|
@@ -59,7 +74,7 @@ export class RoomLayout {
|
|
|
59
74
|
need(i.launch && i.cid && i.session, 'incomplete instance reference');
|
|
60
75
|
await this.supervisor.verify(i);
|
|
61
76
|
}
|
|
62
|
-
async create(definition, controller, bindings = {}, agentTemplates) {
|
|
77
|
+
async create(definition, controller, bindings = {}, agentTemplates, roleLabels) {
|
|
63
78
|
validateLayout(definition);
|
|
64
79
|
need(controller, 'controller required');
|
|
65
80
|
this.checkOwnerRoles(definition);
|
|
@@ -90,7 +105,8 @@ export class RoomLayout {
|
|
|
90
105
|
participants[key] = { owned: !bindings[key], ...(bindings[key] ? { instance: bindings[key] } : {}) };
|
|
91
106
|
}
|
|
92
107
|
this.save({ version: 1, definition, controller, ...(this.owner ? { owner: ownerReference(this.owner) } : {}),
|
|
93
|
-
agent_templates: agentTemplates,
|
|
108
|
+
agent_templates: agentTemplates, ...(roleLabels && Object.keys(roleLabels).length ? { role_labels: roleLabels } : {}),
|
|
109
|
+
participants, rooms: {}, order: [], closed: false });
|
|
94
110
|
});
|
|
95
111
|
}
|
|
96
112
|
checkOwnerRoles(definition) {
|
|
@@ -150,9 +166,10 @@ export class RoomLayout {
|
|
|
150
166
|
need(template, `no factory for ${member}`);
|
|
151
167
|
if (this.supervisor.taskSupervised)
|
|
152
168
|
p.task_supervised = true;
|
|
169
|
+
const labels = participantLabels(s, member, template, p.room);
|
|
153
170
|
await this.mutation(s, `spawn:${resolvedKey}`, () => s.additional_members?.[member]
|
|
154
|
-
? this.supervisor.spawn(resolvedKey, template, s.agent_templates?.[template])
|
|
155
|
-
: this.supervisor.spawn(resolvedKey, template), i => { p.instance = i; });
|
|
171
|
+
? this.supervisor.spawn(resolvedKey, template, s.agent_templates?.[template], labels)
|
|
172
|
+
: this.supervisor.spawn(resolvedKey, template, undefined, labels), i => { p.instance = i; });
|
|
156
173
|
await this.verify(p.instance);
|
|
157
174
|
}
|
|
158
175
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MemberNameAllocation, MemberNaming, TaskRecord } from './types.js';
|
|
2
|
+
export declare const AGENT_NAME_MAX = 64;
|
|
3
|
+
/** Ordinal suffix headroom: `-999`. */
|
|
4
|
+
export declare const MAX_ROLE_ORDINAL = 999;
|
|
5
|
+
export type LabelSource = NonNullable<MemberNameAllocation['label_source']>;
|
|
6
|
+
export interface LabelCandidate {
|
|
7
|
+
label: string | undefined;
|
|
8
|
+
source: LabelSource;
|
|
9
|
+
}
|
|
10
|
+
export declare class MemberNameError extends Error {
|
|
11
|
+
readonly code: 'role_ordinal_exhausted' | 'agent_name_taken' | 'invalid_label';
|
|
12
|
+
constructor(code: 'role_ordinal_exhausted' | 'agent_name_taken' | 'invalid_label', message: string);
|
|
13
|
+
}
|
|
14
|
+
/** PascalCase join of Latin alphanumeric words; diacritics folded, other scripts dropped. */
|
|
15
|
+
export declare function labelStem(label: string): string;
|
|
16
|
+
/** First candidate that survives normalization, else `Agent`. */
|
|
17
|
+
export declare function chooseStem(key: string, candidates: readonly LabelCandidate[]): {
|
|
18
|
+
stem: string;
|
|
19
|
+
source: LabelSource;
|
|
20
|
+
};
|
|
21
|
+
export declare const bucketOf: (stem: string) => string;
|
|
22
|
+
/** Own-property read: buckets such as `constructor` must never hit Object.prototype. */
|
|
23
|
+
export declare const ownEntry: <T>(record: Record<string, T> | undefined, key: string) => T | undefined;
|
|
24
|
+
export declare function formatMemberName(key: string, stem: string, ordinal: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* Pure allocation step: returns the existing allocation for `allocationKey`,
|
|
27
|
+
* or a new one plus the updated naming. The caller persists under the task
|
|
28
|
+
* lock and checks external name conflicts before launch.
|
|
29
|
+
*/
|
|
30
|
+
export declare function allocateName(naming: MemberNaming | undefined, key: string, allocationKey: string, candidates: readonly LabelCandidate[]): {
|
|
31
|
+
naming: MemberNaming;
|
|
32
|
+
allocation: MemberNameAllocation;
|
|
33
|
+
created: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Freeze every historical agent name and seed counters by the same label
|
|
37
|
+
* precedence as new names. Deduplicated by physical agent name; counters merge
|
|
38
|
+
* monotonically and existing allocations are never rewritten. Returns the
|
|
39
|
+
* input object unchanged when there is nothing to add.
|
|
40
|
+
*/
|
|
41
|
+
export declare function freezeLegacyMemberNaming(task: Pick<TaskRecord, 'task_id' | 'room_id' | 'layout' | 'execution_plan' | 'member_naming'> & {
|
|
42
|
+
key: string;
|
|
43
|
+
}): MemberNaming | undefined;
|