@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.
Files changed (106) hide show
  1. package/README.md +42 -6
  2. package/dist/application/fleet-query-service.js +2 -1
  3. package/dist/application/role-command-service.js +9 -0
  4. package/dist/application/task-room-service.d.ts +6 -0
  5. package/dist/application/task-room-service.js +150 -88
  6. package/dist/build-info.json +4 -4
  7. package/dist/canonical-json.d.ts +2 -0
  8. package/dist/canonical-json.js +10 -0
  9. package/dist/cli.js +42 -23
  10. package/dist/config.d.ts +23 -1
  11. package/dist/config.js +149 -10
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +14 -4
  14. package/dist/doctor.d.ts +2 -0
  15. package/dist/doctor.js +23 -4
  16. package/dist/fleet-command-audit.js +5 -1
  17. package/dist/init-guidance.d.ts +1 -1
  18. package/dist/init-guidance.js +1 -1
  19. package/dist/ops.js +2 -17
  20. package/dist/owner-channel/commands.js +1 -1
  21. package/dist/preset-migration.d.ts +27 -0
  22. package/dist/preset-migration.js +200 -0
  23. package/dist/resolved-plan.js +4 -0
  24. package/dist/rooms-tasks/cli.d.ts +2 -0
  25. package/dist/rooms-tasks/cli.js +119 -27
  26. package/dist/rooms-tasks/close.d.ts +1 -1
  27. package/dist/rooms-tasks/close.js +10 -1
  28. package/dist/rooms-tasks/config.js +11 -24
  29. package/dist/rooms-tasks/launch-snapshot.d.ts +10 -0
  30. package/dist/rooms-tasks/launch-snapshot.js +159 -0
  31. package/dist/rooms-tasks/member-overrides.d.ts +27 -0
  32. package/dist/rooms-tasks/member-overrides.js +170 -0
  33. package/dist/rooms-tasks/provision.js +33 -45
  34. package/dist/rooms-tasks/room-state.js +4 -0
  35. package/dist/rooms-tasks/task-state.d.ts +2 -0
  36. package/dist/rooms-tasks/task-state.js +19 -1
  37. package/dist/rooms-tasks/templates.d.ts +4 -1
  38. package/dist/rooms-tasks/templates.js +34 -3
  39. package/dist/rooms-tasks/types.d.ts +31 -6
  40. package/dist/rooms-tasks/types.js +2 -1
  41. package/dist/spawn.js +12 -1
  42. package/dist/web/fleet-config-service.d.ts +1 -0
  43. package/dist/web/fleet-config-service.js +25 -7
  44. package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
  45. package/dist/web-app/index.html +1 -1
  46. package/package.json +1 -1
  47. package/presets/brain-catalog.json +16 -0
  48. package/presets/fleet/agents/FleetCoordinator.yaml +4 -0
  49. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  50. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  51. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  52. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  53. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  54. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  55. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  56. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  57. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  58. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  59. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  60. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  61. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  62. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  63. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  64. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  65. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  66. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  67. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  68. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  69. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  70. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  71. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  72. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  73. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  74. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  75. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  76. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  77. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  78. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  79. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  80. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  81. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  82. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  83. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  84. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  85. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  86. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  87. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  88. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  89. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  90. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  91. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  92. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  93. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  94. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  95. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  96. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  97. package/presets/fleet/room_templates/pair.yaml +2 -2
  98. package/presets/fleet/room_templates/single.yaml +1 -1
  99. package/presets/fleet/room_templates/team.yaml +3 -3
  100. package/presets/manifest.json +2 -1
  101. /package/presets/fleet/{agents → agent_templates}/Agent.yaml +0 -0
  102. /package/presets/fleet/{agents → agent_templates}/Architect.yaml +0 -0
  103. /package/presets/fleet/{agents → agent_templates}/Critic.yaml +0 -0
  104. /package/presets/fleet/{agents → agent_templates}/Developer.yaml +0 -0
  105. /package/presets/fleet/{agents → agent_templates}/Secretary.yaml +0 -0
  106. /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
- Agent document (\`~/fleet/agents/Name.yaml\`):
360
- role: { ref: RoleID }
361
- brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
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 upgrading from hardcoded templates should
462
- run init once (with the same `-c` selection they normally use).
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
- const template = this.createTemplate(cfg, request.template, request.noRoom);
77
- const ref = template && {
78
- name: template.name, version: template.version, content_hash: template.content_hash,
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
- let task = await withTaskListsLock(() => {
82
- const list = resolveTaskList(request.list ?? 'default');
83
- return persistTask({
84
- title: request.title, brief, brief_file: request.briefFile, template: ref,
85
- origin: request.origin, idempotency_key: request.idempotencyKey,
86
- start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
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
- template = snapshotTemplate(definition);
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
- const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
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
- if (!('ref' in member.agent))
349
- return [];
350
- const ref = member.agent.ref;
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 definition = resolveTemplate(input.template, cfg.roomTemplates ?? {});
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: input.template });
510
- const requested = snapshotTemplate(definition);
511
- const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.template;
512
- if (pinned && (pinned.name !== requested.name || pinned.content_hash !== requested.content_hash))
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 room ${room.room_id}'s durable snapshot`);
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 definition = durable && !input.template ? undefined
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
- const snapshot = durable ?? snapshotTemplate(definition);
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
- throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
544
- template: `${task.template.name}@${task.template.version}`
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 (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
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 created = await cowork.createRoom({
645
- room_name: task.title, goal: task.goal?.trim() || task.title,
646
- briefing: task.brief?.trim() || template?.contract?.trim() || task.goal?.trim() || task.title,
647
- quiet_membership: template?.room?.quiet_membership,
648
- anonymous: template?.room?.anonymous,
649
- });
650
- let room = createRoomRecord({
651
- room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
652
- task_id: task.task_id, template_snapshot: template,
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 (template?.members.length)
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 && !template?.members.length)
767
+ if (task.task_id && !launchTemplate?.members.length)
706
768
  activateTask(task.task_id);
707
769
  return room;
708
770
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.10",
3
- "buildId": "7fbfbd6e4119",
4
- "commit": "006179e6e9ae9d9e846045a658a64686c8169ebe",
2
+ "version": "1.1.0-nightly.12",
3
+ "buildId": "6da038d91a6d",
4
+ "commit": "57faa7ff79b4097ac94797b9a591ce54aad01c0a",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-31T14:07:48.123Z",
6
+ "builtAt": "2026-08-31T21:40:11.904Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
@@ -0,0 +1,2 @@
1
+ /** Stable JSON used for semantic configuration fingerprints. */
2
+ export declare function canonicalJson(value: unknown): string;
@@ -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
+ }