@ours.network/fleet 1.2.0-nightly.19 → 1.2.0-nightly.20
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 +10 -1
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/runtime.d.ts +6 -0
- package/dist/agent-ours/runtime.js +36 -0
- package/dist/agent-ours/service.js +2 -1
- package/dist/application/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +82 -0
- package/dist/application/task-room-service.js +197 -9
- package/dist/build-info.json +4 -4
- package/dist/cli.js +2 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/init-wizard.js +4 -2
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/cli.js +20 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -3
- package/dist/rooms-tasks/layout-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.js +2 -1
- package/dist/rooms-tasks/provision.d.ts +3 -0
- package/dist/rooms-tasks/provision.js +44 -22
- package/dist/rooms-tasks/room-state.d.ts +1 -1
- package/dist/rooms-tasks/room-state.js +3 -1
- package/dist/rooms-tasks/task-state.d.ts +19 -0
- package/dist/rooms-tasks/task-state.js +58 -6
- package/dist/rooms-tasks/types.d.ts +14 -0
- package/dist/runner.d.ts +7 -2
- package/dist/runner.js +52 -4
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/temp-lifecycle.js +1 -1
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.js +275 -24
- package/docs/room-layouts.md +92 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +14 -13
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { TaskLayouts, assertLayoutOpenable, writeTaskBrief } from './task-layouts.js';
|
|
2
|
+
import { FleetError } from './errors.js';
|
|
1
3
|
import { readFileSync } from 'node:fs';
|
|
2
4
|
import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
|
|
3
5
|
import { ConfigError, loadConfig } from '../config.js';
|
|
4
6
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
5
7
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
6
8
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
7
|
-
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
|
|
9
|
+
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, updateTaskBrief, updateTaskLayout, } from '../rooms-tasks/task-state.js';
|
|
8
10
|
import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
|
|
9
11
|
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
10
12
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
@@ -72,7 +74,127 @@ export class TaskRoomApplicationService {
|
|
|
72
74
|
this.configurationPath = configurationPath;
|
|
73
75
|
this.deps = deps;
|
|
74
76
|
}
|
|
77
|
+
async addMember(taskId, request) {
|
|
78
|
+
const { requestMemberAddition } = await import('../rooms-tasks/add-member.js');
|
|
79
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
80
|
+
return requestMemberAddition({ taskId, request, cfg, configPath: this.configurationPath,
|
|
81
|
+
cowork: (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers });
|
|
82
|
+
}
|
|
83
|
+
async memberAddition(taskId, requestId) { const { memberAddition } = await import('../rooms-tasks/add-member.js'); return memberAddition(taskId, requestId); }
|
|
84
|
+
layouts() {
|
|
85
|
+
return this.deps.taskLayouts ?? new TaskLayouts(this.configurationPath, { binPath: this.deps.binPath });
|
|
86
|
+
}
|
|
87
|
+
async createLayoutTask(request) {
|
|
88
|
+
if (request.template || request.noRoom || request.members || request.anonymous !== undefined)
|
|
89
|
+
throw new FleetError('invalid_request', 'a layout task cannot be combined with a room template, no-room mode, member overrides or anonymity');
|
|
90
|
+
let layout;
|
|
91
|
+
try {
|
|
92
|
+
layout = this.layouts().resolve(request.layout);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new FleetError('invalid_request', error.message, { details: { layout: request.layout } });
|
|
96
|
+
}
|
|
97
|
+
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
98
|
+
const task = await withTaskListsLock(() => persistTask({
|
|
99
|
+
title: request.title, brief, brief_file: request.briefFile, layout,
|
|
100
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
101
|
+
start: !request.backlog, listId: resolveTaskList(request.list ?? 'default').list_id,
|
|
102
|
+
}));
|
|
103
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
104
|
+
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
105
|
+
list: task.list_name ?? 'default', roomId: undefined, template: `layout:${layout.name}`, agents: [] });
|
|
106
|
+
return task.state === 'provisioning' ? (await this.ensureLayoutWork(task)).task : task;
|
|
107
|
+
}
|
|
108
|
+
/** Layout tasks never provision the legacy single room; their run is a local snapshot. */
|
|
109
|
+
async ensureLayoutWork(task) {
|
|
110
|
+
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
111
|
+
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: task.task_id, state: task.state });
|
|
112
|
+
if (task.state === 'active' || task.state === 'review')
|
|
113
|
+
return { task, status: 'already_active' };
|
|
114
|
+
if (task.state === 'backlog') {
|
|
115
|
+
// Verify the pinned source before leaving Backlog, so a stale choice stays editable on the task page.
|
|
116
|
+
if (!this.layouts().runExists(task.layout) && this.layouts().resolve(task.layout.name).definition_hash !== task.layout.definition_hash)
|
|
117
|
+
throw new FleetError('conflict', `room layout ${task.layout.name} changed after it was chosen for this task; choose it again on the task page to use the edited version`);
|
|
118
|
+
task = transitionTask(task.task_id);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await this.layouts().ensureRun(task);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const reason = `layout run could not be created: ${error.message}`;
|
|
125
|
+
persistBlockTask(task.task_id, reason);
|
|
126
|
+
throw new FleetError('conflict', reason, { details: { task: task.task_id } });
|
|
127
|
+
}
|
|
128
|
+
return { task: activateTask(task.task_id), status: 'ready' };
|
|
129
|
+
}
|
|
130
|
+
/** Compact room list for task listings; never fails the listing on an unreadable run. */
|
|
131
|
+
withLayoutRooms(task) {
|
|
132
|
+
if (!task.layout)
|
|
133
|
+
return task;
|
|
134
|
+
try {
|
|
135
|
+
const view = this.layouts().view(task);
|
|
136
|
+
return { ...task, layout_rooms: (view?.rooms ?? []).map(({ key, state, room_id, identity_cid }) => ({ key, state, ...(room_id ? { room_id } : {}), ...(identity_cid ? { identity_cid } : {}) })) };
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { ...task, layout_rooms: [] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Backlog only: choose, change or clear the task's Room Layout before it starts. */
|
|
143
|
+
async setTaskLayout(input) {
|
|
144
|
+
let next = null;
|
|
145
|
+
if (input.layout !== null) {
|
|
146
|
+
try {
|
|
147
|
+
next = this.layouts().resolve(input.layout);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new FleetError('invalid_request', error.message, { details: { layout: input.layout } });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const current = readTask(input.taskId);
|
|
154
|
+
if (current.layout && this.layouts().runExists(current.layout))
|
|
155
|
+
throw new FleetError('conflict', 'this task already has a layout run; its room layout is fixed');
|
|
156
|
+
let result;
|
|
157
|
+
try {
|
|
158
|
+
result = updateTaskLayout(input.taskId, input.expectedLayout, next);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error instanceof TaskStateError && !/task not found/.test(error.message))
|
|
162
|
+
throw new FleetError('conflict', error.message);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
// Only after the task stopped referencing it; release re-checks every retained reference under its lock.
|
|
166
|
+
if (result.releasedSnapshot)
|
|
167
|
+
releaseLaunchSnapshot(result.releasedSnapshot);
|
|
168
|
+
return result.task;
|
|
169
|
+
}
|
|
170
|
+
taskLayout(taskId) {
|
|
171
|
+
const task = readTask(taskId);
|
|
172
|
+
if (!task.layout)
|
|
173
|
+
throw new FleetError('resource_not_found', 'task has no room layout');
|
|
174
|
+
return this.layouts().view(task);
|
|
175
|
+
}
|
|
176
|
+
async launchTaskLayoutOperation(input) {
|
|
177
|
+
const task = readTask(input.taskId);
|
|
178
|
+
if (!task.layout)
|
|
179
|
+
throw new FleetError('resource_not_found', 'task has no room layout');
|
|
180
|
+
if (input.operation === 'open')
|
|
181
|
+
assertLayoutOpenable(task);
|
|
182
|
+
else if (task.deletion?.status === 'pending' || task.terminal_intent)
|
|
183
|
+
throw new FleetError('conflict', 'task is closing; its layout rooms are cleaned up by the terminal operation');
|
|
184
|
+
return this.layouts().launch(task, input.operation, input.room);
|
|
185
|
+
}
|
|
186
|
+
/** Worker entry for detached layout operations. */
|
|
187
|
+
async runTaskLayoutOperation(input) {
|
|
188
|
+
const task = readTask(input.taskId);
|
|
189
|
+
// Preconditions are enforced inside the worker before side effects; the worker is directly callable.
|
|
190
|
+
if (input.operation === 'open')
|
|
191
|
+
assertLayoutOpenable(task);
|
|
192
|
+
await this.layouts().run(task, input.operation, input.room, input.operationId);
|
|
193
|
+
return this.layouts().view(task);
|
|
194
|
+
}
|
|
75
195
|
async createTask(request) {
|
|
196
|
+
if (request.layout !== undefined)
|
|
197
|
+
return this.createLayoutTask(request);
|
|
76
198
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
77
199
|
let template = this.createTemplate(cfg, request.template, request.noRoom);
|
|
78
200
|
if (request.noRoom && request.anonymous !== undefined)
|
|
@@ -220,16 +342,36 @@ export class TaskRoomApplicationService {
|
|
|
220
342
|
return { deleted: source, moved: assigned.length, destination };
|
|
221
343
|
});
|
|
222
344
|
}
|
|
345
|
+
editTaskDescription(input) {
|
|
346
|
+
const task = updateTaskBrief(input.taskId, input.brief, input.expectedBrief);
|
|
347
|
+
// Layout rooms reference the brief file, so rooms opened later read the edited brief.
|
|
348
|
+
if (task.layout)
|
|
349
|
+
writeTaskBrief(task);
|
|
350
|
+
return task;
|
|
351
|
+
}
|
|
223
352
|
getTask(taskId) {
|
|
224
|
-
const task = readTask(taskId);
|
|
353
|
+
const task = this.withLayoutRooms(readTask(taskId));
|
|
225
354
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
226
355
|
}
|
|
227
356
|
/** Durable provisioning progress only; use active task start/work for a live readiness check. */
|
|
228
357
|
taskProvisioningOutcome(taskId) {
|
|
229
358
|
const task = readTask(taskId);
|
|
359
|
+
if (task.layout) {
|
|
360
|
+
const failed = task.state === 'failed';
|
|
361
|
+
const ready = ['active', 'review', 'done'].includes(task.state);
|
|
362
|
+
const blocker = task.outcome?.summary ?? task.blocked?.reason;
|
|
363
|
+
return {
|
|
364
|
+
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room: undefined,
|
|
365
|
+
launch: { template: `layout:${task.layout.name}`, anonymous: false, owner_attached: false },
|
|
366
|
+
members: { expected: 0, active: 0, launched: 0 },
|
|
367
|
+
...(blocker ? { blocker } : {}),
|
|
368
|
+
...(!ready && !failed ? { next_action: `Correct the blocker, then run ours-fleet task start ${task.task_id}.` } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
230
371
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
231
|
-
const expected = room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
|
|
232
|
-
?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0
|
|
372
|
+
const expected = (room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
|
|
373
|
+
?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0)
|
|
374
|
+
+ (room?.additional_member_templates ?? []).reduce((n, t) => n + t.members.reduce((m, s) => m + s.count, 0), 0);
|
|
233
375
|
const active = room?.member_seats.filter(seat => seat.seat_state === 'active').length ?? 0;
|
|
234
376
|
const launched = room?.member_seats.filter(seat => seat.launch?.state === 'launched').length ?? 0;
|
|
235
377
|
const failed = task.state === 'failed' || room?.saga.phase === 'failed';
|
|
@@ -316,6 +458,15 @@ export class TaskRoomApplicationService {
|
|
|
316
458
|
}
|
|
317
459
|
/** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
|
|
318
460
|
async settleTaskDeletion(input) {
|
|
461
|
+
// Layout resources close first; a failure stays a retryable pending deletion.
|
|
462
|
+
const deleting = (() => { try {
|
|
463
|
+
return getDeletingTask(input.taskId);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return undefined;
|
|
467
|
+
} })();
|
|
468
|
+
if (deleting?.layout)
|
|
469
|
+
await this.layouts().closeAndForget(deleting);
|
|
319
470
|
const result = await settleTaskDeletion({
|
|
320
471
|
taskId: input.taskId,
|
|
321
472
|
cowork: () => {
|
|
@@ -344,7 +495,7 @@ export class TaskRoomApplicationService {
|
|
|
344
495
|
*/
|
|
345
496
|
async launchTaskDeletionWorker(input) {
|
|
346
497
|
const launch = this.deps.launchDeletionWorker
|
|
347
|
-
?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
|
|
498
|
+
?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath, this.deps.binPath?.()));
|
|
348
499
|
const errorAtBefore = (() => {
|
|
349
500
|
try {
|
|
350
501
|
return getDeletingTask(input.taskId).deletion?.error_at;
|
|
@@ -397,6 +548,20 @@ export class TaskRoomApplicationService {
|
|
|
397
548
|
}
|
|
398
549
|
return this.acceptTerminal(input.taskId, 'done', roomId, input.outcome);
|
|
399
550
|
}
|
|
551
|
+
/** Terminal layout tasks archive their rooms and retire only agents their run created. */
|
|
552
|
+
async withLayoutCleanup(plan) {
|
|
553
|
+
// The task is already terminal; a worker launch failure is recorded on the run for retry.
|
|
554
|
+
if (plan.task.layout && this.layouts().runExists(plan.task.layout))
|
|
555
|
+
await this.layouts().launch(plan.task, 'close').catch(() => { });
|
|
556
|
+
return plan;
|
|
557
|
+
}
|
|
558
|
+
/** Retry terminal layout cleanup for known resources (e.g. after a failed close). */
|
|
559
|
+
async retryTaskLayoutCleanup(input) {
|
|
560
|
+
const task = readTask(input.taskId);
|
|
561
|
+
if (!task.layout || !TASK_TERMINAL_STATES.includes(task.state))
|
|
562
|
+
throw new TaskStateError('layout cleanup applies to finished or cancelled layout tasks');
|
|
563
|
+
return this.layouts().launch(task, 'close');
|
|
564
|
+
}
|
|
400
565
|
async cancelTask(input) {
|
|
401
566
|
const task = readTask(input.taskId);
|
|
402
567
|
if (!TASK_CANCELLABLE_STATES.includes(task.state))
|
|
@@ -405,6 +570,23 @@ export class TaskRoomApplicationService {
|
|
|
405
570
|
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
406
571
|
return this.acceptTerminal(input.taskId, 'cancelled', task.room_id);
|
|
407
572
|
}
|
|
573
|
+
/** Continue accepted operations outside the HTTP connection's lifetime. */
|
|
574
|
+
async launchTaskSettlement(taskId) {
|
|
575
|
+
try {
|
|
576
|
+
await launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, this.configurationPath, this.deps.binPath?.());
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
await recordTaskTerminalIntentError(taskId, error instanceof Error ? error.message : String(error), 'The settlement worker could not start. Inspect the task before retrying.');
|
|
580
|
+
}
|
|
581
|
+
return readTask(taskId);
|
|
582
|
+
}
|
|
583
|
+
async launchTaskProvisioning(taskId) {
|
|
584
|
+
const outcome = this.taskProvisioningOutcome(taskId);
|
|
585
|
+
if (outcome.kind === 'in_progress' && !outcome.next_action) {
|
|
586
|
+
await launchFleetWorker(['task', '_provision', taskId], `task-provision-${taskId}`, this.configurationPath, this.deps.binPath?.());
|
|
587
|
+
}
|
|
588
|
+
return outcome;
|
|
589
|
+
}
|
|
408
590
|
async settleTask(input) {
|
|
409
591
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
410
592
|
if (!cfg.rooms)
|
|
@@ -500,7 +682,7 @@ export class TaskRoomApplicationService {
|
|
|
500
682
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
501
683
|
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
502
684
|
await (this.deps.provisionMembers ?? provisionMembers)({
|
|
503
|
-
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
685
|
+
configPath: this.configurationPath, cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
504
686
|
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief, goal: task.title,
|
|
505
687
|
});
|
|
506
688
|
task = readTask(task.task_id);
|
|
@@ -563,7 +745,8 @@ export class TaskRoomApplicationService {
|
|
|
563
745
|
}
|
|
564
746
|
async acceptTerminal(taskId, kind, roomId, outcome) {
|
|
565
747
|
const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
|
|
566
|
-
|
|
748
|
+
// Every terminal path (complete, finish, cancel) closes a layout task's rooms.
|
|
749
|
+
return this.withLayoutCleanup({ task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId });
|
|
567
750
|
}
|
|
568
751
|
listTemplates() {
|
|
569
752
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
@@ -694,6 +877,11 @@ export class TaskRoomApplicationService {
|
|
|
694
877
|
async ensureTaskWork(input) {
|
|
695
878
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
696
879
|
let task = readTask(input.taskId);
|
|
880
|
+
if (task.layout) {
|
|
881
|
+
if (input.template || input.members || input.anonymous !== undefined)
|
|
882
|
+
throw new FleetError('invalid_request', 'a layout task cannot take a room template or member overrides');
|
|
883
|
+
return this.ensureLayoutWork(task);
|
|
884
|
+
}
|
|
697
885
|
const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
698
886
|
const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
|
|
699
887
|
if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
|
|
@@ -858,7 +1046,7 @@ export class TaskRoomApplicationService {
|
|
|
858
1046
|
if (!cfg.rooms)
|
|
859
1047
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
860
1048
|
const cowork = this.deps.cowork ? this.deps.cowork(cfg) : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
861
|
-
await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork, roomId: room.room_id,
|
|
1049
|
+
await (this.deps.provisionMembers ?? provisionMembers)({ configPath: this.configurationPath, cfg, cowork, roomId: room.room_id,
|
|
862
1050
|
taskId: task.task_id, template: snapshot, binPath: (this.deps.binPath ?? getBinPath)(),
|
|
863
1051
|
brief: task.brief, goal: task.title });
|
|
864
1052
|
task = readTask(task.task_id);
|
|
@@ -1004,7 +1192,7 @@ export class TaskRoomApplicationService {
|
|
|
1004
1192
|
if (launchTemplate && (launchTemplate.members.length > 0 || attachOwner))
|
|
1005
1193
|
try {
|
|
1006
1194
|
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
1007
|
-
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
1195
|
+
configPath: this.configurationPath, cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
1008
1196
|
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
1009
1197
|
goal: task.task_id ? task.title : task.goal,
|
|
1010
1198
|
});
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.20",
|
|
3
|
+
"buildId": "bd038efa768e",
|
|
4
|
+
"commit": "5f7b870fe2d026aeadb508a13c6dbdc38f573d2c",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-29T18:01:53.344Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"cowork.http-management-v1",
|
|
9
9
|
"monitor.interrupt.after_tool"
|
package/dist/cli.js
CHANGED
|
@@ -47,6 +47,7 @@ import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCliExit, s
|
|
|
47
47
|
import './harness/claude-code.js'; // registers the claude-code adapter
|
|
48
48
|
import './harness/codex.js'; // registers the codex adapter
|
|
49
49
|
import './harness/hermes.js';
|
|
50
|
+
import { registerLayoutCommands } from './rooms-tasks/layout-cli.js';
|
|
50
51
|
import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
|
|
51
52
|
import { RoleCreationService } from './application/role-creation-service.js';
|
|
52
53
|
import { RoleRemovalService } from './application/role-removal-service.js';
|
|
@@ -1577,6 +1578,7 @@ function configureWebAccess(opts) {
|
|
|
1577
1578
|
return undefined;
|
|
1578
1579
|
}
|
|
1579
1580
|
// ── rooms & tasks ──────────────────────────────────────────────────────────
|
|
1581
|
+
registerLayoutCommands(program, cOpt);
|
|
1580
1582
|
registerTemplateCommands(program, cOpt);
|
|
1581
1583
|
registerTaskCommands(program, cOpt);
|
|
1582
1584
|
registerRoomCommands(program, cOpt);
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ConfigDiagnostic, type YamlMode } from './config-yaml.js';
|
|
|
2
2
|
import type { IsolationConfig, WrapContext } from './isolation/types.js';
|
|
3
3
|
import type { ResolvedWatchdog } from './watchdog/config.js';
|
|
4
4
|
import type { AgentLoopsConfig, ResolvedLoop, ResolvedRoleLoop } from './loops/config.js';
|
|
5
|
+
import { validateRoomsConfig } from './rooms-tasks/config.js';
|
|
5
6
|
export interface OverseeEntry {
|
|
6
7
|
agent: string;
|
|
7
8
|
interval: string;
|
|
@@ -299,6 +300,8 @@ export declare function validateBrainValue(value: BarePreset, file: string, poin
|
|
|
299
300
|
export declare function validateEffectiveAgentTemplate(definition: AgentTemplateDefinition, id?: string): AgentTemplateDefinition;
|
|
300
301
|
export declare function assertBareKeys(value: unknown, allowed: string[], label: string): BarePreset;
|
|
301
302
|
export declare function splitRootFor(base: string): string;
|
|
303
|
+
/** Read only room connection/owner settings for an existing layout. */
|
|
304
|
+
export declare function loadRoomsConfig(configPath?: string): ReturnType<typeof validateRoomsConfig>;
|
|
302
305
|
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
303
306
|
export declare function loadConfig(configPath?: string, options?: {
|
|
304
307
|
yamlMode?: YamlMode;
|
package/dist/config.js
CHANGED
|
@@ -575,6 +575,13 @@ function deepSub(v, vars) {
|
|
|
575
575
|
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deepSub(x, vars)]));
|
|
576
576
|
return v;
|
|
577
577
|
}
|
|
578
|
+
/** Read only room connection/owner settings for an existing layout. */
|
|
579
|
+
export function loadRoomsConfig(configPath = defaultConfigPath()) {
|
|
580
|
+
assertTrustedPath(configPath, 'file');
|
|
581
|
+
const doc = parseFleetDocument(configPath, readFileSync(configPath, 'utf8')).value;
|
|
582
|
+
const vars = (doc.vars ?? {});
|
|
583
|
+
return validateRoomsConfig(deepSub(doc.rooms, vars), vars, configPath);
|
|
584
|
+
}
|
|
578
585
|
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
579
586
|
export function loadConfig(configPath, options = {}) {
|
|
580
587
|
const base = configPath ?? defaultConfigPath();
|
package/dist/init-wizard.js
CHANGED
|
@@ -244,8 +244,10 @@ export function generateSetup(answers) {
|
|
|
244
244
|
}
|
|
245
245
|
files.set('agents/FleetCoordinator.yaml', preset('agents/FleetCoordinator.yaml')
|
|
246
246
|
.replace('brain: { ref: claude-default }', 'brain: { ref: coordination }'));
|
|
247
|
-
for (const name of ['single', 'pair', 'team', 'engineering'])
|
|
247
|
+
for (const name of ['single', 'pair', 'team', 'engineering']) {
|
|
248
248
|
files.set(`room_templates/${name}.yaml`, preset(`room_templates/${name}.yaml`));
|
|
249
|
+
files.set(`room_layouts/${name}.yaml`, preset(`room_layouts/${name}.yaml`));
|
|
250
|
+
}
|
|
249
251
|
return { files, answers };
|
|
250
252
|
}
|
|
251
253
|
/** Run the mutation boundary only after the complete questionnaire is accepted. */
|
|
@@ -393,7 +395,7 @@ function writeStaged(stageManifest, setup) {
|
|
|
393
395
|
replaceFileAtomically(target, contents, 0o600);
|
|
394
396
|
fsyncPath(target);
|
|
395
397
|
}
|
|
396
|
-
for (const kind of ['agents', 'agent_templates', 'roles', 'brains', 'room_templates'])
|
|
398
|
+
for (const kind of ['agents', 'agent_templates', 'roles', 'brains', 'room_templates', 'room_layouts'])
|
|
397
399
|
fsyncPath(join(stageRoot, kind));
|
|
398
400
|
fsyncPath(stageRoot);
|
|
399
401
|
fsyncPath(dirname(stageManifest));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FleetConfig } from '../config.js';
|
|
2
|
+
import { TaskStateError } from './task-state.js';
|
|
3
|
+
import { provisionMembers } from './provision.js';
|
|
4
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
5
|
+
export declare class MemberAdditionRejected extends TaskStateError {
|
|
6
|
+
readonly accepted = false;
|
|
7
|
+
}
|
|
8
|
+
export interface AddMemberRequest {
|
|
9
|
+
requestId: string;
|
|
10
|
+
slot: string;
|
|
11
|
+
role: string;
|
|
12
|
+
brain: string;
|
|
13
|
+
agentTemplate: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function memberAddition(taskId: string, requestId: string): {
|
|
16
|
+
requestId: string;
|
|
17
|
+
taskId: string;
|
|
18
|
+
roleId: string;
|
|
19
|
+
state: "running" | "succeeded" | "attention" | "failed";
|
|
20
|
+
error?: string;
|
|
21
|
+
} | {
|
|
22
|
+
state: string;
|
|
23
|
+
error: string;
|
|
24
|
+
requestId: string;
|
|
25
|
+
taskId: string;
|
|
26
|
+
roleId: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function requestMemberAddition(input: {
|
|
29
|
+
taskId: string;
|
|
30
|
+
request: AddMemberRequest;
|
|
31
|
+
cfg: FleetConfig;
|
|
32
|
+
cowork: CoworkAdapter;
|
|
33
|
+
configPath?: string;
|
|
34
|
+
binPath: string;
|
|
35
|
+
provision?: typeof provisionMembers;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
requestId: string;
|
|
38
|
+
taskId: string;
|
|
39
|
+
roleId: string;
|
|
40
|
+
state: "running" | "succeeded" | "attention" | "failed";
|
|
41
|
+
error?: string;
|
|
42
|
+
} | {
|
|
43
|
+
state: string;
|
|
44
|
+
error: string;
|
|
45
|
+
requestId: string;
|
|
46
|
+
taskId: string;
|
|
47
|
+
roleId: string;
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
5
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
6
|
+
import { stateRoot } from '../paths.js';
|
|
7
|
+
import { getTask, TaskStateError } from './task-state.js';
|
|
8
|
+
import { getRoomRecord } from './room-state.js';
|
|
9
|
+
import { prepareExecutionPlan } from './member-overrides.js';
|
|
10
|
+
import { sealTemplateSnapshot } from './templates.js';
|
|
11
|
+
import { provisionMembers } from './provision.js';
|
|
12
|
+
export class MemberAdditionRejected extends TaskStateError {
|
|
13
|
+
accepted = false;
|
|
14
|
+
}
|
|
15
|
+
const running = new Set();
|
|
16
|
+
function pathFor(taskId, requestId) {
|
|
17
|
+
if (!/^[a-z0-9]{10,40}$/.test(taskId) || !/^[a-zA-Z0-9_-]{8,80}$/.test(requestId))
|
|
18
|
+
throw new TaskStateError('Invalid task or request ID');
|
|
19
|
+
return join(stateRoot(), 'member-additions', taskId, requestId + '.json');
|
|
20
|
+
}
|
|
21
|
+
export function memberAddition(taskId, requestId) {
|
|
22
|
+
getTask(taskId);
|
|
23
|
+
const path = pathFor(taskId, requestId);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
throw new TaskStateError('Member request not found');
|
|
26
|
+
const receipt = JSON.parse(readFileSync(path, 'utf8'));
|
|
27
|
+
const { hash, ...view } = receipt;
|
|
28
|
+
return receipt.state === 'running' && !running.has(path) ? { ...view, state: 'attention', error: 'The server stopped observing this launch. Inspect the existing agent before any recovery.' } : view;
|
|
29
|
+
}
|
|
30
|
+
export async function requestMemberAddition(input) {
|
|
31
|
+
const { request: r, taskId } = input;
|
|
32
|
+
if (!r || typeof r.slot !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(r.slot)
|
|
33
|
+
|| ![r.role, r.brain, r.agentTemplate].every(v => typeof v === 'string' && v.length > 0 && v.length <= 128))
|
|
34
|
+
// No receipt lookup has happened yet: this request ID may already be accepted.
|
|
35
|
+
throw new TaskStateError('Slot, Role, Brain and Agent Template are required');
|
|
36
|
+
const path = pathFor(taskId, r.requestId);
|
|
37
|
+
const hash = createHash('sha256').update(canonicalJson(r)).digest('hex');
|
|
38
|
+
mkdirSync(join(stateRoot(), 'member-additions', taskId), { recursive: true, mode: 0o700 });
|
|
39
|
+
return withFileLock(path + '.lock', async () => {
|
|
40
|
+
if (existsSync(path)) {
|
|
41
|
+
const old = JSON.parse(readFileSync(path, 'utf8'));
|
|
42
|
+
if (old.hash !== hash)
|
|
43
|
+
throw new TaskStateError('Request ID was already used with different settings');
|
|
44
|
+
return memberAddition(taskId, r.requestId);
|
|
45
|
+
}
|
|
46
|
+
const task = getTask(taskId);
|
|
47
|
+
const room = task.room_id && getRoomRecord(task.room_id);
|
|
48
|
+
if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || task.blocked || room.state !== 'active'
|
|
49
|
+
|| room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
|
|
50
|
+
throw new MemberAdditionRejected('Adding an agent requires an active, unblocked task room');
|
|
51
|
+
const roleId = `${taskId.slice(0, 8)}-${r.slot}-1`;
|
|
52
|
+
if (room.member_seats.some(s => s.role_name === roleId))
|
|
53
|
+
throw new MemberAdditionRejected('This member slot already exists');
|
|
54
|
+
const plan = (() => {
|
|
55
|
+
try {
|
|
56
|
+
return prepareExecutionPlan({ name: 'additional-member', version: 1, description: 'Additional task participant',
|
|
57
|
+
contract: room.template_snapshot?.contract, members: [{ slot: r.slot, role: r.role, count: 1, agent_template: r.agentTemplate }] }, input.cfg, { [r.slot]: { role: r.role, brain: r.brain } });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new MemberAdditionRejected(error instanceof Error ? error.message : 'Invalid member configuration');
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
const template = sealTemplateSnapshot(plan.snapshot, input.cfg.agentTemplates ?? {}, plan.launchDefinitions);
|
|
64
|
+
const receipt = { taskId, requestId: r.requestId, roleId, hash, state: 'running' };
|
|
65
|
+
const save = () => replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600);
|
|
66
|
+
save();
|
|
67
|
+
running.add(path);
|
|
68
|
+
void (input.provision ?? provisionMembers)({ cfg: input.cfg, cowork: input.cowork, configPath: input.configPath, binPath: input.binPath,
|
|
69
|
+
taskId, roomId: room.room_id, template, goal: task.title, brief: task.brief, append: true }).then(result => {
|
|
70
|
+
const seat = result.member_seats.find(s => s.role_name === roleId);
|
|
71
|
+
receipt.state = seat?.seat_state === 'active' && getTask(taskId).member_roles.some(m => m.name === roleId) ? 'succeeded' : 'attention';
|
|
72
|
+
if (receipt.state === 'attention')
|
|
73
|
+
receipt.error = 'Admission is not confirmed. Inspect the existing launch; no automatic retry was made.';
|
|
74
|
+
}).catch(() => { receipt.state = 'failed'; receipt.error = 'Member launch failed. Inspect the task and agent logs before recovery.'; })
|
|
75
|
+
.finally(() => { try {
|
|
76
|
+
save();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
running.delete(path);
|
|
80
|
+
} }).catch(() => { running.delete(path); });
|
|
81
|
+
return memberAddition(taskId, r.requestId);
|
|
82
|
+
});
|
|
83
|
+
}
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1185,6 +1185,26 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1185
1185
|
dieTaskRoom(e);
|
|
1186
1186
|
}
|
|
1187
1187
|
});
|
|
1188
|
+
cOpt(taskCmd.command('_layout <id> <operation> [room]', { hidden: true }))
|
|
1189
|
+
.description('internal: run an accepted layout room operation (open, close-room, close)')
|
|
1190
|
+
.option('--json', 'JSON output')
|
|
1191
|
+
.option('--operation-id <id>', 'accepted operation ID; status updates apply only while it is current')
|
|
1192
|
+
.action(async (id, operation, room, opts) => {
|
|
1193
|
+
try {
|
|
1194
|
+
if (!['open', 'close-room', 'close'].includes(operation))
|
|
1195
|
+
throw new Error(`unknown layout operation: ${operation}`);
|
|
1196
|
+
const view = await taskRoomService(opts.configuration).runTaskLayoutOperation({
|
|
1197
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
1198
|
+
operation: operation, room, operationId: opts.operationId,
|
|
1199
|
+
});
|
|
1200
|
+
console.log(JSON.stringify({ schema_version: 1, layout: view }, null, 2));
|
|
1201
|
+
}
|
|
1202
|
+
catch (e) {
|
|
1203
|
+
if (opts.json)
|
|
1204
|
+
die(e);
|
|
1205
|
+
dieTaskRoom(e);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1188
1208
|
cOpt(taskCmd.command('_settle_delete <id>', { hidden: true }))
|
|
1189
1209
|
.description('internal: settle a previously accepted task deletion')
|
|
1190
1210
|
.option('--json', 'JSON output')
|
|
@@ -8,4 +8,4 @@ export declare function presentFleetWorkerLifecycle(presentations: FleetAuditPre
|
|
|
8
8
|
sleep(ms: number): Promise<void>;
|
|
9
9
|
}): Promise<void>;
|
|
10
10
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
11
|
-
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
|
|
11
|
+
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string, binPath?: string): Promise<void>;
|
|
@@ -9,7 +9,7 @@ const LIFECYCLE_ACCEPT_TIMEOUT_MS = 10 * 60_000;
|
|
|
9
9
|
const LIFECYCLE_RETRY_MS = 250;
|
|
10
10
|
export function fleetWorkerEnv(source = process.env) {
|
|
11
11
|
const inheritedKeys = [
|
|
12
|
-
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
|
|
12
|
+
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_FLEET_SOCKET_ROOT', 'OURS_CONFIG',
|
|
13
13
|
];
|
|
14
14
|
const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
15
15
|
const lifecycleStateDir = source[FLEET_WORKER_LIFECYCLE_STATE_DIR_ENV]
|
|
@@ -51,8 +51,8 @@ export async function presentFleetWorkerLifecycle(presentations, deps = {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
54
|
-
export function launchFleetWorker(args, operation, configPath) {
|
|
55
|
-
const cli = [
|
|
54
|
+
export function launchFleetWorker(args, operation, configPath, binPath = process.argv[1]) {
|
|
55
|
+
const cli = [binPath, ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
56
56
|
const env = fleetWorkerEnv();
|
|
57
57
|
const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
58
58
|
const suffix = randomUUID().slice(0, 8);
|