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

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 (90) hide show
  1. package/README.md +86 -90
  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 +14 -6
  9. package/dist/config.js +58 -31
  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 +71 -94
  14. package/dist/doctor.d.ts +5 -1
  15. package/dist/doctor.js +143 -12
  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 +36 -30
  32. package/dist/owner-channel/channel.js +338 -291
  33. package/dist/owner-channel/commands.d.ts +9 -0
  34. package/dist/owner-channel/commands.js +393 -0
  35. package/dist/owner-channel/message-recovery.d.ts +25 -0
  36. package/dist/owner-channel/message-recovery.js +114 -0
  37. package/dist/owner-channel/notices.d.ts +0 -7
  38. package/dist/owner-channel/notices.js +0 -9
  39. package/dist/owner-channel/ours-client.d.ts +148 -0
  40. package/dist/owner-channel/ours-client.js +231 -0
  41. package/dist/rooms-tasks/cli.d.ts +4 -0
  42. package/dist/rooms-tasks/cli.js +1102 -0
  43. package/dist/rooms-tasks/close.d.ts +21 -0
  44. package/dist/rooms-tasks/close.js +159 -0
  45. package/dist/rooms-tasks/config.d.ts +15 -0
  46. package/dist/rooms-tasks/config.js +174 -0
  47. package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
  48. package/dist/rooms-tasks/cowork-adapter.js +272 -0
  49. package/dist/rooms-tasks/external-worker.d.ts +2 -0
  50. package/dist/rooms-tasks/external-worker.js +40 -0
  51. package/dist/rooms-tasks/index.d.ts +10 -0
  52. package/dist/rooms-tasks/index.js +10 -0
  53. package/dist/rooms-tasks/provision.d.ts +21 -0
  54. package/dist/rooms-tasks/provision.js +243 -0
  55. package/dist/rooms-tasks/room-state.d.ts +28 -0
  56. package/dist/rooms-tasks/room-state.js +221 -0
  57. package/dist/rooms-tasks/task-state.d.ts +43 -0
  58. package/dist/rooms-tasks/task-state.js +277 -0
  59. package/dist/rooms-tasks/templates.d.ts +6 -0
  60. package/dist/rooms-tasks/templates.js +94 -0
  61. package/dist/rooms-tasks/terminal.d.ts +23 -0
  62. package/dist/rooms-tasks/terminal.js +53 -0
  63. package/dist/rooms-tasks/types.d.ts +187 -0
  64. package/dist/rooms-tasks/types.js +20 -0
  65. package/dist/runner.d.ts +0 -48
  66. package/dist/runner.js +94 -236
  67. package/dist/session/acp.d.ts +0 -104
  68. package/dist/session/acp.js +10 -213
  69. package/dist/session/conversation-normalizer.d.ts +0 -6
  70. package/dist/session/conversation-normalizer.js +10 -153
  71. package/dist/session/conversation-types.d.ts +4 -23
  72. package/dist/session/types.d.ts +0 -35
  73. package/dist/spawn.js +26 -33
  74. package/dist/supervisor/systemd.js +29 -2
  75. package/dist/temp-lifecycle.d.ts +9 -2
  76. package/dist/temp-lifecycle.js +61 -0
  77. package/dist/watchdog/briefing.js +0 -7
  78. package/dist/watchdog/run.js +3 -3
  79. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  80. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  81. package/dist/web-app/index.html +1 -1
  82. package/dist/worklog.d.ts +1 -7
  83. package/dist/worklog.js +39 -191
  84. package/package.json +3 -1
  85. package/dist/model-env.d.ts +0 -71
  86. package/dist/model-env.js +0 -106
  87. package/dist/owner-channel/mcp.d.ts +0 -24
  88. package/dist/owner-channel/mcp.js +0 -145
  89. package/dist/session/activity.d.ts +0 -31
  90. package/dist/session/activity.js +0 -48
@@ -0,0 +1,221 @@
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
+ goal: input.goal,
27
+ room_identity_cid: input.room_identity_cid,
28
+ task_id: input.task_id,
29
+ template_snapshot: input.template_snapshot,
30
+ saga: { phase: 'persist_intent', step_index: 0 },
31
+ member_seats: [],
32
+ state: 'provisioning',
33
+ created_at: new Date().toISOString(),
34
+ };
35
+ writeRoom(record);
36
+ return record;
37
+ }
38
+ export function getRoomRecord(id) {
39
+ try {
40
+ return readRoom(id);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ export function listRoomRecords(filter) {
47
+ const dir = roomsDir();
48
+ if (!existsSync(dir))
49
+ return [];
50
+ const states = filter?.state
51
+ ? (Array.isArray(filter.state) ? filter.state : [filter.state])
52
+ : undefined;
53
+ const rooms = [];
54
+ for (const f of readdirSync(dir).filter(f => f.endsWith('.json'))) {
55
+ try {
56
+ const r = JSON.parse(readFileSync(join(dir, f), 'utf8'));
57
+ if (!states || states.includes(r.state))
58
+ rooms.push(r);
59
+ }
60
+ catch { /* skip corrupt */ }
61
+ }
62
+ return rooms.sort((a, b) => a.created_at.localeCompare(b.created_at));
63
+ }
64
+ export function advanceSaga(id, phase, stepIndex, detail) {
65
+ const r = readRoom(id);
66
+ r.saga = { phase, step_index: stepIndex ?? 0 };
67
+ if (detail)
68
+ r.provisioning_detail = detail;
69
+ else
70
+ delete r.provisioning_detail;
71
+ writeRoom(r);
72
+ return r;
73
+ }
74
+ export function setSagaError(id, error, hint, detail) {
75
+ const r = readRoom(id);
76
+ r.saga.error = error;
77
+ r.saga.recovery_hint = hint;
78
+ if (detail)
79
+ r.provisioning_detail = detail;
80
+ writeRoom(r);
81
+ return r;
82
+ }
83
+ export function setRoomIdentity(id, roomIdentityCid) {
84
+ const r = readRoom(id);
85
+ r.room_identity_cid = roomIdentityCid;
86
+ writeRoom(r);
87
+ return r;
88
+ }
89
+ export function setOwnerSeat(id, ownerSeatCid, inviteFingerprint) {
90
+ const r = readRoom(id);
91
+ r.owner_seat_cid = ownerSeatCid;
92
+ r.owner_invite_fingerprint = inviteFingerprint;
93
+ writeRoom(r);
94
+ return r;
95
+ }
96
+ export function updateMemberSeats(id, seats) {
97
+ const r = readRoom(id);
98
+ r.member_seats = seats;
99
+ writeRoom(r);
100
+ return r;
101
+ }
102
+ export function activateRoom(id) {
103
+ const r = readRoom(id);
104
+ r.state = 'active';
105
+ r.saga = { phase: 'completed', step_index: 0 };
106
+ delete r.provisioning_detail;
107
+ r.activated_at = new Date().toISOString();
108
+ writeRoom(r);
109
+ return r;
110
+ }
111
+ export function closeRoom(id) {
112
+ const r = readRoom(id);
113
+ if (r.state === 'closed')
114
+ return r;
115
+ r.state = 'closed';
116
+ r.closed_at = new Date().toISOString();
117
+ r.close = {
118
+ phase: 'completed',
119
+ accepted_at: r.close?.accepted_at ?? r.closed_at,
120
+ ...(r.close?.first_failure ? { first_failure: r.close.first_failure } : {}),
121
+ ...(r.close?.first_recovery_hint
122
+ ? { first_recovery_hint: r.close.first_recovery_hint }
123
+ : {}),
124
+ };
125
+ writeRoom(r);
126
+ return r;
127
+ }
128
+ export function beginRoomClose(id) {
129
+ const r = readRoom(id);
130
+ if (r.state === 'closed')
131
+ return r;
132
+ const acceptedAt = r.close?.accepted_at ?? new Date().toISOString();
133
+ r.state = 'closing';
134
+ r.close = {
135
+ phase: r.close?.phase ?? 'retire_members',
136
+ accepted_at: acceptedAt,
137
+ ...(r.close?.error ? { error: r.close.error } : {}),
138
+ ...(r.close?.error_at ? { error_at: r.close.error_at } : {}),
139
+ ...(r.close?.recovery_hint ? { recovery_hint: r.close.recovery_hint } : {}),
140
+ ...(r.close?.first_failure ? { first_failure: r.close.first_failure } : {}),
141
+ ...(r.close?.first_recovery_hint
142
+ ? { first_recovery_hint: r.close.first_recovery_hint }
143
+ : {}),
144
+ };
145
+ writeRoom(r);
146
+ return r;
147
+ }
148
+ const RETIREMENT_ORDER = [
149
+ 'stop_requested', 'liveness_absent', 'archive_secured', 'identity_absent',
150
+ ];
151
+ export function advanceMemberRetirement(id, roleName, phase, launchId, archivePath) {
152
+ const r = readRoom(id);
153
+ if (r.state !== 'closing')
154
+ throw new RoomStateError(`room ${id} is not closing`);
155
+ const seat = r.member_seats.find(candidate => candidate.role_name === roleName);
156
+ if (!seat)
157
+ throw new RoomStateError(`room ${id} has no recorded member ${roleName}`);
158
+ const previous = seat.retirement;
159
+ if (previous && previous.launch_id !== launchId) {
160
+ throw new RoomStateError(`room ${id} member ${roleName} launch changed from ${previous.launch_id} to ${launchId}`);
161
+ }
162
+ const nextIndex = RETIREMENT_ORDER.indexOf(phase);
163
+ const previousIndex = previous ? RETIREMENT_ORDER.indexOf(previous.phase) : -1;
164
+ if (nextIndex < previousIndex) {
165
+ throw new RoomStateError(`room ${id} member ${roleName} retirement cannot move backward to ${phase}`);
166
+ }
167
+ seat.retirement = {
168
+ phase,
169
+ launch_id: launchId,
170
+ updated_at: new Date().toISOString(),
171
+ ...(archivePath ?? previous?.archive_path
172
+ ? { archive_path: archivePath ?? previous?.archive_path }
173
+ : {}),
174
+ };
175
+ if (phase === 'identity_absent')
176
+ seat.seat_state = 'removed';
177
+ writeRoom(r);
178
+ return r;
179
+ }
180
+ export function advanceRoomClose(id, phase) {
181
+ const r = readRoom(id);
182
+ if (r.state === 'closed')
183
+ return r;
184
+ if (r.state !== 'closing' || !r.close)
185
+ throw new RoomStateError(`room ${id} is not closing`);
186
+ const order = ['retire_members', 'close_cowork', 'completed'];
187
+ if (order.indexOf(phase) < order.indexOf(r.close.phase)) {
188
+ throw new RoomStateError(`room ${id} close cannot move backward to ${phase}`);
189
+ }
190
+ const progressed = order.indexOf(phase) > order.indexOf(r.close.phase);
191
+ r.close = {
192
+ phase,
193
+ accepted_at: r.close.accepted_at,
194
+ ...(!progressed && r.close.error ? { error: r.close.error } : {}),
195
+ ...(!progressed && r.close.error_at ? { error_at: r.close.error_at } : {}),
196
+ ...(!progressed && r.close.recovery_hint
197
+ ? { recovery_hint: r.close.recovery_hint }
198
+ : {}),
199
+ ...(r.close.first_failure ? { first_failure: r.close.first_failure } : {}),
200
+ ...(r.close.first_recovery_hint
201
+ ? { first_recovery_hint: r.close.first_recovery_hint }
202
+ : {}),
203
+ };
204
+ writeRoom(r);
205
+ return r;
206
+ }
207
+ export function setRoomCloseError(id, error, recoveryHint) {
208
+ const r = readRoom(id);
209
+ if (r.state === 'closed')
210
+ return r;
211
+ if (r.state !== 'closing' || !r.close)
212
+ throw new RoomStateError(`room ${id} is not closing`);
213
+ r.close.first_failure ??= error;
214
+ r.close.first_recovery_hint ??= recoveryHint;
215
+ r.close.error = error;
216
+ const previousErrorAt = Date.parse(r.close.error_at ?? '');
217
+ r.close.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
218
+ r.close.recovery_hint = recoveryHint;
219
+ writeRoom(r);
220
+ return r;
221
+ }
@@ -0,0 +1,43 @@
1
+ import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent } 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
+ no_room?: boolean;
14
+ room_id?: string;
15
+ }
16
+ export declare function createTask(input: CreateTaskInput): TaskRecord;
17
+ export declare function getTask(id: string): TaskRecord;
18
+ export declare function listTasks(filter?: {
19
+ state?: TaskState | TaskState[];
20
+ }): TaskRecord[];
21
+ export declare function findByIdempotencyKey(key: string): TaskRecord | undefined;
22
+ export declare function startTask(id: string): TaskRecord;
23
+ export declare function activateTask(id: string): TaskRecord;
24
+ export declare function blockTask(id: string, reason: string): TaskRecord;
25
+ export declare function unblockTask(id: string): TaskRecord;
26
+ export declare function reviewTask(id: string): TaskRecord;
27
+ export declare function completeTask(id: string, outcome?: TaskOutcome): TaskRecord;
28
+ export declare function cancelTask(id: string): TaskRecord;
29
+ /** Persist the first terminal request. Callers serialize this with the task-operation lock. */
30
+ export declare function beginTaskTerminalIntent(id: string, input: {
31
+ kind: TaskTerminalIntent['kind'];
32
+ roomId?: string;
33
+ outcome?: TaskOutcome;
34
+ }): TaskRecord;
35
+ /** Record an actionable failure without claiming the requested terminal state. */
36
+ export declare function setTaskTerminalIntentError(id: string, error: string, recoveryHint: string): TaskRecord;
37
+ /** Atomically publish the task terminal state and settled audit cursor. */
38
+ export declare function finishTaskTerminalIntent(id: string): TaskRecord;
39
+ export declare function failTask(id: string, error: string): TaskRecord;
40
+ export declare function updateTaskRoom(id: string, roomId: string, roomIdentityCid: string): TaskRecord;
41
+ export declare function updateTaskTemplate(id: string, template: TaskTemplateRef): TaskRecord;
42
+ export declare function updateTaskMembers(id: string, members: TaskMemberRole[]): TaskRecord;
43
+ export declare function deleteTask(id: string): void;
@@ -0,0 +1,277 @@
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
+ function assertNoPendingTerminalIntent(task) {
41
+ if (task.terminal_intent?.status === 'pending') {
42
+ throw new TaskStateError(`task ${task.task_id} has a pending '${task.terminal_intent.kind}' terminal intent`);
43
+ }
44
+ }
45
+ export function createTask(input) {
46
+ const key = input.idempotency_key ?? randomUUID();
47
+ const existing = findByIdempotencyKey(key);
48
+ if (existing)
49
+ return existing;
50
+ const state = input.start === false ? 'backlog' : 'provisioning';
51
+ const record = {
52
+ task_id: generateTaskId(),
53
+ title: input.title,
54
+ brief: input.brief,
55
+ brief_file: input.brief_file,
56
+ state,
57
+ template: input.template,
58
+ no_room: input.no_room || undefined,
59
+ room_id: input.room_id,
60
+ member_roles: [],
61
+ origin: input.origin,
62
+ idempotency_key: key,
63
+ created_at: new Date().toISOString(),
64
+ started_at: state === 'provisioning' ? new Date().toISOString() : undefined,
65
+ };
66
+ writeTask(record);
67
+ return record;
68
+ }
69
+ export function getTask(id) { return readTask(id); }
70
+ export function listTasks(filter) {
71
+ const dir = tasksDir();
72
+ if (!existsSync(dir))
73
+ return [];
74
+ const states = filter?.state
75
+ ? (Array.isArray(filter.state) ? filter.state : [filter.state])
76
+ : undefined;
77
+ const tasks = [];
78
+ for (const f of readdirSync(dir).filter(f => f.endsWith('.json'))) {
79
+ try {
80
+ const t = JSON.parse(readFileSync(join(dir, f), 'utf8'));
81
+ if (!states || states.includes(t.state))
82
+ tasks.push(t);
83
+ }
84
+ catch { /* corrupt file, skip */ }
85
+ }
86
+ return tasks.sort((a, b) => a.created_at.localeCompare(b.created_at));
87
+ }
88
+ export function findByIdempotencyKey(key) {
89
+ return listTasks().find(t => t.idempotency_key === key);
90
+ }
91
+ export function startTask(id) {
92
+ const t = readTask(id);
93
+ assertNoPendingTerminalIntent(t);
94
+ assertTransition(t.state, 'provisioning');
95
+ t.state = 'provisioning';
96
+ t.started_at = new Date().toISOString();
97
+ writeTask(t);
98
+ return t;
99
+ }
100
+ export function activateTask(id) {
101
+ const t = readTask(id);
102
+ assertNoPendingTerminalIntent(t);
103
+ assertTransition(t.state, 'active');
104
+ t.state = 'active';
105
+ delete t.blocked;
106
+ writeTask(t);
107
+ return t;
108
+ }
109
+ export function blockTask(id, reason) {
110
+ const t = readTask(id);
111
+ assertNoPendingTerminalIntent(t);
112
+ if (TASK_TERMINAL_STATES.includes(t.state))
113
+ throw new TaskStateError(`cannot block a '${t.state}' task`);
114
+ t.blocked = { reason, at: new Date().toISOString() };
115
+ writeTask(t);
116
+ return t;
117
+ }
118
+ export function unblockTask(id) {
119
+ const t = readTask(id);
120
+ assertNoPendingTerminalIntent(t);
121
+ if (!t.blocked)
122
+ throw new TaskStateError('task is not blocked');
123
+ delete t.blocked;
124
+ writeTask(t);
125
+ return t;
126
+ }
127
+ export function reviewTask(id) {
128
+ const t = readTask(id);
129
+ assertNoPendingTerminalIntent(t);
130
+ assertTransition(t.state, 'review');
131
+ t.state = 'review';
132
+ delete t.blocked;
133
+ writeTask(t);
134
+ return t;
135
+ }
136
+ export function completeTask(id, outcome) {
137
+ const t = readTask(id);
138
+ assertNoPendingTerminalIntent(t);
139
+ assertTransition(t.state, 'done');
140
+ t.state = 'done';
141
+ t.ended_at = new Date().toISOString();
142
+ delete t.blocked;
143
+ if (outcome)
144
+ t.outcome = outcome;
145
+ writeTask(t);
146
+ return t;
147
+ }
148
+ export function cancelTask(id) {
149
+ const t = readTask(id);
150
+ assertNoPendingTerminalIntent(t);
151
+ if (!TASK_CANCELLABLE_STATES.includes(t.state))
152
+ throw new TaskStateError(`cannot cancel a '${t.state}' task`);
153
+ t.state = 'cancelled';
154
+ t.ended_at = new Date().toISOString();
155
+ delete t.blocked;
156
+ writeTask(t);
157
+ return t;
158
+ }
159
+ function sameOutcome(left, right) {
160
+ return JSON.stringify(left) === JSON.stringify(right);
161
+ }
162
+ /** Persist the first terminal request. Callers serialize this with the task-operation lock. */
163
+ export function beginTaskTerminalIntent(id, input) {
164
+ const t = readTask(id);
165
+ const existing = t.terminal_intent;
166
+ if (existing) {
167
+ if (existing.kind !== input.kind || existing.room_id !== input.roomId
168
+ || !sameOutcome(existing.outcome, input.outcome)) {
169
+ throw new TaskStateError(`task ${id} already has a conflicting '${existing.kind}' terminal intent`);
170
+ }
171
+ return t;
172
+ }
173
+ if (TASK_TERMINAL_STATES.includes(t.state)) {
174
+ throw new TaskStateError(`task ${id} is already in terminal state '${t.state}'`);
175
+ }
176
+ if (input.kind === 'done' && t.state !== 'review') {
177
+ throw new TaskStateError(`cannot transition from '${t.state}' to 'done'`);
178
+ }
179
+ if (input.kind === 'cancelled' && !TASK_CANCELLABLE_STATES.includes(t.state)) {
180
+ throw new TaskStateError(`cannot cancel a '${t.state}' task`);
181
+ }
182
+ if (input.roomId !== undefined && t.room_id !== input.roomId) {
183
+ throw new TaskStateError(`task ${id} is not tied to room ${input.roomId}`);
184
+ }
185
+ t.terminal_intent = {
186
+ kind: input.kind,
187
+ status: 'pending',
188
+ room_id: input.roomId,
189
+ outcome: input.outcome,
190
+ accepted_at: new Date().toISOString(),
191
+ };
192
+ writeTask(t);
193
+ return t;
194
+ }
195
+ /** Record an actionable failure without claiming the requested terminal state. */
196
+ export function setTaskTerminalIntentError(id, error, recoveryHint) {
197
+ const t = readTask(id);
198
+ if (!t.terminal_intent)
199
+ throw new TaskStateError(`task ${id} has no terminal intent`);
200
+ if (t.terminal_intent.status === 'settled')
201
+ return t;
202
+ t.terminal_intent.first_failure ??= error;
203
+ t.terminal_intent.first_recovery_hint ??= recoveryHint;
204
+ t.terminal_intent.error = error;
205
+ const previousErrorAt = Date.parse(t.terminal_intent.error_at ?? '');
206
+ t.terminal_intent.error_at = new Date(Math.max(Date.now(), Number.isFinite(previousErrorAt) ? previousErrorAt + 1 : 0)).toISOString();
207
+ t.terminal_intent.recovery_hint = recoveryHint;
208
+ writeTask(t);
209
+ return t;
210
+ }
211
+ /** Atomically publish the task terminal state and settled audit cursor. */
212
+ export function finishTaskTerminalIntent(id) {
213
+ const t = readTask(id);
214
+ const intent = t.terminal_intent;
215
+ if (!intent)
216
+ throw new TaskStateError(`task ${id} has no terminal intent`);
217
+ if (intent.status === 'settled')
218
+ return t;
219
+ if (TASK_TERMINAL_STATES.includes(t.state)) {
220
+ throw new TaskStateError(`task ${id} reached terminal state '${t.state}' outside its intent`);
221
+ }
222
+ if (intent.kind === 'done')
223
+ assertTransition(t.state, 'done');
224
+ else if (!TASK_CANCELLABLE_STATES.includes(t.state)) {
225
+ throw new TaskStateError(`cannot cancel a '${t.state}' task`);
226
+ }
227
+ t.state = intent.kind;
228
+ t.ended_at = new Date().toISOString();
229
+ delete t.blocked;
230
+ if (intent.outcome)
231
+ t.outcome = intent.outcome;
232
+ intent.status = 'settled';
233
+ intent.settled_at = t.ended_at;
234
+ delete intent.error;
235
+ delete intent.error_at;
236
+ delete intent.recovery_hint;
237
+ writeTask(t);
238
+ return t;
239
+ }
240
+ export function failTask(id, error) {
241
+ const t = readTask(id);
242
+ assertNoPendingTerminalIntent(t);
243
+ assertTransition(t.state, 'failed');
244
+ t.state = 'failed';
245
+ t.ended_at = new Date().toISOString();
246
+ t.outcome = { summary: error };
247
+ delete t.blocked;
248
+ writeTask(t);
249
+ return t;
250
+ }
251
+ export function updateTaskRoom(id, roomId, roomIdentityCid) {
252
+ const t = readTask(id);
253
+ assertNoPendingTerminalIntent(t);
254
+ t.room_id = roomId;
255
+ t.room_identity_cid = roomIdentityCid;
256
+ writeTask(t);
257
+ return t;
258
+ }
259
+ export function updateTaskTemplate(id, template) {
260
+ const t = readTask(id);
261
+ assertNoPendingTerminalIntent(t);
262
+ t.template = template;
263
+ writeTask(t);
264
+ return t;
265
+ }
266
+ export function updateTaskMembers(id, members) {
267
+ const t = readTask(id);
268
+ assertNoPendingTerminalIntent(t);
269
+ t.member_roles = members;
270
+ writeTask(t);
271
+ return t;
272
+ }
273
+ export function deleteTask(id) {
274
+ const p = taskPath(id);
275
+ if (existsSync(p))
276
+ rmSync(p);
277
+ }
@@ -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[];
@@ -0,0 +1,94 @@
1
+ import { createHash } from 'node:crypto';
2
+ const TEAM = {
3
+ name: 'team',
4
+ version: 1,
5
+ builtin: true,
6
+ description: 'Phased task pipeline: Architect specifies, Developer implements, Tester verifies',
7
+ room: { quiet_membership: false, anonymous: false },
8
+ contract: [
9
+ 'Architect produces a spec and posts it to the room. Work pauses for owner approval.',
10
+ 'Developer implements per approved spec.',
11
+ 'Tester verifies spec conformance and test coverage.',
12
+ 'Completion requires tester sign-off.',
13
+ ].join('\n'),
14
+ members: [
15
+ { slot: 'architect', role: 'Architect', count: 1, role_ref: 'Architect' },
16
+ { slot: 'developer', role: 'Developer', count: 1, role_ref: 'Developer' },
17
+ { slot: 'tester', role: 'Tester', count: 1, role_ref: 'Tester' },
18
+ ],
19
+ };
20
+ const PAIR = {
21
+ name: 'pair',
22
+ version: 1,
23
+ builtin: true,
24
+ description: 'Deliberation pair: Secretary writes code, Critic reviews — every decision deliberated together',
25
+ room: { quiet_membership: false, anonymous: false },
26
+ contract: [
27
+ 'Secretary and Critic deliberate every decision together before acting.',
28
+ 'Secretary writes code; Critic reviews and challenges.',
29
+ 'No material change lands without both agreeing.',
30
+ 'Completion requires joint sign-off.',
31
+ ].join('\n'),
32
+ members: [
33
+ { slot: 'secretary', role: 'Secretary', count: 1, role_ref: 'Secretary' },
34
+ { slot: 'critic', role: 'Critic', count: 1, role_ref: 'Critic' },
35
+ ],
36
+ };
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];
52
+ export function hashTemplate(t) {
53
+ const canonical = JSON.stringify({
54
+ name: t.name, version: t.version, description: t.description,
55
+ room: t.room, contract: t.contract, members: t.members,
56
+ });
57
+ return createHash('sha256').update(canonical).digest('hex');
58
+ }
59
+ export function snapshotTemplate(t) {
60
+ return { ...t, content_hash: hashTemplate(t) };
61
+ }
62
+ export function resolveTemplate(name, customTemplates) {
63
+ const atIdx = name.lastIndexOf('@');
64
+ const baseName = atIdx > 0 ? name.slice(0, atIdx) : name;
65
+ const versionStr = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
66
+ const version = versionStr ? parseInt(versionStr, 10) : undefined;
67
+ const custom = customTemplates[baseName];
68
+ if (custom) {
69
+ if (version !== undefined && custom.version !== version)
70
+ return undefined;
71
+ return custom;
72
+ }
73
+ const builtin = BUILTIN_TEMPLATES.find(b => b.name === baseName);
74
+ if (builtin) {
75
+ if (version !== undefined && builtin.version !== version)
76
+ return undefined;
77
+ return builtin;
78
+ }
79
+ return undefined;
80
+ }
81
+ export function listTemplates(customTemplates) {
82
+ const result = [];
83
+ const overridden = new Set();
84
+ for (const [name, t] of Object.entries(customTemplates)) {
85
+ result.push({ ...t, name });
86
+ if (BUILTIN_TEMPLATES.some(b => b.name === name))
87
+ overridden.add(name);
88
+ }
89
+ for (const b of BUILTIN_TEMPLATES) {
90
+ if (!overridden.has(b.name))
91
+ result.push(b);
92
+ }
93
+ return result.sort((a, b) => a.name.localeCompare(b.name));
94
+ }
@@ -0,0 +1,23 @@
1
+ import { type RoomCloseDeps } from './close.js';
2
+ import type { CoworkAdapter } from './cowork-adapter.js';
3
+ import type { TaskOutcome, TaskRecord, TaskTerminalIntent } from './types.js';
4
+ export interface TaskTerminalInput {
5
+ taskId: string;
6
+ kind: TaskTerminalIntent['kind'];
7
+ roomId?: string;
8
+ outcome?: TaskOutcome;
9
+ }
10
+ /** Persist first-wins terminal intent before any acknowledgement or close effect. */
11
+ export declare function acceptTaskTerminalIntent(input: TaskTerminalInput): Promise<TaskRecord>;
12
+ export interface SettleTaskTerminalDeps {
13
+ roomClose?: RoomCloseDeps;
14
+ afterRoomClosed?(): void | Promise<void>;
15
+ }
16
+ /** Close the recorded room, then atomically settle the persisted task intent. */
17
+ export declare function settleTaskTerminalIntent(input: {
18
+ taskId: string;
19
+ cowork: Pick<CoworkAdapter, 'closeRoom'>;
20
+ deps?: SettleTaskTerminalDeps;
21
+ }): Promise<TaskRecord>;
22
+ /** Persist failure to launch an external settle worker under the same task lock. */
23
+ export declare function recordTaskTerminalIntentError(taskId: string, error: string, recoveryHint: string): Promise<TaskRecord>;