@ours.network/fleet 0.19.0-nightly.6 → 0.19.0-nightly.8
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
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.19.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "0.19.0-nightly.8",
|
|
3
|
+
"buildId": "d22ed5b7bff3",
|
|
4
|
+
"commit": "39cad43ae0d8f08341775953baeda7b77eb3ade4",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-23T23:06:12.416Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -2,9 +2,10 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { loadConfig, ConfigError } from '../config.js';
|
|
3
3
|
import { provisionMembers, cleanupMembers, getBinPath } from './provision.js';
|
|
4
4
|
import { resolveTemplate, listTemplates, snapshotTemplate, hashTemplate, } from './templates.js';
|
|
5
|
-
import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, updateTaskRoom, } from './task-state.js';
|
|
5
|
+
import { createTask, getTask, listTasks, startTask, activateTask, blockTask, unblockTask, reviewTask, completeTask, cancelTask, updateTaskRoom, updateTaskTemplate, } from './task-state.js';
|
|
6
6
|
import { createRoomRecord, getRoomRecord, listRoomRecords, closeRoom as closeRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, } from './room-state.js';
|
|
7
7
|
import { createCoworkAdapter, CoworkProtocolError, CoworkUnavailableError } from './cowork-adapter.js';
|
|
8
|
+
import { TASK_TERMINAL_STATES } from './types.js';
|
|
8
9
|
function die(e) {
|
|
9
10
|
const msg = e instanceof Error ? e.message : String(e);
|
|
10
11
|
process.stderr.write(`error: ${msg}\n`);
|
|
@@ -204,7 +205,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
204
205
|
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
205
206
|
}
|
|
206
207
|
else if (opts.room !== false) {
|
|
207
|
-
const defaultTpl = cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? '
|
|
208
|
+
const defaultTpl = cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'team';
|
|
208
209
|
const t = resolveTemplate(defaultTpl, allTemplates(cfg));
|
|
209
210
|
if (t) {
|
|
210
211
|
const snap = snapshotTemplate(t);
|
|
@@ -534,6 +535,174 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
534
535
|
die(e);
|
|
535
536
|
}
|
|
536
537
|
});
|
|
538
|
+
cOpt(taskCmd.command('work <id>'))
|
|
539
|
+
.description('ensure a task has a room and agents running')
|
|
540
|
+
.option('--template <name>', 'room template')
|
|
541
|
+
.option('--json', 'JSON output')
|
|
542
|
+
.action(async (id, opts) => {
|
|
543
|
+
try {
|
|
544
|
+
const cfg = loadCfg(opts);
|
|
545
|
+
let t = getTask(id);
|
|
546
|
+
if (TASK_TERMINAL_STATES.includes(t.state))
|
|
547
|
+
die(new Error(`task ${id} is in terminal state '${t.state}'`));
|
|
548
|
+
if (t.state === 'active' && t.room_id) {
|
|
549
|
+
// An explicit --template must agree with the room the task already
|
|
550
|
+
// runs in; a conflicting override is an error, not a silent no-op.
|
|
551
|
+
if (opts.template) {
|
|
552
|
+
const override = resolveTemplate(opts.template, allTemplates(cfg));
|
|
553
|
+
if (!override)
|
|
554
|
+
die(new Error(`template not found: ${opts.template}`));
|
|
555
|
+
const overrideSnap = snapshotTemplate(override);
|
|
556
|
+
const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
|
|
557
|
+
if (roomSnap && (roomSnap.name !== overrideSnap.name || roomSnap.content_hash !== overrideSnap.content_hash))
|
|
558
|
+
die(new Error(`template ${overrideSnap.name}@${overrideSnap.version} does not match room ${t.room_id}'s provisioned template ${roomSnap.name}@${roomSnap.version}`));
|
|
559
|
+
}
|
|
560
|
+
if (opts.json) {
|
|
561
|
+
console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
console.log('Task already has an active room');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Select and validate the template snapshot before any state change or
|
|
568
|
+
// provisioning. An explicit --template re-pins the task; a stored ref
|
|
569
|
+
// must still match its snapshot (same contract as `task start`).
|
|
570
|
+
const templateName = opts.template
|
|
571
|
+
?? t.template?.name
|
|
572
|
+
?? cfg.tasks?.default_room_template
|
|
573
|
+
?? cfg.rooms?.defaults?.template
|
|
574
|
+
?? 'single';
|
|
575
|
+
const resolved = resolveTemplate(templateName, allTemplates(cfg));
|
|
576
|
+
if (!resolved)
|
|
577
|
+
die(new Error(`template not found: ${templateName}`));
|
|
578
|
+
const snap = snapshotTemplate(resolved);
|
|
579
|
+
if (!opts.template && t.template && snap.content_hash !== t.template.content_hash)
|
|
580
|
+
die(new Error(`task template snapshot no longer matches ${t.template.name}@${t.template.version}`));
|
|
581
|
+
// A provisioned room is pinned to its snapshot: never resume or re-pin
|
|
582
|
+
// an existing room under a different template.
|
|
583
|
+
if (t.room_id) {
|
|
584
|
+
const roomSnap = getRoomRecord(t.room_id)?.template_snapshot ?? t.template;
|
|
585
|
+
if (roomSnap && (roomSnap.name !== snap.name || roomSnap.content_hash !== snap.content_hash))
|
|
586
|
+
die(new Error(`template ${snap.name}@${snap.version} does not match room ${t.room_id}'s provisioned template ${roomSnap.name}@${roomSnap.version}`));
|
|
587
|
+
}
|
|
588
|
+
let templateRef = t.template;
|
|
589
|
+
if (!templateRef || templateRef.name !== snap.name || templateRef.content_hash !== snap.content_hash) {
|
|
590
|
+
templateRef = { name: snap.name, version: snap.version, content_hash: snap.content_hash };
|
|
591
|
+
t = updateTaskTemplate(t.task_id, templateRef);
|
|
592
|
+
}
|
|
593
|
+
if (t.state === 'backlog') {
|
|
594
|
+
t = startTask(id);
|
|
595
|
+
}
|
|
596
|
+
if (!t.room_id) {
|
|
597
|
+
try {
|
|
598
|
+
await provisionRoom(cfg, {
|
|
599
|
+
name: t.title,
|
|
600
|
+
goal: t.title,
|
|
601
|
+
brief: t.brief,
|
|
602
|
+
template: snap,
|
|
603
|
+
taskId: t.task_id,
|
|
604
|
+
onCreated: room => {
|
|
605
|
+
t = updateTaskRoom(t.task_id, room.room_id, room.room_identity_cid);
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
t = getTask(t.task_id);
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
if (error instanceof CoworkUnavailableError) {
|
|
612
|
+
blockTask(t.task_id, 'Cowork management socket is unavailable');
|
|
613
|
+
}
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (t.state === 'provisioning') {
|
|
618
|
+
// The task already has a room mid-provisioning (e.g. seats were still
|
|
619
|
+
// pending on the last run). Resume it exactly as `task recover` does.
|
|
620
|
+
const room = getRoomRecord(t.room_id);
|
|
621
|
+
const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
|
|
622
|
+
if (room && resumable.includes(room.saga.phase)) {
|
|
623
|
+
try {
|
|
624
|
+
await provisionMembers({
|
|
625
|
+
cfg,
|
|
626
|
+
cowork: coworkFor(cfg),
|
|
627
|
+
roomId: room.room_id,
|
|
628
|
+
taskId: t.task_id,
|
|
629
|
+
template: snap,
|
|
630
|
+
binPath: getBinPath(),
|
|
631
|
+
brief: t.brief,
|
|
632
|
+
goal: t.title,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
if (error instanceof CoworkUnavailableError) {
|
|
637
|
+
blockTask(t.task_id, 'Cowork management socket is unavailable');
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
t = getTask(t.task_id);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
die(new Error(`task ${id} has room ${t.room_id} in a non-resumable state — run 'task recover ${id}'`));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (opts.json) {
|
|
648
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
console.log(`Task ${t.task_id} · ${t.state}`);
|
|
652
|
+
if (templateRef)
|
|
653
|
+
console.log(`Template: ${templateRef.name}@${templateRef.version}`);
|
|
654
|
+
if (t.room_id)
|
|
655
|
+
console.log(`Room: ${t.room_id}`);
|
|
656
|
+
if (t.member_roles.length) {
|
|
657
|
+
console.log('Agents:');
|
|
658
|
+
for (const m of t.member_roles)
|
|
659
|
+
console.log(` ${m.name} (${m.cowork_role})`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
die(e);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
cOpt(taskCmd.command('finish <id>'))
|
|
667
|
+
.description('finish a task: transition to done and close its room')
|
|
668
|
+
.option('--summary <text>', 'completion summary')
|
|
669
|
+
.option('--summary-file <path>', 'completion summary from file')
|
|
670
|
+
.option('--json', 'JSON output')
|
|
671
|
+
.action(async (id, opts) => {
|
|
672
|
+
try {
|
|
673
|
+
let summary = opts.summary;
|
|
674
|
+
if (opts.summaryFile)
|
|
675
|
+
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
676
|
+
const outcome = summary ? { summary } : undefined;
|
|
677
|
+
let t = getTask(id);
|
|
678
|
+
if (TASK_TERMINAL_STATES.includes(t.state))
|
|
679
|
+
die(new Error(`task ${id} is already in terminal state '${t.state}'`));
|
|
680
|
+
if (t.state === 'active') {
|
|
681
|
+
reviewTask(id);
|
|
682
|
+
}
|
|
683
|
+
t = completeTask(id, outcome);
|
|
684
|
+
if (t.room_id) {
|
|
685
|
+
const cfg = loadCfg(opts);
|
|
686
|
+
try {
|
|
687
|
+
await cleanupMembers({
|
|
688
|
+
roomId: t.room_id,
|
|
689
|
+
taskId: id,
|
|
690
|
+
closeCoworkRoom: true,
|
|
691
|
+
cowork: coworkFor(cfg),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
catch { /* room cleanup is best-effort */ }
|
|
695
|
+
}
|
|
696
|
+
if (opts.json) {
|
|
697
|
+
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
console.log(`Task ${t.task_id} · done`);
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
die(e);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
537
706
|
}
|
|
538
707
|
export function registerRoomCommands(parent, cOpt) {
|
|
539
708
|
const roomCmd = parent.command('room').description('room orchestration operations');
|
|
@@ -96,71 +96,90 @@ export async function provisionMembers(input) {
|
|
|
96
96
|
const plan = expandMembers(template, prefix);
|
|
97
97
|
const createdIdentities = [];
|
|
98
98
|
const members = [];
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// A room that already reached wait_seats keeps its member identities, seats
|
|
100
|
+
// and accepted invitations; recreating them on a retry would collide. Resume
|
|
101
|
+
// from the persisted seats and re-check seat activity instead.
|
|
102
|
+
const existing = getRoomRecord(roomId);
|
|
103
|
+
const persistedSeats = existing?.member_seats ?? [];
|
|
104
|
+
const resuming = existing !== undefined
|
|
105
|
+
&& ['wait_seats', 'launch_work', 'activate'].includes(existing.saga.phase)
|
|
106
|
+
&& plan.length > 0
|
|
107
|
+
&& plan.every(p => persistedSeats.some(s => s.role_name === p.name));
|
|
108
|
+
let seats;
|
|
109
|
+
if (resuming) {
|
|
102
110
|
for (const planned of plan) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
createdIdentities.push(planned.name);
|
|
106
|
-
members.push({ ...planned, cid });
|
|
111
|
+
const seat = persistedSeats.find(s => s.role_name === planned.name);
|
|
112
|
+
members.push({ ...planned, cid: seat.identity_cid });
|
|
107
113
|
}
|
|
114
|
+
seats = persistedSeats;
|
|
108
115
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
else {
|
|
117
|
+
// Phase 4: create_members
|
|
118
|
+
advanceSaga(roomId, 'create_members', 3);
|
|
119
|
+
try {
|
|
120
|
+
for (const planned of plan) {
|
|
121
|
+
const bio = `Fleet task member: ${planned.coworkRole} for ${taskId ?? roomId}`;
|
|
122
|
+
const { cid } = await createMemberIdentity(planned.name, bio);
|
|
123
|
+
createdIdentities.push(planned.name);
|
|
124
|
+
members.push({ ...planned, cid });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
for (const name of createdIdentities)
|
|
129
|
+
await removeMemberIdentity(name);
|
|
130
|
+
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Member identity creation failed. Retry with `task recover`.', 'member_failed');
|
|
131
|
+
if (taskId)
|
|
132
|
+
failTask(taskId, error instanceof Error ? error.message : String(error));
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
seats = members.map(m => ({
|
|
136
|
+
role_name: m.name,
|
|
128
137
|
identity_cid: m.cid,
|
|
129
138
|
slot: m.slot,
|
|
130
139
|
cowork_role: m.coworkRole,
|
|
140
|
+
seat_state: 'pending',
|
|
131
141
|
}));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
updateMemberSeats(roomId, seats);
|
|
143
|
+
if (taskId) {
|
|
144
|
+
const taskMembers = members.map(m => ({
|
|
145
|
+
name: m.name,
|
|
146
|
+
identity_cid: m.cid,
|
|
147
|
+
slot: m.slot,
|
|
148
|
+
cowork_role: m.coworkRole,
|
|
149
|
+
}));
|
|
150
|
+
updateTaskMembers(taskId, taskMembers);
|
|
151
|
+
}
|
|
152
|
+
// Phase 5: join_role_groups — serial by Cowork role
|
|
153
|
+
advanceSaga(roomId, 'join_role_groups', 4);
|
|
154
|
+
const roleGroups = new Map();
|
|
155
|
+
for (const m of members) {
|
|
156
|
+
const group = roleGroups.get(m.coworkRole) ?? [];
|
|
157
|
+
group.push(m);
|
|
158
|
+
roleGroups.set(m.coworkRole, group);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
for (const [coworkRole, group] of roleGroups) {
|
|
162
|
+
const { invite } = await cowork.issueInvite(roomId, {
|
|
151
163
|
role: coworkRole,
|
|
152
|
-
|
|
164
|
+
min_accepts: group.length,
|
|
153
165
|
});
|
|
166
|
+
// invite is used in-memory only — NEVER persisted
|
|
167
|
+
for (const member of group) {
|
|
168
|
+
await cowork.acceptInvite(roomId, invite, {
|
|
169
|
+
role: coworkRole,
|
|
170
|
+
expected_cid: member.cid,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
catch (error) {
|
|
176
|
+
for (const name of createdIdentities)
|
|
177
|
+
await removeMemberIdentity(name);
|
|
178
|
+
setSagaError(roomId, error instanceof Error ? error.message : String(error), 'Role-group admission failed. Retry with `task recover`.', 'member_failed');
|
|
179
|
+
if (taskId)
|
|
180
|
+
failTask(taskId, error instanceof Error ? error.message : String(error));
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
164
183
|
}
|
|
165
184
|
// Phase 6: wait_seats
|
|
166
185
|
advanceSaga(roomId, 'wait_seats', 5);
|
|
@@ -28,5 +28,6 @@ export declare function completeTask(id: string, outcome?: TaskOutcome): TaskRec
|
|
|
28
28
|
export declare function cancelTask(id: string): TaskRecord;
|
|
29
29
|
export declare function failTask(id: string, error: string): TaskRecord;
|
|
30
30
|
export declare function updateTaskRoom(id: string, roomId: string, roomIdentityCid: string): TaskRecord;
|
|
31
|
+
export declare function updateTaskTemplate(id: string, template: TaskTemplateRef): TaskRecord;
|
|
31
32
|
export declare function updateTaskMembers(id: string, members: TaskMemberRole[]): TaskRecord;
|
|
32
33
|
export declare function deleteTask(id: string): void;
|
|
@@ -161,6 +161,12 @@ export function updateTaskRoom(id, roomId, roomIdentityCid) {
|
|
|
161
161
|
writeTask(t);
|
|
162
162
|
return t;
|
|
163
163
|
}
|
|
164
|
+
export function updateTaskTemplate(id, template) {
|
|
165
|
+
const t = readTask(id);
|
|
166
|
+
t.template = template;
|
|
167
|
+
writeTask(t);
|
|
168
|
+
return t;
|
|
169
|
+
}
|
|
164
170
|
export function updateTaskMembers(id, members) {
|
|
165
171
|
const t = readTask(id);
|
|
166
172
|
t.member_roles = members;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
const
|
|
3
|
-
name: '
|
|
2
|
+
const TEAM = {
|
|
3
|
+
name: 'team',
|
|
4
4
|
version: 1,
|
|
5
5
|
builtin: true,
|
|
6
6
|
description: 'Phased task pipeline: Architect specifies, Developer implements, Tester verifies',
|
|
@@ -17,8 +17,8 @@ const BRIEFING = {
|
|
|
17
17
|
{ slot: 'tester', role: 'Tester', count: 1, role_ref: 'Tester' },
|
|
18
18
|
],
|
|
19
19
|
};
|
|
20
|
-
const
|
|
21
|
-
name: '
|
|
20
|
+
const PAIR = {
|
|
21
|
+
name: 'pair',
|
|
22
22
|
version: 1,
|
|
23
23
|
builtin: true,
|
|
24
24
|
description: 'Deliberation pair: Secretary writes code, Critic reviews — every decision deliberated together',
|
|
@@ -34,7 +34,21 @@ const CONSILIUM = {
|
|
|
34
34
|
{ slot: 'critic', role: 'Critic', count: 1, role_ref: 'Critic' },
|
|
35
35
|
],
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
const SINGLE = {
|
|
38
|
+
name: 'single',
|
|
39
|
+
version: 1,
|
|
40
|
+
builtin: true,
|
|
41
|
+
description: 'Solo agent: one general-purpose agent works the task with owner oversight',
|
|
42
|
+
room: { quiet_membership: false, anonymous: false },
|
|
43
|
+
contract: [
|
|
44
|
+
'Agent works the task autonomously, consulting the owner when blocked.',
|
|
45
|
+
'Owner reviews and approves completion.',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
members: [
|
|
48
|
+
{ slot: 'agent', role: 'Agent', count: 1, role_ref: 'Agent' },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
export const BUILTIN_TEMPLATES = [TEAM, PAIR, SINGLE];
|
|
38
52
|
export function hashTemplate(t) {
|
|
39
53
|
const canonical = JSON.stringify({
|
|
40
54
|
name: t.name, version: t.version, description: t.description,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.19.0-nightly.
|
|
3
|
+
"version": "0.19.0-nightly.8",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|