@ours.network/fleet 0.19.0-nightly.8 → 1.0.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/README.md +96 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +145 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { withFileLock } from '../atomic-file.js';
|
|
2
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
3
|
+
import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
4
|
+
export interface RoomCloseDeps {
|
|
5
|
+
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
6
|
+
launchId: string;
|
|
7
|
+
}>;
|
|
8
|
+
requestStop?(role: string): Promise<void>;
|
|
9
|
+
waitForLivenessAbsent?(role: string, launchId: string): Promise<void>;
|
|
10
|
+
secureArchive?(role: string, launchId: string): Promise<string>;
|
|
11
|
+
removeIdentity?(seat: RoomMemberSeat): Promise<void>;
|
|
12
|
+
lock?: typeof withFileLock;
|
|
13
|
+
}
|
|
14
|
+
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
15
|
+
export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
|
|
16
|
+
export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
|
|
17
|
+
export declare function closeManagedRoom(input: {
|
|
18
|
+
roomId: string;
|
|
19
|
+
cowork: Pick<CoworkAdapter, 'closeRoom'>;
|
|
20
|
+
deps?: RoomCloseDeps;
|
|
21
|
+
}): Promise<RoomOrchestrationRecord>;
|
|
22
|
+
export interface ManagedRoomDeleteResult {
|
|
23
|
+
room_id: string;
|
|
24
|
+
deleted: true;
|
|
25
|
+
}
|
|
26
|
+
/** Remove retained state written by prerelease builds that stopped at `closed`. */
|
|
27
|
+
export declare function deleteLegacyClosedRooms(input: {
|
|
28
|
+
cowork: Pick<CoworkAdapter, 'deleteRoom'>;
|
|
29
|
+
}): Promise<string[]>;
|
|
30
|
+
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
31
|
+
export declare function deleteManagedRoom(input: {
|
|
32
|
+
roomId: string;
|
|
33
|
+
cowork: Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'>;
|
|
34
|
+
deps?: RoomCloseDeps;
|
|
35
|
+
}): Promise<ManagedRoomDeleteResult>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
5
|
+
import { withFileLock } from '../atomic-file.js';
|
|
6
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
7
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
8
|
+
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
9
|
+
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
10
|
+
const STOP_POLLS = 50;
|
|
11
|
+
const STOP_POLL_MS = 100;
|
|
12
|
+
function roomCloseLockPath(roomId) {
|
|
13
|
+
return join(stateRoot(), 'locks', 'room-close', encodeURIComponent(roomId));
|
|
14
|
+
}
|
|
15
|
+
function errorText(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function exactMemberIdentity(seat) {
|
|
19
|
+
const dir = agentDir(seat.role_name, true);
|
|
20
|
+
const identityPath = join(dir, '.identity');
|
|
21
|
+
if (!existsSync(dir) || !existsSync(identityPath)) {
|
|
22
|
+
throw new Error(`room member '${seat.role_name}' has no live Fleet temp-state identity proof; refusing retirement`);
|
|
23
|
+
}
|
|
24
|
+
const identity = readFileSync(identityPath, 'utf8').trim();
|
|
25
|
+
if (identity !== seat.role_name) {
|
|
26
|
+
throw new Error(`room member '${seat.role_name}' temp state binds identity '${identity}'; refusing mismatched retirement`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function inspectMember(seat) {
|
|
30
|
+
exactMemberIdentity(seat);
|
|
31
|
+
const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
|
|
32
|
+
if (!supervisor || supervisor.role !== seat.role_name) {
|
|
33
|
+
throw new Error(`room member '${seat.role_name}' has no exact Fleet supervisor ownership proof`);
|
|
34
|
+
}
|
|
35
|
+
return { launchId: supervisor.launchId };
|
|
36
|
+
}
|
|
37
|
+
async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {}) {
|
|
38
|
+
const dir = agentDir(role, true);
|
|
39
|
+
const sleep = lifecycleDeps.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
40
|
+
for (let attempt = 0; attempt < STOP_POLLS; attempt++) {
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
return; // secureStoppedTempArchive proves the exact archive next.
|
|
43
|
+
const supervisor = readTempSupervisor(dir);
|
|
44
|
+
if (!supervisor || supervisor.role !== role || supervisor.launchId !== launchId) {
|
|
45
|
+
throw new Error(`temporary role '${role}' changed while waiting for launch ${launchId} to stop`);
|
|
46
|
+
}
|
|
47
|
+
const state = await tempSupervisorLiveness(dir, lifecycleDeps);
|
|
48
|
+
if (state === 'stopped')
|
|
49
|
+
return;
|
|
50
|
+
if (attempt < STOP_POLLS - 1)
|
|
51
|
+
await sleep(STOP_POLL_MS);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`temporary role '${role}' did not reach proven stopped liveness`);
|
|
54
|
+
}
|
|
55
|
+
async function withIdentityClient(work) {
|
|
56
|
+
const client = await attachOursClient({
|
|
57
|
+
env: process.env,
|
|
58
|
+
leaseToken: `ours-fleet-room-close-${process.pid}-${randomUUID()}`,
|
|
59
|
+
clientPid: process.pid,
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
return await work(client);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await client.releaseLease().catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function listedIdentity(rows, name) {
|
|
69
|
+
return rows.find(row => row.name === name);
|
|
70
|
+
}
|
|
71
|
+
async function removeExactMemberIdentity(seat) {
|
|
72
|
+
await withIdentityClient(async (client) => {
|
|
73
|
+
const before = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
74
|
+
if (!before)
|
|
75
|
+
return;
|
|
76
|
+
if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
|
|
77
|
+
throw new Error(`room member '${seat.role_name}' identity CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await client.removeIdentity({ name: seat.role_name });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const code = error instanceof Error && error.name === 'OursError'
|
|
84
|
+
? error.code
|
|
85
|
+
: undefined;
|
|
86
|
+
if (code !== 'NO_SUCH_IDENTITY')
|
|
87
|
+
throw error;
|
|
88
|
+
const after = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
89
|
+
if (after)
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const after = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
93
|
+
if (after) {
|
|
94
|
+
throw new Error(`room member '${seat.role_name}' identity still exists after remove_identity`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function retireMember(roomId, seat, deps) {
|
|
99
|
+
let room = getRoomRecord(roomId);
|
|
100
|
+
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
101
|
+
let retirement = current.retirement;
|
|
102
|
+
if (!retirement) {
|
|
103
|
+
const ownership = await (deps.inspectMember ?? inspectMember)(current);
|
|
104
|
+
room = advanceMemberRetirement(roomId, current.role_name, 'stop_requested', ownership.launchId);
|
|
105
|
+
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
106
|
+
retirement = current.retirement;
|
|
107
|
+
}
|
|
108
|
+
if (retirement.phase === 'stop_requested') {
|
|
109
|
+
await (deps.requestStop ?? (async (role) => { await stopTempSupervisor(role); }))(current.role_name);
|
|
110
|
+
await (deps.waitForLivenessAbsent ?? waitForLivenessAbsent)(current.role_name, retirement.launch_id);
|
|
111
|
+
room = advanceMemberRetirement(roomId, current.role_name, 'liveness_absent', retirement.launch_id);
|
|
112
|
+
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
113
|
+
retirement = current.retirement;
|
|
114
|
+
}
|
|
115
|
+
if (retirement.phase === 'liveness_absent') {
|
|
116
|
+
const archivePath = await (deps.secureArchive ?? secureStoppedTempArchive)(current.role_name, retirement.launch_id);
|
|
117
|
+
room = advanceMemberRetirement(roomId, current.role_name, 'archive_secured', retirement.launch_id, archivePath);
|
|
118
|
+
current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
119
|
+
retirement = current.retirement;
|
|
120
|
+
}
|
|
121
|
+
if (retirement.phase === 'archive_secured') {
|
|
122
|
+
await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
|
|
123
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', retirement.launch_id, retirement.archive_path);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
127
|
+
export function acceptManagedRoomClose(roomId) {
|
|
128
|
+
return withFileLock(roomCloseLockPath(roomId), () => beginRoomClose(roomId), {}, CLOSE_LOCK_STALE_MS);
|
|
129
|
+
}
|
|
130
|
+
export function recordManagedRoomCloseError(roomId, error, recoveryHint) {
|
|
131
|
+
return withFileLock(roomCloseLockPath(roomId), () => setRoomCloseError(roomId, error, recoveryHint), {}, CLOSE_LOCK_STALE_MS);
|
|
132
|
+
}
|
|
133
|
+
export async function closeManagedRoom(input) {
|
|
134
|
+
const deps = input.deps ?? {};
|
|
135
|
+
const lock = deps.lock ?? withFileLock;
|
|
136
|
+
return lock(roomCloseLockPath(input.roomId), async () => {
|
|
137
|
+
let room = beginRoomClose(input.roomId);
|
|
138
|
+
if (room.state === 'closed')
|
|
139
|
+
return room;
|
|
140
|
+
try {
|
|
141
|
+
if (room.close?.phase === 'retire_members') {
|
|
142
|
+
for (const seat of room.member_seats) {
|
|
143
|
+
if (seat.retirement?.phase === 'identity_absent')
|
|
144
|
+
continue;
|
|
145
|
+
await retireMember(input.roomId, seat, deps);
|
|
146
|
+
}
|
|
147
|
+
room = advanceRoomClose(input.roomId, 'close_cowork');
|
|
148
|
+
}
|
|
149
|
+
if (room.close?.phase === 'close_cowork') {
|
|
150
|
+
await input.cowork.closeRoom(input.roomId);
|
|
151
|
+
}
|
|
152
|
+
return closeRoom(input.roomId);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
setRoomCloseError(input.roomId, errorText(error), `Retry 'ours-fleet room delete ${input.roomId} ${input.roomId}' or run room recover.`);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
159
|
+
}
|
|
160
|
+
/** Remove retained state written by prerelease builds that stopped at `closed`. */
|
|
161
|
+
export async function deleteLegacyClosedRooms(input) {
|
|
162
|
+
const deleted = [];
|
|
163
|
+
for (const room of listRoomRecords({ state: 'closed' })) {
|
|
164
|
+
await input.cowork.deleteRoom(room.room_id);
|
|
165
|
+
deleteRoomRecord(room.room_id);
|
|
166
|
+
deleted.push(room.room_id);
|
|
167
|
+
}
|
|
168
|
+
return deleted;
|
|
169
|
+
}
|
|
170
|
+
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
171
|
+
export async function deleteManagedRoom(input) {
|
|
172
|
+
await closeManagedRoom(input);
|
|
173
|
+
try {
|
|
174
|
+
await input.cowork.deleteRoom(input.roomId);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
setRoomCloseError(input.roomId, errorText(error), `Retry 'ours-fleet room delete ${input.roomId} ${input.roomId}' or run room recover.`);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
deleteRoomRecord(input.roomId);
|
|
181
|
+
return { room_id: input.roomId, deleted: true };
|
|
182
|
+
}
|
|
@@ -43,10 +43,13 @@ function resolveInvite(owner, vars, path) {
|
|
|
43
43
|
export function validateRoomsConfig(raw, vars, path) {
|
|
44
44
|
if (!isPlainObject(raw))
|
|
45
45
|
throw new RoomsTasksConfigError(path, 'rooms: must be a mapping');
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// Migration-only input: provider selection was exposed before rooms shipped,
|
|
47
|
+
// even though cowork was the sole implementation. Accept the one historical
|
|
48
|
+
// value without carrying it into the current schema or resolved config.
|
|
49
|
+
if (Object.hasOwn(raw, 'provider') && raw.provider !== 'cowork')
|
|
50
|
+
throw new RoomsTasksConfigError(path, 'rooms.provider: only the legacy value \'cowork\' can be migrated; remove this field because ours-cowork is always used');
|
|
51
|
+
const { provider: _legacyProvider, ...current } = raw;
|
|
52
|
+
rejectUnknown(current, RK, path, 'rooms');
|
|
50
53
|
let cowork;
|
|
51
54
|
if (raw.cowork !== undefined) {
|
|
52
55
|
if (!isPlainObject(raw.cowork))
|
|
@@ -83,7 +86,7 @@ export function validateRoomsConfig(raw, vars, path) {
|
|
|
83
86
|
close_when_task_done: raw.defaults.close_when_task_done,
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
|
-
return {
|
|
89
|
+
return { cowork, owner, defaults, _invite: invite };
|
|
87
90
|
}
|
|
88
91
|
export function validateTasksConfig(raw, path) {
|
|
89
92
|
if (!isPlainObject(raw))
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* by Cowork's Unix socket and projects the response into orchestration data.
|
|
6
6
|
*/
|
|
7
7
|
import type { Socket } from 'node:net';
|
|
8
|
+
import type { RoomHistoryEvidence } from './types.js';
|
|
8
9
|
export interface CoworkRoomCreateResult {
|
|
9
10
|
room_id: string;
|
|
10
11
|
identity_name: string;
|
|
@@ -32,6 +33,18 @@ export interface CoworkRoomInfo {
|
|
|
32
33
|
seats: CoworkSeatInfo[];
|
|
33
34
|
goal?: string;
|
|
34
35
|
briefing?: string;
|
|
36
|
+
role_briefings: Record<string, CoworkRoleBriefingInfo>;
|
|
37
|
+
}
|
|
38
|
+
export interface CoworkRoleBriefingInfo {
|
|
39
|
+
role: string;
|
|
40
|
+
text: string;
|
|
41
|
+
version: number;
|
|
42
|
+
updated_at: string;
|
|
43
|
+
}
|
|
44
|
+
export interface CoworkHistoryPage {
|
|
45
|
+
records: RoomHistoryEvidence[];
|
|
46
|
+
raw_count: number;
|
|
47
|
+
next_after: number;
|
|
35
48
|
}
|
|
36
49
|
export interface CoworkAdapter {
|
|
37
50
|
available(): Promise<boolean>;
|
|
@@ -50,9 +63,18 @@ export interface CoworkAdapter {
|
|
|
50
63
|
role: string;
|
|
51
64
|
min_accepts: number;
|
|
52
65
|
}): Promise<CoworkInviteResult>;
|
|
66
|
+
setRoleBriefing(roomId: string, opts: {
|
|
67
|
+
role: string;
|
|
68
|
+
text: string;
|
|
69
|
+
}): Promise<CoworkRoleBriefingInfo>;
|
|
70
|
+
getHistory(roomId: string, opts?: {
|
|
71
|
+
after?: number;
|
|
72
|
+
limit?: number;
|
|
73
|
+
}): Promise<CoworkHistoryPage>;
|
|
53
74
|
getRoom(roomId: string): Promise<CoworkRoomInfo | undefined>;
|
|
54
75
|
listRooms(): Promise<CoworkRoomInfo[]>;
|
|
55
76
|
closeRoom(roomId: string): Promise<void>;
|
|
77
|
+
deleteRoom(roomId: string): Promise<void>;
|
|
56
78
|
getSeats(roomId: string): Promise<CoworkSeatInfo[]>;
|
|
57
79
|
recoverRoom(roomId: string): Promise<CoworkRoomInfo>;
|
|
58
80
|
}
|
|
@@ -65,6 +65,17 @@ function projectRoom(value, operation) {
|
|
|
65
65
|
if (!Array.isArray(room.seats))
|
|
66
66
|
throw new CoworkProtocolError(operation, 'room.seats must be an array');
|
|
67
67
|
const mission = room.mission === undefined ? undefined : object(room.mission, operation, 'room.mission');
|
|
68
|
+
const roleBriefings = room.role_briefings === undefined ? {} : object(room.role_briefings, operation, 'room.role_briefings');
|
|
69
|
+
const projectedBriefings = {};
|
|
70
|
+
for (const [role, value] of Object.entries(roleBriefings)) {
|
|
71
|
+
const briefing = object(value, operation, `room.role_briefings.${role}`);
|
|
72
|
+
projectedBriefings[role] = {
|
|
73
|
+
role,
|
|
74
|
+
text: text(briefing.text, operation, `room.role_briefings.${role}.text`),
|
|
75
|
+
version: positiveInteger(briefing.version, operation, `room.role_briefings.${role}.version`),
|
|
76
|
+
updated_at: string(briefing.updated_at, operation, `room.role_briefings.${role}.updated_at`),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
68
79
|
return {
|
|
69
80
|
room_id: string(room.room_id, operation, 'room.room_id'),
|
|
70
81
|
identity_name: string(room.identity_name, operation, 'room.identity_name'),
|
|
@@ -74,10 +85,82 @@ function projectRoom(value, operation) {
|
|
|
74
85
|
room_name: string(room.room_name, operation, 'room.room_name'),
|
|
75
86
|
state: roomState(room.state, operation),
|
|
76
87
|
seats: room.seats.map((seat) => projectSeat(seat, operation)),
|
|
88
|
+
role_briefings: projectedBriefings,
|
|
77
89
|
...(typeof mission?.goal === 'string' ? { goal: mission.goal } : {}),
|
|
78
90
|
...(typeof mission?.briefing === 'string' ? { briefing: mission.briefing } : {}),
|
|
79
91
|
};
|
|
80
92
|
}
|
|
93
|
+
function positiveInteger(value, operation, label) {
|
|
94
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
95
|
+
throw new CoworkProtocolError(operation, `${label} must be a positive safe integer`);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function stringArray(value, operation, label) {
|
|
99
|
+
if (!Array.isArray(value))
|
|
100
|
+
throw new CoworkProtocolError(operation, `${label} must be an array`);
|
|
101
|
+
return value.map((entry, index) => string(entry, operation, `${label}[${index}]`));
|
|
102
|
+
}
|
|
103
|
+
function projectHistoryRecord(value) {
|
|
104
|
+
const operation = 'room.history';
|
|
105
|
+
const record = object(value, operation, 'record');
|
|
106
|
+
const kind = string(record.kind, operation, 'record.kind');
|
|
107
|
+
const common = {
|
|
108
|
+
seq: positiveInteger(record.seq, operation, 'record.seq'),
|
|
109
|
+
record_id: string(record.record_id, operation, 'record.record_id'),
|
|
110
|
+
at: string(record.at, operation, 'record.at'),
|
|
111
|
+
};
|
|
112
|
+
if (kind === 'message') {
|
|
113
|
+
if (record.category !== 'role_briefing' && record.category !== 'chat')
|
|
114
|
+
return undefined;
|
|
115
|
+
const author = object(record.author, operation, 'record.author');
|
|
116
|
+
return {
|
|
117
|
+
kind, ...common,
|
|
118
|
+
message_id: string(record.message_id, operation, 'record.message_id'),
|
|
119
|
+
category: record.category,
|
|
120
|
+
author: {
|
|
121
|
+
identity: string(author.identity, operation, 'record.author.identity'),
|
|
122
|
+
display_name: string(author.display_name, operation, 'record.author.display_name'),
|
|
123
|
+
role: string(author.role, operation, 'record.author.role'),
|
|
124
|
+
},
|
|
125
|
+
text: text(record.text, operation, 'record.text'),
|
|
126
|
+
recipient_identities: stringArray(record.recipient_identities, operation, 'record.recipient_identities'),
|
|
127
|
+
...(record.briefing_role === undefined ? {} : {
|
|
128
|
+
briefing_role: string(record.briefing_role, operation, 'record.briefing_role'),
|
|
129
|
+
}),
|
|
130
|
+
...(record.briefing_version === undefined ? {} : {
|
|
131
|
+
briefing_version: positiveInteger(record.briefing_version, operation, 'record.briefing_version'),
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (kind === 'relay_intent') {
|
|
136
|
+
if (record.message_id === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
return {
|
|
139
|
+
kind, ...common,
|
|
140
|
+
message_id: string(record.message_id, operation, 'record.message_id'),
|
|
141
|
+
recipient_identity: string(record.recipient_identity, operation, 'record.recipient_identity'),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (kind === 'relay_result') {
|
|
145
|
+
if (record.message_id === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (record.status !== 'queued' && record.status !== 'send_failed'
|
|
148
|
+
&& record.status !== 'skipped_removed') {
|
|
149
|
+
throw new CoworkProtocolError(operation, 'record.status is invalid');
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
kind, ...common,
|
|
153
|
+
intent_record_id: string(record.intent_record_id, operation, 'record.intent_record_id'),
|
|
154
|
+
message_id: string(record.message_id, operation, 'record.message_id'),
|
|
155
|
+
recipient_identity: string(record.recipient_identity, operation, 'record.recipient_identity'),
|
|
156
|
+
status: record.status,
|
|
157
|
+
...(record.wire_id === undefined ? {} : {
|
|
158
|
+
wire_id: string(record.wire_id, operation, 'record.wire_id'),
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
81
164
|
/** Resolve the same config/state inputs as ours-cowork and return its Unix socket. */
|
|
82
165
|
export function resolveCoworkSocketPath(options = {}) {
|
|
83
166
|
if (options.socketPath)
|
|
@@ -239,6 +322,36 @@ export function createCoworkAdapter(options = {}) {
|
|
|
239
322
|
? invite.min_accepts : opts.min_accepts,
|
|
240
323
|
};
|
|
241
324
|
},
|
|
325
|
+
async setRoleBriefing(roomId, opts) {
|
|
326
|
+
const result = object(await call('room.briefing.role.set', {
|
|
327
|
+
room_id: roomId, role: opts.role, text: opts.text,
|
|
328
|
+
}), 'room.briefing.role.set', 'room');
|
|
329
|
+
const briefings = object(result.role_briefings, 'room.briefing.role.set', 'room.role_briefings');
|
|
330
|
+
const briefing = object(briefings[opts.role], 'room.briefing.role.set', `room.role_briefings.${opts.role}`);
|
|
331
|
+
return {
|
|
332
|
+
role: opts.role,
|
|
333
|
+
text: text(briefing.text, 'room.briefing.role.set', 'briefing.text'),
|
|
334
|
+
version: positiveInteger(briefing.version, 'room.briefing.role.set', 'briefing.version'),
|
|
335
|
+
updated_at: string(briefing.updated_at, 'room.briefing.role.set', 'briefing.updated_at'),
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
async getHistory(roomId, opts = {}) {
|
|
339
|
+
const result = await call('room.history', {
|
|
340
|
+
room_id: roomId,
|
|
341
|
+
...(opts.after === undefined ? {} : { after: opts.after }),
|
|
342
|
+
...(opts.limit === undefined ? {} : { limit: opts.limit }),
|
|
343
|
+
view: 'operator',
|
|
344
|
+
});
|
|
345
|
+
if (!Array.isArray(result))
|
|
346
|
+
throw new CoworkProtocolError('room.history', 'result must be an array');
|
|
347
|
+
const raw = result.map(record => object(record, 'room.history', 'record'));
|
|
348
|
+
const records = raw.flatMap(record => {
|
|
349
|
+
const projected = projectHistoryRecord(record);
|
|
350
|
+
return projected ? [projected] : [];
|
|
351
|
+
});
|
|
352
|
+
const next_after = raw.length === 0 ? (opts.after ?? 0) : Math.max(...raw.map(record => positiveInteger(record.seq, 'room.history', 'record.seq')));
|
|
353
|
+
return { records, raw_count: raw.length, next_after };
|
|
354
|
+
},
|
|
242
355
|
async getRoom(roomId) {
|
|
243
356
|
try {
|
|
244
357
|
return projectRoom(await call('room.show', { room_id: roomId }), 'room.show');
|
|
@@ -256,6 +369,9 @@ export function createCoworkAdapter(options = {}) {
|
|
|
256
369
|
return result.map((room) => projectRoom(room, 'room.list'));
|
|
257
370
|
},
|
|
258
371
|
async closeRoom(roomId) { await call('room.close', { room_id: roomId }); },
|
|
372
|
+
async deleteRoom(roomId) {
|
|
373
|
+
await call('room.delete', { room_id: roomId, confirm: true });
|
|
374
|
+
},
|
|
259
375
|
async getSeats(roomId) {
|
|
260
376
|
const result = await call('room.participants', { room_id: roomId });
|
|
261
377
|
if (!Array.isArray(result))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
4
|
+
export function launchFleetWorker(args, operation, configPath) {
|
|
5
|
+
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
6
|
+
const inheritedKeys = [
|
|
7
|
+
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
|
|
8
|
+
'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
|
|
9
|
+
];
|
|
10
|
+
const inherited = inheritedKeys
|
|
11
|
+
.flatMap(key => process.env[key] === undefined ? [] : [`${key}=${process.env[key]}`]);
|
|
12
|
+
const suffix = randomUUID().slice(0, 8);
|
|
13
|
+
const safeOperation = operation.replace(/[^A-Za-z0-9_.-]/g, '-');
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (process.env.OURS_FLEET_SUPERVISOR !== 'none' && process.platform === 'linux') {
|
|
16
|
+
execFile('systemd-run', [
|
|
17
|
+
'--user', '--quiet', '--collect',
|
|
18
|
+
`--unit=ours-fleet-${safeOperation}-${suffix}`,
|
|
19
|
+
'--property=Type=exec', '--property=KillMode=control-group',
|
|
20
|
+
...inherited.map(value => `--setenv=${value}`),
|
|
21
|
+
process.execPath, ...cli,
|
|
22
|
+
], { timeout: 15_000 }, error => error ? reject(error) : resolve());
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (process.env.OURS_FLEET_SUPERVISOR !== 'none' && process.platform === 'darwin') {
|
|
26
|
+
execFile('launchctl', [
|
|
27
|
+
'submit', '-l', `network.ours.fleet.${safeOperation}.${suffix}`, '--',
|
|
28
|
+
'/usr/bin/env', ...inherited, process.execPath, ...cli,
|
|
29
|
+
], { timeout: 15_000 }, error => error ? reject(error) : resolve());
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const env = Object.fromEntries(inherited.map(value => {
|
|
33
|
+
const split = value.indexOf('=');
|
|
34
|
+
return [value.slice(0, split), value.slice(split + 1)];
|
|
35
|
+
}));
|
|
36
|
+
const child = spawn(process.execPath, cli, { detached: true, stdio: 'ignore', env });
|
|
37
|
+
child.once('error', reject);
|
|
38
|
+
child.once('spawn', () => { child.unref(); resolve(); });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -5,4 +5,6 @@ export * from './task-state.js';
|
|
|
5
5
|
export * from './room-state.js';
|
|
6
6
|
export * from './cowork-adapter.js';
|
|
7
7
|
export { provisionMembers, cleanupMembers } from './provision.js';
|
|
8
|
+
export { acceptManagedRoomClose, closeManagedRoom, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from './close.js';
|
|
9
|
+
export { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from './terminal.js';
|
|
8
10
|
export { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './cli.js';
|
|
@@ -5,4 +5,6 @@ export * from './task-state.js';
|
|
|
5
5
|
export * from './room-state.js';
|
|
6
6
|
export * from './cowork-adapter.js';
|
|
7
7
|
export { provisionMembers, cleanupMembers } from './provision.js';
|
|
8
|
+
export { acceptManagedRoomClose, closeManagedRoom, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from './close.js';
|
|
9
|
+
export { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from './terminal.js';
|
|
8
10
|
export { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './cli.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RoomOrchestrationState, TaskState } from './types.js';
|
|
2
|
+
export declare const MARKDOWN_MAX_CODE_POINTS = 3500;
|
|
3
|
+
export declare const MARKDOWN_MAX_BYTES = 12000;
|
|
4
|
+
export interface StatusPresentation {
|
|
5
|
+
icon: string;
|
|
6
|
+
word: string;
|
|
7
|
+
}
|
|
8
|
+
export type MarkdownField = {
|
|
9
|
+
label: string;
|
|
10
|
+
value: unknown;
|
|
11
|
+
kind?: 'prose' | 'code' | 'markdown';
|
|
12
|
+
multiline?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type MarkdownSection = {
|
|
15
|
+
heading?: string;
|
|
16
|
+
items?: unknown[];
|
|
17
|
+
markdownItems?: string[];
|
|
18
|
+
prose?: unknown;
|
|
19
|
+
};
|
|
20
|
+
export declare function withinMarkdownBounds(value: string): boolean;
|
|
21
|
+
/** Escaped single-line prose suitable for a field or list item. */
|
|
22
|
+
export declare function markdownProse(value: unknown): string;
|
|
23
|
+
/** Escaped heading text. Newlines and heading/list syntax cannot escape the heading. */
|
|
24
|
+
export declare function markdownHeading(value: unknown): string;
|
|
25
|
+
/** Escaped multiline prose. Intended line breaks remain line breaks. */
|
|
26
|
+
export declare function markdownMultiline(value: unknown): string;
|
|
27
|
+
/** CommonMark code span with a delimiter longer than every run in the value. */
|
|
28
|
+
export declare function markdownCode(value: unknown): string;
|
|
29
|
+
export declare function taskStatus(value: TaskState | string): string;
|
|
30
|
+
export declare function roomStatus(value: RoomOrchestrationState | string): string;
|
|
31
|
+
export declare function renderMarkdownResult(input: {
|
|
32
|
+
icon: string;
|
|
33
|
+
title: string;
|
|
34
|
+
fields?: MarkdownField[];
|
|
35
|
+
sections?: MarkdownSection[];
|
|
36
|
+
}): string;
|
|
37
|
+
export declare function renderMarkdownList(input: {
|
|
38
|
+
icon: string;
|
|
39
|
+
title: string;
|
|
40
|
+
empty: string;
|
|
41
|
+
records: string[];
|
|
42
|
+
}): string;
|
|
43
|
+
export type FailureKind = 'usage' | 'not_found' | 'conflict' | 'state' | 'pending' | 'unexpected';
|
|
44
|
+
export declare function renderMarkdownFailure(input: {
|
|
45
|
+
kind: FailureKind;
|
|
46
|
+
subject: string;
|
|
47
|
+
detail?: unknown;
|
|
48
|
+
action: string;
|
|
49
|
+
}): string;
|