@ours.network/fleet 0.18.1 → 0.19.0-nightly.2

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.
Files changed (79) hide show
  1. package/README.md +57 -91
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +5 -5
  7. package/dist/cli.js +12 -37
  8. package/dist/config.d.ts +8 -6
  9. package/dist/config.js +46 -30
  10. package/dist/creation.d.ts +38 -22
  11. package/dist/creation.js +111 -24
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +44 -95
  14. package/dist/doctor.d.ts +5 -1
  15. package/dist/doctor.js +18 -11
  16. package/dist/fleet-proxy.d.ts +0 -5
  17. package/dist/harness/acp-agent.js +6 -11
  18. package/dist/harness/claude-code.js +11 -200
  19. package/dist/harness/codex.d.ts +1 -4
  20. package/dist/harness/codex.js +12 -70
  21. package/dist/harness/types.d.ts +4 -54
  22. package/dist/loops/manager.d.ts +1 -30
  23. package/dist/loops/manager.js +6 -69
  24. package/dist/loops/state.d.ts +0 -18
  25. package/dist/loops/state.js +0 -4
  26. package/dist/monitor.js +1 -1
  27. package/dist/ops.d.ts +3 -0
  28. package/dist/ops.js +8 -3
  29. package/dist/owner-channel/attachments.d.ts +25 -2
  30. package/dist/owner-channel/attachments.js +61 -5
  31. package/dist/owner-channel/channel.d.ts +29 -30
  32. package/dist/owner-channel/channel.js +291 -291
  33. package/dist/owner-channel/commands.js +89 -0
  34. package/dist/owner-channel/message-recovery.d.ts +25 -0
  35. package/dist/owner-channel/message-recovery.js +114 -0
  36. package/dist/owner-channel/notices.d.ts +0 -7
  37. package/dist/owner-channel/notices.js +0 -9
  38. package/dist/owner-channel/ours-client.d.ts +148 -0
  39. package/dist/owner-channel/ours-client.js +231 -0
  40. package/dist/rooms-tasks/cli.d.ts +4 -0
  41. package/dist/rooms-tasks/cli.js +565 -0
  42. package/dist/rooms-tasks/config.d.ts +15 -0
  43. package/dist/rooms-tasks/config.js +171 -0
  44. package/dist/rooms-tasks/cowork-adapter.d.ts +80 -0
  45. package/dist/rooms-tasks/cowork-adapter.js +48 -0
  46. package/dist/rooms-tasks/index.d.ts +7 -0
  47. package/dist/rooms-tasks/index.js +7 -0
  48. package/dist/rooms-tasks/room-state.d.ts +22 -0
  49. package/dist/rooms-tasks/room-state.js +117 -0
  50. package/dist/rooms-tasks/task-state.d.ts +31 -0
  51. package/dist/rooms-tasks/task-state.js +173 -0
  52. package/dist/rooms-tasks/templates.d.ts +6 -0
  53. package/dist/rooms-tasks/templates.js +80 -0
  54. package/dist/rooms-tasks/types.d.ts +153 -0
  55. package/dist/rooms-tasks/types.js +20 -0
  56. package/dist/runner.d.ts +0 -48
  57. package/dist/runner.js +94 -236
  58. package/dist/session/acp.d.ts +0 -104
  59. package/dist/session/acp.js +10 -213
  60. package/dist/session/conversation-normalizer.d.ts +0 -6
  61. package/dist/session/conversation-normalizer.js +10 -153
  62. package/dist/session/conversation-types.d.ts +4 -23
  63. package/dist/session/types.d.ts +0 -35
  64. package/dist/spawn.js +26 -33
  65. package/dist/supervisor/systemd.js +29 -2
  66. package/dist/watchdog/briefing.js +0 -7
  67. package/dist/watchdog/run.js +3 -3
  68. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  69. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  70. package/dist/web-app/index.html +1 -1
  71. package/dist/worklog.d.ts +1 -7
  72. package/dist/worklog.js +39 -191
  73. package/package.json +3 -1
  74. package/dist/model-env.d.ts +0 -71
  75. package/dist/model-env.js +0 -106
  76. package/dist/owner-channel/mcp.d.ts +0 -24
  77. package/dist/owner-channel/mcp.js +0 -145
  78. package/dist/session/activity.d.ts +0 -31
  79. package/dist/session/activity.js +0 -48
@@ -0,0 +1,171 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, TEMPLATE_OVERRIDE_KEYS as TOK, } from './types.js';
4
+ import { BUILTIN_TEMPLATES } from './templates.js';
5
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
6
+ const CID_RE = /^[0-9a-fA-F]{64}$/;
7
+ export class RoomsTasksConfigError extends Error {
8
+ path;
9
+ constructor(path, message) {
10
+ super(`${path}: ${message}`);
11
+ this.path = path;
12
+ }
13
+ }
14
+ function rejectUnknown(obj, allowed, path, section) {
15
+ const bad = Object.keys(obj).filter(k => !allowed.includes(k));
16
+ if (bad.length)
17
+ throw new RoomsTasksConfigError(path, `${section} has unknown key(s) ${bad.join(', ')}; allowed: ${allowed.join(', ')}`);
18
+ }
19
+ export function fingerprint(invite) {
20
+ return createHash('sha256').update(invite.trim()).digest('hex');
21
+ }
22
+ function resolveInvite(owner, vars, path) {
23
+ const inline = owner.public_invite;
24
+ const file = owner.public_invite_file;
25
+ if (inline && file)
26
+ throw new RoomsTasksConfigError(path, 'rooms.owner: exactly one of public_invite or public_invite_file');
27
+ if (!inline && !file)
28
+ return undefined;
29
+ let raw;
30
+ if (file) {
31
+ const resolved = file.replace(/\$\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
32
+ if (!existsSync(resolved))
33
+ throw new RoomsTasksConfigError(path, `rooms.owner.public_invite_file: not found: ${resolved}`);
34
+ raw = readFileSync(resolved, 'utf8').trim();
35
+ }
36
+ else {
37
+ raw = inline;
38
+ }
39
+ if (!raw)
40
+ throw new RoomsTasksConfigError(path, 'rooms.owner: invite is empty');
41
+ return { value: raw, fingerprint: fingerprint(raw) };
42
+ }
43
+ export function validateRoomsConfig(raw, vars, path) {
44
+ if (!isPlainObject(raw))
45
+ throw new RoomsTasksConfigError(path, 'rooms: must be a mapping');
46
+ rejectUnknown(raw, RK, path, 'rooms');
47
+ const provider = raw.provider ?? 'cowork';
48
+ if (provider !== 'cowork')
49
+ throw new RoomsTasksConfigError(path, `rooms.provider: unsupported '${provider}'; allowed: cowork`);
50
+ let cowork;
51
+ if (raw.cowork !== undefined) {
52
+ if (!isPlainObject(raw.cowork))
53
+ throw new RoomsTasksConfigError(path, 'rooms.cowork: must be a mapping');
54
+ rejectUnknown(raw.cowork, RCK, path, 'rooms.cowork');
55
+ cowork = { config: raw.cowork.config };
56
+ }
57
+ if (!isPlainObject(raw.owner))
58
+ throw new RoomsTasksConfigError(path, 'rooms.owner: required and must be a mapping');
59
+ rejectUnknown(raw.owner, ROK, path, 'rooms.owner');
60
+ const ownerProvider = raw.owner.provider ?? 'messenger-server';
61
+ const expectedCid = raw.owner.expected_cid;
62
+ if (!expectedCid)
63
+ throw new RoomsTasksConfigError(path, 'rooms.owner.expected_cid: required');
64
+ if (!CID_RE.test(expectedCid))
65
+ throw new RoomsTasksConfigError(path, 'rooms.owner.expected_cid: must be exactly 64 hexadecimal characters');
66
+ const ownerRole = raw.owner.role ?? 'Owner';
67
+ const invite = resolveInvite(raw.owner, vars, path);
68
+ const owner = {
69
+ provider: ownerProvider,
70
+ expected_cid: expectedCid.toLowerCase(),
71
+ role: ownerRole,
72
+ ...(raw.owner.public_invite ? { public_invite: '[REDACTED]' } : {}),
73
+ ...(raw.owner.public_invite_file ? { public_invite_file: raw.owner.public_invite_file } : {}),
74
+ };
75
+ let defaults;
76
+ if (raw.defaults !== undefined) {
77
+ if (!isPlainObject(raw.defaults))
78
+ throw new RoomsTasksConfigError(path, 'rooms.defaults: must be a mapping');
79
+ rejectUnknown(raw.defaults, RDK, path, 'rooms.defaults');
80
+ defaults = {
81
+ template: raw.defaults.template,
82
+ attach_owner: raw.defaults.attach_owner,
83
+ close_when_task_done: raw.defaults.close_when_task_done,
84
+ };
85
+ }
86
+ return { provider, cowork, owner, defaults, _invite: invite };
87
+ }
88
+ export function validateTasksConfig(raw, path) {
89
+ if (!isPlainObject(raw))
90
+ throw new RoomsTasksConfigError(path, 'tasks: must be a mapping');
91
+ rejectUnknown(raw, TK, path, 'tasks');
92
+ const createMode = raw.create_mode;
93
+ if (createMode !== undefined && createMode !== 'start' && createMode !== 'backlog')
94
+ throw new RoomsTasksConfigError(path, `tasks.create_mode: must be 'start' or 'backlog'`);
95
+ if (raw.close_room_on_done !== undefined && typeof raw.close_room_on_done !== 'boolean')
96
+ throw new RoomsTasksConfigError(path, 'tasks.close_room_on_done: must be a boolean');
97
+ if (raw.retain_completed_for !== undefined && typeof raw.retain_completed_for !== 'string')
98
+ throw new RoomsTasksConfigError(path, 'tasks.retain_completed_for: must be a duration string');
99
+ return {
100
+ default_room_template: raw.default_room_template,
101
+ create_mode: createMode,
102
+ close_room_on_done: raw.close_room_on_done,
103
+ retain_completed_for: raw.retain_completed_for,
104
+ };
105
+ }
106
+ export function validateRoomTemplatesConfig(raw, path) {
107
+ if (!isPlainObject(raw))
108
+ throw new RoomsTasksConfigError(path, 'room_templates: must be a mapping');
109
+ const result = {};
110
+ for (const [name, tplRaw] of Object.entries(raw)) {
111
+ if (!/^[a-z0-9_-]+$/.test(name))
112
+ throw new RoomsTasksConfigError(path, `room_templates: invalid template name '${name}' (allowed: [a-z0-9_-])`);
113
+ if (!isPlainObject(tplRaw))
114
+ throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
115
+ rejectUnknown(tplRaw, [...TPK, 'name'], path, `room_templates.${name}`);
116
+ const isBuiltinOverride = BUILTIN_TEMPLATES.some(b => b.name === name);
117
+ if (isBuiltinOverride && !tplRaw.override_builtin)
118
+ throw new RoomsTasksConfigError(path, `room_templates.${name}: overrides built-in template; set override_builtin: true and bump version`);
119
+ const version = tplRaw.version;
120
+ if (version === undefined || !Number.isInteger(version) || version < 1)
121
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
122
+ if (!tplRaw.description || typeof tplRaw.description !== 'string')
123
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.description: required string`);
124
+ if (!Array.isArray(tplRaw.members) || !tplRaw.members.length)
125
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members: required non-empty array`);
126
+ const members = tplRaw.members.map((m, i) => {
127
+ if (!isPlainObject(m))
128
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}]: must be a mapping`);
129
+ rejectUnknown(m, TMK, path, `room_templates.${name}.members[${i}]`);
130
+ if (!m.slot || typeof m.slot !== 'string')
131
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].slot: required string`);
132
+ if (!m.role || typeof m.role !== 'string')
133
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
134
+ if (!Number.isInteger(m.count) || m.count < 1)
135
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
136
+ if (!m.role_ref || typeof m.role_ref !== 'string')
137
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role_ref: required string`);
138
+ if (m.overrides !== undefined) {
139
+ if (!isPlainObject(m.overrides))
140
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].overrides: must be a mapping`);
141
+ rejectUnknown(m.overrides, TOK, path, `room_templates.${name}.members[${i}].overrides`);
142
+ }
143
+ return {
144
+ slot: m.slot,
145
+ role: m.role,
146
+ count: m.count,
147
+ role_ref: m.role_ref,
148
+ overrides: m.overrides,
149
+ };
150
+ });
151
+ let room;
152
+ if (tplRaw.room !== undefined) {
153
+ if (!isPlainObject(tplRaw.room))
154
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.room: must be a mapping`);
155
+ room = {
156
+ quiet_membership: tplRaw.room.quiet_membership,
157
+ anonymous: tplRaw.room.anonymous,
158
+ };
159
+ }
160
+ result[name] = {
161
+ name,
162
+ version: version,
163
+ description: tplRaw.description,
164
+ room,
165
+ contract: tplRaw.contract,
166
+ members,
167
+ };
168
+ }
169
+ return result;
170
+ }
171
+ export const FLEET_D_ALLOWED_KEYS = ['roles', 'rooms', 'room_templates', 'tasks'];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Cowork private management-socket adapter.
3
+ *
4
+ * Fleet orchestrates tasks/rooms but Cowork owns room state. This adapter
5
+ * communicates with Cowork via its Unix management socket using versioned
6
+ * JSON RPC. All methods fail closed: if Cowork is unreachable or returns
7
+ * an unexpected response, the operation fails rather than guessing.
8
+ *
9
+ * BLOCKED: The management socket protocol is defined in ours-cowork but
10
+ * the exact prerelease wire format needs verification. This adapter
11
+ * defines the interface Fleet needs and provides a fail-closed stub
12
+ * that reports the blocker. Replace the stub with socket calls once
13
+ * the Cowork management socket contract is verified.
14
+ */
15
+ export interface CoworkRoomCreateResult {
16
+ room_id: string;
17
+ identity_name: string;
18
+ identity_cid: string;
19
+ }
20
+ export interface CoworkInviteAcceptResult {
21
+ seat_cid: string;
22
+ seat_state: 'pending' | 'active';
23
+ }
24
+ export interface CoworkInviteResult {
25
+ invite: string;
26
+ min_accepts: number;
27
+ }
28
+ export interface CoworkSeatInfo {
29
+ identity_cid: string;
30
+ role: string;
31
+ seat_state: 'pending' | 'active' | 'removed';
32
+ }
33
+ export interface CoworkRoomInfo {
34
+ room_id: string;
35
+ identity_name: string;
36
+ identity_cid: string;
37
+ room_name: string;
38
+ state: 'provisioning' | 'active' | 'closing' | 'closed';
39
+ seats: CoworkSeatInfo[];
40
+ goal?: string;
41
+ briefing?: string;
42
+ }
43
+ export interface CoworkAdapter {
44
+ available(): Promise<boolean>;
45
+ createRoom(opts: {
46
+ room_name: string;
47
+ goal?: string;
48
+ briefing?: string;
49
+ quiet_membership?: boolean;
50
+ anonymous?: boolean;
51
+ }): Promise<CoworkRoomCreateResult>;
52
+ acceptInvite(roomId: string, invite: string, opts: {
53
+ role: string;
54
+ expected_cid: string;
55
+ }): Promise<CoworkInviteAcceptResult>;
56
+ issueInvite(roomId: string, opts: {
57
+ role: string;
58
+ min_accepts: number;
59
+ }): Promise<CoworkInviteResult>;
60
+ acceptAsIdentity(roomId: string, invite: string, identityCid: string, opts: {
61
+ role: string;
62
+ }): Promise<CoworkInviteAcceptResult>;
63
+ getRoom(roomId: string): Promise<CoworkRoomInfo | undefined>;
64
+ listRooms(): Promise<CoworkRoomInfo[]>;
65
+ closeRoom(roomId: string): Promise<void>;
66
+ getSeats(roomId: string): Promise<CoworkSeatInfo[]>;
67
+ }
68
+ export declare class CoworkUnavailableError extends Error {
69
+ constructor(message?: string);
70
+ }
71
+ export declare class CoworkProtocolError extends Error {
72
+ readonly operation: string;
73
+ constructor(operation: string, message: string);
74
+ }
75
+ /**
76
+ * Fail-closed stub adapter. Every method throws CoworkUnavailableError.
77
+ * This is the correct behavior until the Cowork management socket
78
+ * integration is verified against the prerelease wire format.
79
+ */
80
+ export declare function createStubAdapter(): CoworkAdapter;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Cowork private management-socket adapter.
3
+ *
4
+ * Fleet orchestrates tasks/rooms but Cowork owns room state. This adapter
5
+ * communicates with Cowork via its Unix management socket using versioned
6
+ * JSON RPC. All methods fail closed: if Cowork is unreachable or returns
7
+ * an unexpected response, the operation fails rather than guessing.
8
+ *
9
+ * BLOCKED: The management socket protocol is defined in ours-cowork but
10
+ * the exact prerelease wire format needs verification. This adapter
11
+ * defines the interface Fleet needs and provides a fail-closed stub
12
+ * that reports the blocker. Replace the stub with socket calls once
13
+ * the Cowork management socket contract is verified.
14
+ */
15
+ export class CoworkUnavailableError extends Error {
16
+ constructor(message = 'Cowork management socket is not reachable') {
17
+ super(message);
18
+ }
19
+ }
20
+ export class CoworkProtocolError extends Error {
21
+ operation;
22
+ constructor(operation, message) {
23
+ super(`cowork ${operation}: ${message}`);
24
+ this.operation = operation;
25
+ }
26
+ }
27
+ /**
28
+ * Fail-closed stub adapter. Every method throws CoworkUnavailableError.
29
+ * This is the correct behavior until the Cowork management socket
30
+ * integration is verified against the prerelease wire format.
31
+ */
32
+ export function createStubAdapter() {
33
+ const unavailable = (op) => {
34
+ throw new CoworkUnavailableError(`Cowork adapter not yet connected: ${op}. ` +
35
+ 'Blocker: Cowork management socket wire format must be verified against ours-cowork/prerelease.');
36
+ };
37
+ return {
38
+ async available() { return false; },
39
+ async createRoom() { return unavailable('createRoom'); },
40
+ async acceptInvite() { return unavailable('acceptInvite'); },
41
+ async issueInvite() { return unavailable('issueInvite'); },
42
+ async acceptAsIdentity() { return unavailable('acceptAsIdentity'); },
43
+ async getRoom() { return unavailable('getRoom'); },
44
+ async listRooms() { return unavailable('listRooms'); },
45
+ async closeRoom() { return unavailable('closeRoom'); },
46
+ async getSeats() { return unavailable('getSeats'); },
47
+ };
48
+ }
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './templates.js';
3
+ export * from './config.js';
4
+ export * from './task-state.js';
5
+ export * from './room-state.js';
6
+ export * from './cowork-adapter.js';
7
+ export { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './cli.js';
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './templates.js';
3
+ export * from './config.js';
4
+ export * from './task-state.js';
5
+ export * from './room-state.js';
6
+ export * from './cowork-adapter.js';
7
+ export { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './cli.js';
@@ -0,0 +1,22 @@
1
+ import type { RoomOrchestrationRecord, RoomOrchestrationState, SagaPhase, RoomMemberSeat, ProvisioningDetail } from './types.js';
2
+ export declare const roomsDir: () => string;
3
+ export declare class RoomStateError extends Error {
4
+ }
5
+ export interface CreateRoomInput {
6
+ room_id: string;
7
+ room_name: string;
8
+ task_id?: string;
9
+ template_snapshot?: import('./types.js').TemplateSnapshot;
10
+ }
11
+ export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
12
+ export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
13
+ export declare function listRoomRecords(filter?: {
14
+ state?: RoomOrchestrationState | RoomOrchestrationState[];
15
+ }): RoomOrchestrationRecord[];
16
+ export declare function advanceSaga(id: string, phase: SagaPhase, stepIndex?: number, detail?: ProvisioningDetail): RoomOrchestrationRecord;
17
+ export declare function setSagaError(id: string, error: string, hint?: string, detail?: ProvisioningDetail): RoomOrchestrationRecord;
18
+ export declare function setRoomIdentity(id: string, roomIdentityCid: string): RoomOrchestrationRecord;
19
+ export declare function setOwnerSeat(id: string, ownerSeatCid: string, inviteFingerprint: string): RoomOrchestrationRecord;
20
+ export declare function updateMemberSeats(id: string, seats: RoomMemberSeat[]): RoomOrchestrationRecord;
21
+ export declare function activateRoom(id: string): RoomOrchestrationRecord;
22
+ export declare function closeRoom(id: string): RoomOrchestrationRecord;
@@ -0,0 +1,117 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { replaceFileAtomically } from '../atomic-file.js';
4
+ import { stateRoot } from '../paths.js';
5
+ export const roomsDir = () => join(stateRoot(), 'rooms');
6
+ function roomPath(id) { return join(roomsDir(), `${id}.json`); }
7
+ export class RoomStateError extends Error {
8
+ }
9
+ function readRoom(id) {
10
+ const p = roomPath(id);
11
+ if (!existsSync(p))
12
+ throw new RoomStateError(`room not found: ${id}`);
13
+ return JSON.parse(readFileSync(p, 'utf8'));
14
+ }
15
+ function writeRoom(record) {
16
+ mkdirSync(roomsDir(), { recursive: true });
17
+ replaceFileAtomically(roomPath(record.room_id), JSON.stringify(record, null, 2) + '\n');
18
+ }
19
+ export function createRoomRecord(input) {
20
+ const existing = getRoomRecord(input.room_id);
21
+ if (existing)
22
+ return existing;
23
+ const record = {
24
+ room_id: input.room_id,
25
+ room_name: input.room_name,
26
+ task_id: input.task_id,
27
+ template_snapshot: input.template_snapshot,
28
+ saga: { phase: 'persist_intent', step_index: 0 },
29
+ member_seats: [],
30
+ state: 'provisioning',
31
+ created_at: new Date().toISOString(),
32
+ };
33
+ writeRoom(record);
34
+ return record;
35
+ }
36
+ export function getRoomRecord(id) {
37
+ try {
38
+ return readRoom(id);
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ export function listRoomRecords(filter) {
45
+ const dir = roomsDir();
46
+ if (!existsSync(dir))
47
+ return [];
48
+ const states = filter?.state
49
+ ? (Array.isArray(filter.state) ? filter.state : [filter.state])
50
+ : undefined;
51
+ const rooms = [];
52
+ for (const f of readdirSync(dir).filter(f => f.endsWith('.json'))) {
53
+ try {
54
+ const r = JSON.parse(readFileSync(join(dir, f), 'utf8'));
55
+ if (!states || states.includes(r.state))
56
+ rooms.push(r);
57
+ }
58
+ catch { /* skip corrupt */ }
59
+ }
60
+ return rooms.sort((a, b) => a.created_at.localeCompare(b.created_at));
61
+ }
62
+ export function advanceSaga(id, phase, stepIndex, detail) {
63
+ const r = readRoom(id);
64
+ r.saga = { phase, step_index: stepIndex ?? 0 };
65
+ if (detail)
66
+ r.provisioning_detail = detail;
67
+ else
68
+ delete r.provisioning_detail;
69
+ writeRoom(r);
70
+ return r;
71
+ }
72
+ export function setSagaError(id, error, hint, detail) {
73
+ const r = readRoom(id);
74
+ r.saga.error = error;
75
+ r.saga.recovery_hint = hint;
76
+ if (detail)
77
+ r.provisioning_detail = detail;
78
+ writeRoom(r);
79
+ return r;
80
+ }
81
+ export function setRoomIdentity(id, roomIdentityCid) {
82
+ const r = readRoom(id);
83
+ r.room_identity_cid = roomIdentityCid;
84
+ writeRoom(r);
85
+ return r;
86
+ }
87
+ export function setOwnerSeat(id, ownerSeatCid, inviteFingerprint) {
88
+ const r = readRoom(id);
89
+ r.owner_seat_cid = ownerSeatCid;
90
+ r.owner_invite_fingerprint = inviteFingerprint;
91
+ writeRoom(r);
92
+ return r;
93
+ }
94
+ export function updateMemberSeats(id, seats) {
95
+ const r = readRoom(id);
96
+ r.member_seats = seats;
97
+ writeRoom(r);
98
+ return r;
99
+ }
100
+ export function activateRoom(id) {
101
+ const r = readRoom(id);
102
+ r.state = 'active';
103
+ r.saga = { phase: 'completed', step_index: 0 };
104
+ delete r.provisioning_detail;
105
+ r.activated_at = new Date().toISOString();
106
+ writeRoom(r);
107
+ return r;
108
+ }
109
+ export function closeRoom(id) {
110
+ const r = readRoom(id);
111
+ if (r.state === 'closed')
112
+ return r;
113
+ r.state = 'closed';
114
+ r.closed_at = new Date().toISOString();
115
+ writeRoom(r);
116
+ return r;
117
+ }
@@ -0,0 +1,31 @@
1
+ import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole } from './types.js';
2
+ export declare const tasksDir: () => string;
3
+ export declare class TaskStateError extends Error {
4
+ }
5
+ export interface CreateTaskInput {
6
+ title: string;
7
+ brief?: string;
8
+ brief_file?: string;
9
+ template?: TaskTemplateRef;
10
+ origin: TaskOrigin;
11
+ idempotency_key?: string;
12
+ start?: boolean;
13
+ room_id?: string;
14
+ }
15
+ export declare function createTask(input: CreateTaskInput): TaskRecord;
16
+ export declare function getTask(id: string): TaskRecord;
17
+ export declare function listTasks(filter?: {
18
+ state?: TaskState | TaskState[];
19
+ }): TaskRecord[];
20
+ export declare function findByIdempotencyKey(key: string): TaskRecord | undefined;
21
+ export declare function startTask(id: string): TaskRecord;
22
+ export declare function activateTask(id: string): TaskRecord;
23
+ export declare function blockTask(id: string, reason: string): TaskRecord;
24
+ export declare function unblockTask(id: string): TaskRecord;
25
+ export declare function reviewTask(id: string): TaskRecord;
26
+ export declare function completeTask(id: string, outcome?: TaskOutcome): TaskRecord;
27
+ export declare function cancelTask(id: string): TaskRecord;
28
+ export declare function failTask(id: string, error: string): TaskRecord;
29
+ export declare function updateTaskRoom(id: string, roomId: string, roomIdentityCid: string): TaskRecord;
30
+ export declare function updateTaskMembers(id: string, members: TaskMemberRole[]): TaskRecord;
31
+ export declare function deleteTask(id: string): void;
@@ -0,0 +1,173 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { replaceFileAtomically } from '../atomic-file.js';
5
+ import { stateRoot } from '../paths.js';
6
+ import { TASK_TERMINAL_STATES, TASK_CANCELLABLE_STATES } from './types.js';
7
+ export const tasksDir = () => join(stateRoot(), 'tasks');
8
+ function taskPath(id) { return join(tasksDir(), `${id}.json`); }
9
+ function generateTaskId() {
10
+ const ts = Date.now().toString(36).padStart(9, '0');
11
+ const rand = randomUUID().replace(/-/g, '').slice(0, 8);
12
+ return `${ts}${rand}`;
13
+ }
14
+ export class TaskStateError extends Error {
15
+ }
16
+ function readTask(id) {
17
+ const p = taskPath(id);
18
+ if (!existsSync(p))
19
+ throw new TaskStateError(`task not found: ${id}`);
20
+ return JSON.parse(readFileSync(p, 'utf8'));
21
+ }
22
+ function writeTask(record) {
23
+ mkdirSync(tasksDir(), { recursive: true });
24
+ replaceFileAtomically(taskPath(record.task_id), JSON.stringify(record, null, 2) + '\n');
25
+ }
26
+ // ── Lifecycle transitions ───────────────────────────────────────────────
27
+ const VALID_TRANSITIONS = {
28
+ backlog: ['provisioning', 'cancelled'],
29
+ provisioning: ['active', 'failed', 'cancelled'],
30
+ active: ['review', 'failed', 'cancelled'],
31
+ review: ['done', 'failed', 'cancelled'],
32
+ done: [],
33
+ cancelled: [],
34
+ failed: [],
35
+ };
36
+ function assertTransition(from, to) {
37
+ if (!VALID_TRANSITIONS[from].includes(to))
38
+ throw new TaskStateError(`cannot transition from '${from}' to '${to}'`);
39
+ }
40
+ export function createTask(input) {
41
+ const key = input.idempotency_key ?? randomUUID();
42
+ const existing = findByIdempotencyKey(key);
43
+ if (existing)
44
+ return existing;
45
+ const state = input.start === false ? 'backlog' : 'provisioning';
46
+ const record = {
47
+ task_id: generateTaskId(),
48
+ title: input.title,
49
+ brief: input.brief,
50
+ brief_file: input.brief_file,
51
+ state,
52
+ template: input.template,
53
+ room_id: input.room_id,
54
+ member_roles: [],
55
+ origin: input.origin,
56
+ idempotency_key: key,
57
+ created_at: new Date().toISOString(),
58
+ started_at: state === 'provisioning' ? new Date().toISOString() : undefined,
59
+ };
60
+ writeTask(record);
61
+ return record;
62
+ }
63
+ export function getTask(id) { return readTask(id); }
64
+ export function listTasks(filter) {
65
+ const dir = tasksDir();
66
+ if (!existsSync(dir))
67
+ return [];
68
+ const states = filter?.state
69
+ ? (Array.isArray(filter.state) ? filter.state : [filter.state])
70
+ : undefined;
71
+ const tasks = [];
72
+ for (const f of readdirSync(dir).filter(f => f.endsWith('.json'))) {
73
+ try {
74
+ const t = JSON.parse(readFileSync(join(dir, f), 'utf8'));
75
+ if (!states || states.includes(t.state))
76
+ tasks.push(t);
77
+ }
78
+ catch { /* corrupt file, skip */ }
79
+ }
80
+ return tasks.sort((a, b) => a.created_at.localeCompare(b.created_at));
81
+ }
82
+ export function findByIdempotencyKey(key) {
83
+ return listTasks().find(t => t.idempotency_key === key);
84
+ }
85
+ export function startTask(id) {
86
+ const t = readTask(id);
87
+ assertTransition(t.state, 'provisioning');
88
+ t.state = 'provisioning';
89
+ t.started_at = new Date().toISOString();
90
+ writeTask(t);
91
+ return t;
92
+ }
93
+ export function activateTask(id) {
94
+ const t = readTask(id);
95
+ assertTransition(t.state, 'active');
96
+ t.state = 'active';
97
+ delete t.blocked;
98
+ writeTask(t);
99
+ return t;
100
+ }
101
+ export function blockTask(id, reason) {
102
+ const t = readTask(id);
103
+ if (TASK_TERMINAL_STATES.includes(t.state))
104
+ throw new TaskStateError(`cannot block a '${t.state}' task`);
105
+ t.blocked = { reason, at: new Date().toISOString() };
106
+ writeTask(t);
107
+ return t;
108
+ }
109
+ export function unblockTask(id) {
110
+ const t = readTask(id);
111
+ if (!t.blocked)
112
+ throw new TaskStateError('task is not blocked');
113
+ delete t.blocked;
114
+ writeTask(t);
115
+ return t;
116
+ }
117
+ export function reviewTask(id) {
118
+ const t = readTask(id);
119
+ assertTransition(t.state, 'review');
120
+ t.state = 'review';
121
+ delete t.blocked;
122
+ writeTask(t);
123
+ return t;
124
+ }
125
+ export function completeTask(id, outcome) {
126
+ const t = readTask(id);
127
+ assertTransition(t.state, 'done');
128
+ t.state = 'done';
129
+ t.ended_at = new Date().toISOString();
130
+ delete t.blocked;
131
+ if (outcome)
132
+ t.outcome = outcome;
133
+ writeTask(t);
134
+ return t;
135
+ }
136
+ export function cancelTask(id) {
137
+ const t = readTask(id);
138
+ if (!TASK_CANCELLABLE_STATES.includes(t.state))
139
+ throw new TaskStateError(`cannot cancel a '${t.state}' task`);
140
+ t.state = 'cancelled';
141
+ t.ended_at = new Date().toISOString();
142
+ delete t.blocked;
143
+ writeTask(t);
144
+ return t;
145
+ }
146
+ export function failTask(id, error) {
147
+ const t = readTask(id);
148
+ assertTransition(t.state, 'failed');
149
+ t.state = 'failed';
150
+ t.ended_at = new Date().toISOString();
151
+ t.outcome = { summary: error };
152
+ delete t.blocked;
153
+ writeTask(t);
154
+ return t;
155
+ }
156
+ export function updateTaskRoom(id, roomId, roomIdentityCid) {
157
+ const t = readTask(id);
158
+ t.room_id = roomId;
159
+ t.room_identity_cid = roomIdentityCid;
160
+ writeTask(t);
161
+ return t;
162
+ }
163
+ export function updateTaskMembers(id, members) {
164
+ const t = readTask(id);
165
+ t.member_roles = members;
166
+ writeTask(t);
167
+ return t;
168
+ }
169
+ export function deleteTask(id) {
170
+ const p = taskPath(id);
171
+ if (existsSync(p))
172
+ rmSync(p);
173
+ }
@@ -0,0 +1,6 @@
1
+ import type { TemplateDefinition, TemplateSnapshot } from './types.js';
2
+ export declare const BUILTIN_TEMPLATES: readonly TemplateDefinition[];
3
+ export declare function hashTemplate(t: TemplateDefinition): string;
4
+ export declare function snapshotTemplate(t: TemplateDefinition): TemplateSnapshot;
5
+ export declare function resolveTemplate(name: string, customTemplates: Record<string, TemplateDefinition>): TemplateDefinition | undefined;
6
+ export declare function listTemplates(customTemplates: Record<string, TemplateDefinition>): TemplateDefinition[];