@ours.network/fleet 1.1.0-nightly.10 → 1.1.0-nightly.12
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 +42 -6
- package/dist/application/fleet-query-service.js +2 -1
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/task-room-service.d.ts +6 -0
- package/dist/application/task-room-service.js +150 -88
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +42 -23
- package/dist/config.d.ts +23 -1
- package/dist/config.js +149 -10
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +14 -4
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +23 -4
- package/dist/fleet-command-audit.js +5 -1
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/commands.js +1 -1
- package/dist/preset-migration.d.ts +27 -0
- package/dist/preset-migration.js +200 -0
- package/dist/resolved-plan.js +4 -0
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +119 -27
- package/dist/rooms-tasks/close.d.ts +1 -1
- package/dist/rooms-tasks/close.js +10 -1
- package/dist/rooms-tasks/config.js +11 -24
- package/dist/rooms-tasks/launch-snapshot.d.ts +10 -0
- package/dist/rooms-tasks/launch-snapshot.js +159 -0
- package/dist/rooms-tasks/member-overrides.d.ts +27 -0
- package/dist/rooms-tasks/member-overrides.js +170 -0
- package/dist/rooms-tasks/provision.js +33 -45
- package/dist/rooms-tasks/room-state.js +4 -0
- package/dist/rooms-tasks/task-state.d.ts +2 -0
- package/dist/rooms-tasks/task-state.js +19 -1
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/types.d.ts +31 -6
- package/dist/rooms-tasks/types.js +2 -1
- package/dist/spawn.js +12 -1
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +1 -1
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +4 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/room_templates/pair.yaml +2 -2
- package/presets/fleet/room_templates/single.yaml +1 -1
- package/presets/fleet/room_templates/team.yaml +3 -3
- package/presets/manifest.json +2 -1
- /package/presets/fleet/{agents → agent_templates}/Agent.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Architect.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Critic.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Developer.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Secretary.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Tester.yaml +0 -0
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ brain: { inline: { harness: codex, session: acp } }
|
|
|
43
43
|
## How it works
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
~/fleet.yaml + ~/fleet/{agents,roles,brains,room_templates}/*.yaml your declaration
|
|
46
|
+
~/fleet.yaml + ~/fleet/{agents,agent_templates,roles,brains,room_templates}/*.yaml your declaration
|
|
47
47
|
│ ours-fleet up
|
|
48
48
|
▼
|
|
49
49
|
briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
|
|
@@ -356,9 +356,42 @@ defaults:
|
|
|
356
356
|
max_kb: 1024 # rotate only above this active-log size
|
|
357
357
|
keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
|
|
358
358
|
max_archives: 12 # recent beside log; older preserved cold
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Agent Template document (\`~/fleet/agent_templates/Worker.yaml\`) is inert and reusable:
|
|
362
|
+
|
|
363
|
+
```yaml
|
|
364
|
+
role: { ref: developer }
|
|
365
|
+
brain: { ref: claude-default }
|
|
366
|
+
permissions: { approval: ask, filesystem: workspace, unattended: deny }
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Persistent Agent instance (\`~/fleet/agents/Name.yaml\`) either remains a canonical
|
|
370
|
+
Role/Brain composition or explicitly reuses a template:
|
|
371
|
+
|
|
372
|
+
```yaml
|
|
373
|
+
template: Worker
|
|
374
|
+
overrides:
|
|
375
|
+
cwd: /work/name
|
|
376
|
+
permissions: { approval: allow }
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Templates never create identities, supervisors, sessions, or lifecycle/status rows.
|
|
380
|
+
Map overrides merge recursively; scalars and arrays replace; null deletion is rejected.
|
|
381
|
+
|
|
382
|
+
Canonical Agent document (\`~/fleet/agents/Name.yaml\`):
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
role: { ref: RoleID }
|
|
386
|
+
brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
|
|
387
|
+
identity: "Display Name" # ours identity to bind (default: Name)
|
|
388
|
+
cwd: ${work_root}/repo # where the harness process runs
|
|
389
|
+
coordinator: FleetCoordinator # announce target on boot
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Additional operational field inventory (schematic, not one YAML document):
|
|
393
|
+
|
|
394
|
+
```text
|
|
362
395
|
identity: "Display Name" # ours identity to bind (default: Name)
|
|
363
396
|
cwd: ${work_root}/repo # where the harness process runs
|
|
364
397
|
coordinator: FleetCoordinator # announce target on boot
|
|
@@ -458,8 +491,11 @@ the packaged preset revision and source directory and only seeds missing files.
|
|
|
458
491
|
It never upgrades an edited preset. To adopt a newer packaged file explicitly,
|
|
459
492
|
copy the reported source file beside the existing target as `.new-default`, review
|
|
460
493
|
`diff -u`, then replace the target yourself. This is the sole adoption operation;
|
|
461
|
-
rerunning init is not an update. Users
|
|
462
|
-
|
|
494
|
+
rerunning init is not an update. Users with the exact generated six-worker legacy
|
|
495
|
+
starter set must migrate explicitly: first run `ours-fleet migrate-agent-templates -c FILE`
|
|
496
|
+
for a zero-write plan, then review every move/addition/recovery path and rerun with
|
|
497
|
+
`--write`. Customized or ambiguous known starters are refused without
|
|
498
|
+
mutation; unrelated custom persistent Agents are preserved.
|
|
463
499
|
|
|
464
500
|
Rooms always use `ours-cowork`; there is no room-provider selector. Configure
|
|
465
501
|
the cowork daemon connection and the room owner directly:
|
|
@@ -76,7 +76,8 @@ export class FleetQueryService {
|
|
|
76
76
|
this.control = options.control ?? controlRequest;
|
|
77
77
|
}
|
|
78
78
|
async list() {
|
|
79
|
-
const roles = await this.options.repository.list()
|
|
79
|
+
const roles = (await this.options.repository.list())
|
|
80
|
+
.filter(role => role.configured && role.lifetime === 'permanent');
|
|
80
81
|
return Promise.all(roles.map(async (role) => {
|
|
81
82
|
const status = await this.status(role);
|
|
82
83
|
return { role, status, capabilities: roleCapabilities(role, status, this.options.capabilityContext) };
|
|
@@ -12,6 +12,10 @@ export class RoleLifecycleService {
|
|
|
12
12
|
const config = input.config ?? loadConfig(this.options.configPath);
|
|
13
13
|
const selected = input.roleIds.length ? input.roleIds : config.roles.map(role => role.name);
|
|
14
14
|
for (const roleId of selected) {
|
|
15
|
+
if (config.agentTemplates?.[roleId] && !config.roles.some(role => role.name === roleId))
|
|
16
|
+
throw new FleetError('capability_unavailable', `'${roleId}' is an inert Agent Template; lifecycle commands target only persistent fleet/agents instances`);
|
|
17
|
+
if (!config.roles.some(role => role.name === roleId))
|
|
18
|
+
throw new FleetError('role_not_found', `no such role '${roleId}'`);
|
|
15
19
|
const role = await this.options.repository.get(roleId);
|
|
16
20
|
if (!role)
|
|
17
21
|
throw new FleetError('role_not_found', `no such role '${roleId}'`);
|
|
@@ -51,6 +55,11 @@ export class RoleCommandService {
|
|
|
51
55
|
throw new FleetError('idempotency_conflict', 'action ID already belongs to another operation');
|
|
52
56
|
return prior;
|
|
53
57
|
}
|
|
58
|
+
const config = loadConfig(this.options.configPath);
|
|
59
|
+
if (config.agentTemplates?.[input.roleId] && !config.roles.some(role => role.name === input.roleId))
|
|
60
|
+
throw new FleetError('capability_unavailable', `'${input.roleId}' is an inert Agent Template; lifecycle commands target only persistent fleet/agents instances`);
|
|
61
|
+
if (!config.roles.some(role => role.name === input.roleId))
|
|
62
|
+
throw new FleetError('role_not_found', `no such role '${input.roleId}'`);
|
|
54
63
|
const role = await this.options.repository.get(input.roleId);
|
|
55
64
|
if (!role)
|
|
56
65
|
throw new FleetError('role_not_found', `no such role '${input.roleId}'`);
|
|
@@ -2,6 +2,7 @@ import { type FleetConfig } from '../config.js';
|
|
|
2
2
|
import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
3
3
|
import { provisionMembers } from '../rooms-tasks/provision.js';
|
|
4
4
|
import { moveTaskToList } from '../rooms-tasks/task-state.js';
|
|
5
|
+
import { type MemberOverrides } from '../rooms-tasks/member-overrides.js';
|
|
5
6
|
import type { RoomOrchestrationRecord, TaskListRecord, TaskOrigin, TaskOutcome, TaskRecord, TaskState } from '../rooms-tasks/types.js';
|
|
6
7
|
export type TaskRoomActor = {
|
|
7
8
|
kind: 'local_control';
|
|
@@ -57,6 +58,7 @@ export interface CreateTaskRequest {
|
|
|
57
58
|
idempotencyKey?: string;
|
|
58
59
|
origin: TaskOrigin;
|
|
59
60
|
list?: string;
|
|
61
|
+
members?: MemberOverrides;
|
|
60
62
|
}
|
|
61
63
|
export interface CreateRoomRequest {
|
|
62
64
|
actor: TaskRoomActor;
|
|
@@ -65,6 +67,7 @@ export interface CreateRoomRequest {
|
|
|
65
67
|
goal?: string;
|
|
66
68
|
brief?: string;
|
|
67
69
|
briefFile?: string;
|
|
70
|
+
members?: MemberOverrides;
|
|
68
71
|
}
|
|
69
72
|
export interface TaskRoomServiceDeps {
|
|
70
73
|
loadConfiguration?(path?: string): FleetConfig;
|
|
@@ -84,6 +87,8 @@ export declare class TaskRoomApplicationService {
|
|
|
84
87
|
startTask(input: {
|
|
85
88
|
actor: TaskRoomActor;
|
|
86
89
|
taskId: string;
|
|
90
|
+
template?: string;
|
|
91
|
+
members?: MemberOverrides;
|
|
87
92
|
}): Promise<TaskRecord>;
|
|
88
93
|
listTasks(filter?: {
|
|
89
94
|
state?: TaskState | TaskState[];
|
|
@@ -252,6 +257,7 @@ export declare class TaskRoomApplicationService {
|
|
|
252
257
|
actor: TaskRoomActor;
|
|
253
258
|
taskId: string;
|
|
254
259
|
template?: string;
|
|
260
|
+
members?: MemberOverrides;
|
|
255
261
|
}): Promise<{
|
|
256
262
|
task: TaskRecord;
|
|
257
263
|
status: 'ready' | 'already_active';
|
|
@@ -4,10 +4,12 @@ import { ConfigError, loadConfig } from '../config.js';
|
|
|
4
4
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
5
5
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
6
6
|
import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
|
|
7
|
-
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
|
|
7
|
+
import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
|
|
8
8
|
import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
|
|
9
|
+
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
10
|
+
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
11
|
+
import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
|
|
9
12
|
import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
|
|
10
|
-
import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
11
13
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
12
14
|
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
|
|
13
15
|
import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
@@ -73,19 +75,46 @@ export class TaskRoomApplicationService {
|
|
|
73
75
|
}
|
|
74
76
|
async createTask(request) {
|
|
75
77
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
let template = this.createTemplate(cfg, request.template, request.noRoom);
|
|
79
|
+
let launchDefinitions;
|
|
80
|
+
let executionPlan;
|
|
81
|
+
let preparedPlan;
|
|
82
|
+
if (template) {
|
|
83
|
+
const definition = resolveTemplate(template.name, cfg.roomTemplates ?? {});
|
|
84
|
+
preparedPlan = prepareExecutionPlan(definition, cfg, request.members ?? {});
|
|
85
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
86
|
+
}
|
|
80
87
|
const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
const unlock = preparedPlan ? acquireLaunchSnapshotLock() : undefined;
|
|
89
|
+
let sealedHash;
|
|
90
|
+
let task;
|
|
91
|
+
try {
|
|
92
|
+
if (preparedPlan) {
|
|
93
|
+
template = sealTemplateSnapshot(preparedPlan.snapshot, cfg.agentTemplates ?? {}, preparedPlan.launchDefinitions);
|
|
94
|
+
sealedHash = template.launch_snapshot_hash;
|
|
95
|
+
executionPlan = { schema_version: 1, snapshot: template, overrides: preparedPlan.overrides,
|
|
96
|
+
overrides_hash: preparedPlan.overridesHash,
|
|
97
|
+
plan_hash: preparedPlan.planHash };
|
|
98
|
+
}
|
|
99
|
+
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
100
|
+
task = await withTaskListsLock(() => {
|
|
101
|
+
const list = resolveTaskList(request.list ?? 'default');
|
|
102
|
+
return persistTask({
|
|
103
|
+
title: request.title, brief, brief_file: request.briefFile, template: ref,
|
|
104
|
+
execution_plan: executionPlan,
|
|
105
|
+
origin: request.origin, idempotency_key: request.idempotencyKey,
|
|
106
|
+
start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
|
|
107
|
+
});
|
|
87
108
|
});
|
|
88
|
-
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
unlock?.();
|
|
112
|
+
if (sealedHash)
|
|
113
|
+
releaseLaunchSnapshot(sealedHash);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
unlock?.();
|
|
117
|
+
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
89
118
|
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
90
119
|
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
91
120
|
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
@@ -95,7 +124,7 @@ export class TaskRoomApplicationService {
|
|
|
95
124
|
try {
|
|
96
125
|
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
97
126
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
98
|
-
});
|
|
127
|
+
}, launchDefinitions);
|
|
99
128
|
task = readTask(task.task_id);
|
|
100
129
|
if (task.state === 'active' && room.state === 'active')
|
|
101
130
|
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
@@ -127,47 +156,19 @@ export class TaskRoomApplicationService {
|
|
|
127
156
|
const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
|
|
128
157
|
if (!definition)
|
|
129
158
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
|
|
130
|
-
|
|
159
|
+
if (request.members && Object.keys(request.members).length) {
|
|
160
|
+
const prepared = prepareExecutionPlan(definition, cfg, request.members);
|
|
161
|
+
template = prepared.snapshot;
|
|
162
|
+
return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions);
|
|
163
|
+
}
|
|
164
|
+
template = snapshotTemplate(definition, cfg.agentTemplates);
|
|
131
165
|
}
|
|
132
166
|
return this.provisionRoom(cfg, {
|
|
133
167
|
title: request.name, brief, goal: request.goal,
|
|
134
168
|
}, template, () => { });
|
|
135
169
|
}
|
|
136
170
|
async startTask(input) {
|
|
137
|
-
|
|
138
|
-
let task = transitionTask(input.taskId);
|
|
139
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
|
|
140
|
-
title: task.title, previousState: 'backlog', newState: task.state,
|
|
141
|
-
revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
142
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
143
|
-
agents: [] });
|
|
144
|
-
if (task.template && !task.room_id) {
|
|
145
|
-
const template = this.existingTemplate(cfg, task);
|
|
146
|
-
try {
|
|
147
|
-
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
148
|
-
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
149
|
-
});
|
|
150
|
-
task = readTask(task.task_id);
|
|
151
|
-
if (task.state === 'active' && room.state === 'active')
|
|
152
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
153
|
-
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
154
|
-
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
155
|
-
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
156
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
157
|
-
agents: taskAgents(task) });
|
|
158
|
-
else
|
|
159
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
160
|
-
state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
const failed = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
164
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
165
|
-
state: readTask(task.task_id).state, category: 'provision_failed',
|
|
166
|
-
eventId: failed ? roomFailureEventId(failed) : task.created_at });
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return task;
|
|
171
|
+
return (await this.ensureTaskWork(input)).task;
|
|
171
172
|
}
|
|
172
173
|
listTasks(filter) {
|
|
173
174
|
const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
|
|
@@ -345,11 +346,9 @@ export class TaskRoomApplicationService {
|
|
|
345
346
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
346
347
|
return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
|
|
347
348
|
const issues = template.members.flatMap(member => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return cfg.roles.some(role => role.name === ref)
|
|
352
|
-
? [] : [`member ${member.slot}: Agent ref '${ref}' not found`];
|
|
349
|
+
const ref = member.agent_template;
|
|
350
|
+
return cfg.agentTemplates?.[ref]
|
|
351
|
+
? [] : [`member ${member.slot}: Agent Template '${ref}' not found`];
|
|
353
352
|
});
|
|
354
353
|
return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
|
|
355
354
|
});
|
|
@@ -503,53 +502,101 @@ export class TaskRoomApplicationService {
|
|
|
503
502
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
504
503
|
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
|
|
505
504
|
if (task.state === 'active' && task.room_id) {
|
|
506
|
-
if (input.template) {
|
|
507
|
-
const
|
|
505
|
+
if (input.template || input.members) {
|
|
506
|
+
const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
507
|
+
if (!pinned)
|
|
508
|
+
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
509
|
+
const templateName = input.template ?? pinned.name;
|
|
510
|
+
if ((!input.template || input.template === pinned.name) && input.members
|
|
511
|
+
&& task.execution_plan?.overrides_hash === hashMemberOverrides(input.members))
|
|
512
|
+
return { task, status: 'already_active' };
|
|
513
|
+
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
508
514
|
if (!definition)
|
|
509
|
-
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template:
|
|
510
|
-
const requested =
|
|
511
|
-
const
|
|
512
|
-
if (pinned
|
|
515
|
+
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
516
|
+
const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
|
|
517
|
+
const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
|
|
518
|
+
if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
|
|
513
519
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
514
|
-
requested: `${requested.name}@${requested.version}`, room: task.room_id,
|
|
520
|
+
requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
|
|
515
521
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
516
522
|
});
|
|
517
523
|
}
|
|
518
524
|
return { task, status: 'already_active' };
|
|
519
525
|
}
|
|
520
526
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
521
|
-
const durable = room?.template_snapshot;
|
|
527
|
+
const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
|
|
522
528
|
if (durable && (!task.template || task.template.name !== durable.name
|
|
523
529
|
|| task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
|
|
524
|
-
throw new Error(`task ${task.task_id} template reference does not match
|
|
530
|
+
throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
|
|
525
531
|
const templateName = input.template ?? durable?.name ?? task.template?.name
|
|
526
532
|
?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
|
|
527
|
-
const
|
|
533
|
+
const storedOverridesMatch = !!(durable && input.members && task.execution_plan?.overrides_hash
|
|
534
|
+
=== hashMemberOverrides(input.members));
|
|
535
|
+
const definition = durable && !input.template && (!input.members || storedOverridesMatch) ? undefined
|
|
528
536
|
: resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
529
537
|
if (input.template && !definition)
|
|
530
538
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
531
539
|
if (!durable && !definition)
|
|
532
540
|
throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
|
|
533
|
-
|
|
541
|
+
let launchDefinitions;
|
|
542
|
+
let preparedPlan = !durable && definition
|
|
543
|
+
? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
|
|
544
|
+
let snapshot = durable ?? preparedPlan.snapshot;
|
|
545
|
+
if (preparedPlan)
|
|
546
|
+
launchDefinitions = preparedPlan.launchDefinitions;
|
|
547
|
+
if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
|
|
548
|
+
if (!definition)
|
|
549
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
|
|
550
|
+
const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
|
|
551
|
+
preparedPlan = prepared;
|
|
552
|
+
if (durable && prepared.planHash !== durable.content_hash)
|
|
553
|
+
throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
|
|
554
|
+
snapshot = prepared.snapshot;
|
|
555
|
+
launchDefinitions = prepared.launchDefinitions;
|
|
556
|
+
}
|
|
534
557
|
if (durable && input.template && definition) {
|
|
535
|
-
const requested = snapshotTemplate(definition);
|
|
558
|
+
const requested = snapshotTemplate(definition, cfg.agentTemplates);
|
|
536
559
|
if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
|
|
537
560
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
538
561
|
requested: `${requested.name}@${requested.version}`, room: room.room_id,
|
|
539
562
|
provisioned: `${durable.name}@${durable.version}`
|
|
540
563
|
});
|
|
541
564
|
}
|
|
542
|
-
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
565
|
+
if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
|
|
566
|
+
// Pre-execution-plan backlog records used the template-only hash. Accept
|
|
567
|
+
// that exact legacy pin once, then upgrade it to the complete sealed plan.
|
|
568
|
+
const legacyHash = definition
|
|
569
|
+
? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
|
|
570
|
+
if (task.template.content_hash !== legacyHash)
|
|
571
|
+
throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
|
|
572
|
+
template: `${task.template.name}@${task.template.version}`
|
|
573
|
+
});
|
|
574
|
+
}
|
|
546
575
|
if (task.room_id && room?.template_snapshot
|
|
547
576
|
&& (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
|
|
548
577
|
throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
|
|
549
578
|
requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
|
|
550
579
|
provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
|
|
551
580
|
});
|
|
552
|
-
if (
|
|
581
|
+
if (preparedPlan && !task.room_id) {
|
|
582
|
+
const unlock = acquireLaunchSnapshotLock();
|
|
583
|
+
let sealed;
|
|
584
|
+
try {
|
|
585
|
+
sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
|
|
586
|
+
task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
|
|
587
|
+
overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
|
|
588
|
+
plan_hash: preparedPlan.planHash });
|
|
589
|
+
snapshot = sealed;
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
unlock();
|
|
593
|
+
if (sealed?.launch_snapshot_hash)
|
|
594
|
+
releaseLaunchSnapshot(sealed.launch_snapshot_hash);
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
unlock();
|
|
598
|
+
}
|
|
599
|
+
else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
|
|
553
600
|
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
554
601
|
if (task.state === 'backlog') {
|
|
555
602
|
task = transitionTask(task.task_id);
|
|
@@ -562,7 +609,7 @@ export class TaskRoomApplicationService {
|
|
|
562
609
|
try {
|
|
563
610
|
await this.provisionRoom(cfg, task, snapshot, created => {
|
|
564
611
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
565
|
-
});
|
|
612
|
+
}, launchDefinitions);
|
|
566
613
|
task = readTask(task.task_id);
|
|
567
614
|
}
|
|
568
615
|
catch (error) {
|
|
@@ -619,17 +666,17 @@ export class TaskRoomApplicationService {
|
|
|
619
666
|
throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
|
|
620
667
|
return undefined;
|
|
621
668
|
}
|
|
622
|
-
return snapshotTemplate(definition);
|
|
669
|
+
return snapshotTemplate(definition, cfg.agentTemplates);
|
|
623
670
|
}
|
|
624
671
|
existingTemplate(cfg, task) {
|
|
625
672
|
const ref = task.template;
|
|
626
673
|
const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
|
|
627
|
-
const snapshot = definition ? snapshotTemplate(definition) : undefined;
|
|
674
|
+
const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
|
|
628
675
|
if (!snapshot || snapshot.content_hash !== ref.content_hash)
|
|
629
676
|
throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
|
|
630
677
|
return snapshot;
|
|
631
678
|
}
|
|
632
|
-
async provisionRoom(cfg, task, template, onCreated) {
|
|
679
|
+
async provisionRoom(cfg, task, template, onCreated, launchDefinitions) {
|
|
633
680
|
const rooms = cfg.rooms;
|
|
634
681
|
if (!rooms)
|
|
635
682
|
throw new ConfigError('rooms: configuration is required');
|
|
@@ -641,16 +688,31 @@ export class TaskRoomApplicationService {
|
|
|
641
688
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
642
689
|
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
643
690
|
})();
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
691
|
+
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
692
|
+
let launchTemplate;
|
|
693
|
+
let created;
|
|
694
|
+
let room;
|
|
695
|
+
try {
|
|
696
|
+
launchTemplate = template ? (template.launch_snapshot_hash ? template
|
|
697
|
+
: sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
|
|
698
|
+
created = await cowork.createRoom({
|
|
699
|
+
room_name: task.title, goal: task.goal?.trim() || task.title,
|
|
700
|
+
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
701
|
+
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
702
|
+
anonymous: launchTemplate?.room?.anonymous,
|
|
703
|
+
});
|
|
704
|
+
room = createRoomRecord({
|
|
705
|
+
room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
|
|
706
|
+
task_id: task.task_id, template_snapshot: launchTemplate,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
unlockSnapshot?.();
|
|
711
|
+
if (launchTemplate?.launch_snapshot_hash)
|
|
712
|
+
releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
unlockSnapshot?.();
|
|
654
716
|
recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
|
|
655
717
|
name: room.room_name, previousState: 'none', newState: 'provisioning',
|
|
656
718
|
revision: room.created_at, taskId: room.task_id,
|
|
@@ -677,10 +739,10 @@ export class TaskRoomApplicationService {
|
|
|
677
739
|
}
|
|
678
740
|
}
|
|
679
741
|
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
680
|
-
if (
|
|
742
|
+
if (launchTemplate?.members.length)
|
|
681
743
|
try {
|
|
682
744
|
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
683
|
-
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
|
|
745
|
+
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
684
746
|
binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
|
|
685
747
|
goal: task.task_id ? task.title : task.goal,
|
|
686
748
|
});
|
|
@@ -702,7 +764,7 @@ export class TaskRoomApplicationService {
|
|
|
702
764
|
else
|
|
703
765
|
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
|
|
704
766
|
state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
705
|
-
if (task.task_id && !
|
|
767
|
+
if (task.task_id && !launchTemplate?.members.length)
|
|
706
768
|
activateTask(task.task_id);
|
|
707
769
|
return room;
|
|
708
770
|
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.1.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.1.0-nightly.12",
|
|
3
|
+
"buildId": "6da038d91a6d",
|
|
4
|
+
"commit": "57faa7ff79b4097ac94797b9a591ce54aad01c0a",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-31T21:40:11.904Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Stable JSON used for semantic configuration fingerprints. */
|
|
2
|
+
export function canonicalJson(value) {
|
|
3
|
+
if (Array.isArray(value))
|
|
4
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
5
|
+
if (value && typeof value === 'object')
|
|
6
|
+
return `{${Object.entries(value)
|
|
7
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
8
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|