@ours.network/fleet 1.2.0-nightly.1 → 1.2.0-nightly.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +110 -21
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/file-rpc.d.ts +23 -0
  7. package/dist/agent-ours/file-rpc.js +123 -0
  8. package/dist/agent-ours/harness.d.ts +14 -0
  9. package/dist/agent-ours/harness.js +64 -0
  10. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  11. package/dist/agent-ours/mcp-endpoint.js +111 -0
  12. package/dist/agent-ours/runtime.d.ts +53 -0
  13. package/dist/agent-ours/runtime.js +247 -0
  14. package/dist/agent-ours/service.d.ts +23 -0
  15. package/dist/agent-ours/service.js +364 -0
  16. package/dist/agent-ours/state.d.ts +45 -0
  17. package/dist/agent-ours/state.js +95 -0
  18. package/dist/agent-ours/wire.d.ts +14 -0
  19. package/dist/agent-ours/wire.js +55 -0
  20. package/dist/agent-recovery-gate.js +1 -1
  21. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  22. package/dist/application/legacy-supervisor-identity.js +43 -0
  23. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  24. package/dist/application/supervisor-ours-tools.js +121 -0
  25. package/dist/application/task-room-service.d.ts +6 -2
  26. package/dist/application/task-room-service.js +78 -21
  27. package/dist/briefing.js +10 -59
  28. package/dist/build-info.json +5 -4
  29. package/dist/capabilities.d.ts +2 -1
  30. package/dist/capabilities.js +2 -0
  31. package/dist/cli.js +63 -2
  32. package/dist/client-profile.d.ts +1 -0
  33. package/dist/client-profile.js +18 -3
  34. package/dist/config.d.ts +1 -0
  35. package/dist/creation.d.ts +1 -1
  36. package/dist/creation.js +3 -14
  37. package/dist/docs.d.ts +3 -3
  38. package/dist/docs.js +32 -23
  39. package/dist/doctor.d.ts +1 -1
  40. package/dist/doctor.js +29 -10
  41. package/dist/fleet-command-audit.js +3 -2
  42. package/dist/harness/agent-session.d.ts +2 -0
  43. package/dist/harness/claude-code-session.js +4 -2
  44. package/dist/harness/codex-session.js +9 -2
  45. package/dist/harness/codex.d.ts +3 -2
  46. package/dist/harness/codex.js +4 -3
  47. package/dist/harness/hermes-session.js +7 -1
  48. package/dist/owner-channel/channel.d.ts +3 -0
  49. package/dist/owner-channel/channel.js +24 -4
  50. package/dist/owner-channel/commands.js +5 -0
  51. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  52. package/dist/rooms-tasks/archived-absence.js +48 -0
  53. package/dist/rooms-tasks/cli.js +6 -1
  54. package/dist/rooms-tasks/close.d.ts +3 -0
  55. package/dist/rooms-tasks/close.js +95 -17
  56. package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
  57. package/dist/rooms-tasks/cowork-adapter.js +10 -2
  58. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  59. package/dist/rooms-tasks/cowork-http.js +70 -0
  60. package/dist/rooms-tasks/deletion.js +40 -3
  61. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  62. package/dist/rooms-tasks/live-readiness.js +75 -0
  63. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  64. package/dist/rooms-tasks/member-startup.js +2 -0
  65. package/dist/rooms-tasks/provision.d.ts +11 -0
  66. package/dist/rooms-tasks/provision.js +138 -26
  67. package/dist/rooms-tasks/room-state.js +13 -0
  68. package/dist/rooms-tasks/task-state.d.ts +13 -3
  69. package/dist/rooms-tasks/task-state.js +42 -6
  70. package/dist/rooms-tasks/types.d.ts +12 -0
  71. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  72. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  73. package/dist/rooms-tasks/workspace.d.ts +26 -0
  74. package/dist/rooms-tasks/workspace.js +195 -0
  75. package/dist/runner.d.ts +14 -0
  76. package/dist/runner.js +527 -495
  77. package/dist/session/codex-app-server-transport.js +1 -1
  78. package/dist/spawn.js +19 -16
  79. package/dist/startup-readiness.d.ts +13 -0
  80. package/dist/startup-readiness.js +38 -0
  81. package/dist/supervisor/systemd.js +5 -0
  82. package/dist/temp-supervisor-recovery.d.ts +3 -0
  83. package/dist/temp-supervisor-recovery.js +46 -0
  84. package/dist/watchdog/briefing.js +3 -16
  85. package/dist/watchdog/run.js +19 -5
  86. package/dist/web/server.d.ts +2 -0
  87. package/dist/web/server.js +18 -1
  88. package/docs/task-workspaces.md +98 -0
  89. package/package.json +6 -3
  90. package/presets/fleet/roles/Coordinator.yaml +22 -0
@@ -0,0 +1,159 @@
1
+ export interface SupervisorToolRequest {
2
+ tool: string;
3
+ arguments?: Record<string, unknown>;
4
+ }
5
+ /** CLI and REST use the same fixed-identity MCP server as the agent harness. */
6
+ export declare class SupervisorOursTools {
7
+ private withClient;
8
+ list(role: string): Promise<{
9
+ tools: {
10
+ inputSchema: {
11
+ [x: string]: unknown;
12
+ type: "object";
13
+ properties?: Record<string, object> | undefined;
14
+ required?: string[] | undefined;
15
+ };
16
+ name: string;
17
+ description?: string | undefined;
18
+ outputSchema?: {
19
+ [x: string]: unknown;
20
+ type: "object";
21
+ properties?: Record<string, object> | undefined;
22
+ required?: string[] | undefined;
23
+ } | undefined;
24
+ annotations?: {
25
+ title?: string | undefined;
26
+ readOnlyHint?: boolean | undefined;
27
+ destructiveHint?: boolean | undefined;
28
+ idempotentHint?: boolean | undefined;
29
+ openWorldHint?: boolean | undefined;
30
+ } | undefined;
31
+ execution?: {
32
+ taskSupport?: "optional" | "required" | "forbidden" | undefined;
33
+ } | undefined;
34
+ _meta?: Record<string, unknown> | undefined;
35
+ icons?: {
36
+ src: string;
37
+ mimeType?: string | undefined;
38
+ sizes?: string[] | undefined;
39
+ theme?: "light" | "dark" | undefined;
40
+ }[] | undefined;
41
+ title?: string | undefined;
42
+ }[];
43
+ _meta?: {
44
+ [x: string]: unknown;
45
+ progressToken?: string | number | undefined;
46
+ "io.modelcontextprotocol/related-task"?: {
47
+ taskId: string;
48
+ } | undefined;
49
+ } | undefined;
50
+ nextCursor?: string | undefined;
51
+ agent: string;
52
+ identity: {
53
+ name: string;
54
+ cid: string;
55
+ generation: number;
56
+ };
57
+ }>;
58
+ call(role: string, request: SupervisorToolRequest): Promise<{
59
+ agent: string;
60
+ identity: {
61
+ name: string;
62
+ cid: string;
63
+ generation: number;
64
+ };
65
+ result: {
66
+ [x: string]: unknown;
67
+ content: ({
68
+ type: "text";
69
+ text: string;
70
+ annotations?: {
71
+ audience?: ("user" | "assistant")[] | undefined;
72
+ priority?: number | undefined;
73
+ lastModified?: string | undefined;
74
+ } | undefined;
75
+ _meta?: Record<string, unknown> | undefined;
76
+ } | {
77
+ type: "image";
78
+ data: string;
79
+ mimeType: string;
80
+ annotations?: {
81
+ audience?: ("user" | "assistant")[] | undefined;
82
+ priority?: number | undefined;
83
+ lastModified?: string | undefined;
84
+ } | undefined;
85
+ _meta?: Record<string, unknown> | undefined;
86
+ } | {
87
+ type: "audio";
88
+ data: string;
89
+ mimeType: string;
90
+ annotations?: {
91
+ audience?: ("user" | "assistant")[] | undefined;
92
+ priority?: number | undefined;
93
+ lastModified?: string | undefined;
94
+ } | undefined;
95
+ _meta?: Record<string, unknown> | undefined;
96
+ } | {
97
+ type: "resource";
98
+ resource: {
99
+ uri: string;
100
+ text: string;
101
+ mimeType?: string | undefined;
102
+ _meta?: Record<string, unknown> | undefined;
103
+ } | {
104
+ uri: string;
105
+ blob: string;
106
+ mimeType?: string | undefined;
107
+ _meta?: Record<string, unknown> | undefined;
108
+ };
109
+ annotations?: {
110
+ audience?: ("user" | "assistant")[] | undefined;
111
+ priority?: number | undefined;
112
+ lastModified?: string | undefined;
113
+ } | undefined;
114
+ _meta?: Record<string, unknown> | undefined;
115
+ } | {
116
+ uri: string;
117
+ name: string;
118
+ type: "resource_link";
119
+ description?: string | undefined;
120
+ mimeType?: string | undefined;
121
+ size?: number | undefined;
122
+ annotations?: {
123
+ audience?: ("user" | "assistant")[] | undefined;
124
+ priority?: number | undefined;
125
+ lastModified?: string | undefined;
126
+ } | undefined;
127
+ _meta?: {
128
+ [x: string]: unknown;
129
+ } | undefined;
130
+ icons?: {
131
+ src: string;
132
+ mimeType?: string | undefined;
133
+ sizes?: string[] | undefined;
134
+ theme?: "light" | "dark" | undefined;
135
+ }[] | undefined;
136
+ title?: string | undefined;
137
+ })[];
138
+ _meta?: {
139
+ [x: string]: unknown;
140
+ progressToken?: string | number | undefined;
141
+ "io.modelcontextprotocol/related-task"?: {
142
+ taskId: string;
143
+ } | undefined;
144
+ } | undefined;
145
+ structuredContent?: Record<string, unknown> | undefined;
146
+ isError?: boolean | undefined;
147
+ } | {
148
+ [x: string]: unknown;
149
+ toolResult: unknown;
150
+ _meta?: {
151
+ [x: string]: unknown;
152
+ progressToken?: string | number | undefined;
153
+ "io.modelcontextprotocol/related-task"?: {
154
+ taskId: string;
155
+ } | undefined;
156
+ } | undefined;
157
+ };
158
+ }>;
159
+ }
@@ -0,0 +1,121 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
+ import { agentDir } from '../paths.js';
8
+ import { ROLE_NAME_RE } from '../config.js';
9
+ import { legacySupervisorIdentity } from './legacy-supervisor-identity.js';
10
+ import { FleetError } from './errors.js';
11
+ /** CLI and REST use the same fixed-identity MCP server as the agent harness. */
12
+ export class SupervisorOursTools {
13
+ async withClient(role, work) {
14
+ if (!ROLE_NAME_RE.test(role))
15
+ throw new FleetError('invalid_request', 'invalid agent name');
16
+ const findCandidates = () => [false, true].map(temporary => ({ temporary, dir: agentDir(role, temporary) }))
17
+ .map(row => ({ ...row, path: join(row.dir, '.ours-bridge', 'descriptor.json') }))
18
+ .filter(row => existsSync(row.path));
19
+ const candidates = findCandidates();
20
+ if (candidates.length !== 1)
21
+ throw new FleetError('capability_unavailable', 'agent supervisor endpoint is missing or ambiguous');
22
+ const selected = candidates[0];
23
+ let descriptor;
24
+ let identityName;
25
+ try {
26
+ descriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
27
+ identityName = readFileSync(join(selected.dir, '.identity'), 'utf8').trim();
28
+ if (!descriptor || typeof descriptor !== 'object')
29
+ throw new Error();
30
+ }
31
+ catch {
32
+ throw new FleetError('capability_unavailable', 'supervisor identity metadata cannot be read');
33
+ }
34
+ const legacy = ['role', 'identity', 'cid'].every(key => descriptor[key] === undefined);
35
+ const identity = legacy
36
+ ? legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation)
37
+ : { name: identityName, cid: descriptor.cid, generation: descriptor.generation };
38
+ if (legacy && descriptor.socket !== join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`))
39
+ throw new FleetError('capability_unavailable', 'legacy supervisor socket does not match the selected agent');
40
+ if ((!legacy && (descriptor.role !== role || descriptor.identity !== identityName))
41
+ || !/^[a-f0-9]{64}$/i.test(identity.cid ?? '') || !Number.isSafeInteger(descriptor.generation) || descriptor.generation < 1)
42
+ throw new FleetError('capability_unavailable', 'supervisor identity metadata is unavailable or mismatched');
43
+ const verifySelection = () => {
44
+ try {
45
+ const current = findCandidates();
46
+ const freshDescriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
47
+ if (current.length !== 1 || current[0].path !== selected.path
48
+ || readFileSync(join(selected.dir, '.identity'), 'utf8').trim() !== identityName
49
+ || ['role', 'identity', 'cid', 'socket', 'capability', 'generation']
50
+ .some(key => freshDescriptor[key] !== descriptor[key]))
51
+ throw new Error();
52
+ }
53
+ catch {
54
+ throw new FleetError('capability_unavailable', 'selected supervisor assignment changed');
55
+ }
56
+ };
57
+ const client = new Client({ name: 'ours-fleet-supervisor-tools', version: '1' });
58
+ const transport = new StdioClientTransport({
59
+ command: process.execPath,
60
+ args: [fileURLToPath(new URL('../agent-ours/bridge.js', import.meta.url))],
61
+ env: { ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined)),
62
+ FLEET_OURS_BRIDGE_DESCRIPTOR: selected.path,
63
+ FLEET_OURS_BRIDGE_EXPECTED: JSON.stringify({ role: descriptor.role, identity: descriptor.identity, cid: descriptor.cid, generation: descriptor.generation,
64
+ transportDigest: createHash('sha256').update(JSON.stringify([descriptor.socket, descriptor.capability, descriptor.generation])).digest('hex') }) },
65
+ stderr: 'pipe',
66
+ });
67
+ try {
68
+ await client.connect(transport);
69
+ const verify = async () => {
70
+ if (!legacy) {
71
+ verifySelection();
72
+ return;
73
+ }
74
+ const actual = await client.callTool({ name: 'current_identity', arguments: {} });
75
+ const blocks = actual.content;
76
+ const lines = blocks?.length === 1 && blocks[0].type === 'text' ? blocks[0].text?.split('\n') : undefined;
77
+ const prefix = `Bound to "${identity.name}" (${identity.cid})`;
78
+ const suffix = lines?.[0].startsWith(prefix) ? lines[0].slice(prefix.length) : undefined;
79
+ const roleSuffix = typeof suffix === 'string' && /^ — role "[^"\r\n]+" under root "[^"\r\n]+"\.$/.test(suffix);
80
+ const temporaryLine = 'TEMPORARY identity owned by the Fleet supervisor for this logical agent instance. Bridge or harness disconnect retains it; terminal supervisor release deletes local state with best-effort peer notices.';
81
+ if (actual.isError || !(roleSuffix || (selected.temporary && suffix === '.'))
82
+ || (selected.temporary ? lines?.[1] !== temporaryLine : lines?.[1]?.startsWith('TEMPORARY')))
83
+ throw new FleetError('capability_unavailable', 'connected supervisor identity does not match legacy ownership proof');
84
+ const fresh = legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation);
85
+ if (!('proof' in identity) || fresh.proof !== identity.proof)
86
+ throw new FleetError('capability_unavailable', 'legacy supervisor identity changed');
87
+ verifySelection();
88
+ };
89
+ await verify();
90
+ return await work(client, { name: identity.name, cid: identity.cid, generation: identity.generation }, verify);
91
+ }
92
+ catch (error) {
93
+ if (error instanceof FleetError)
94
+ throw error;
95
+ // Do not leak arguments, invites, capabilities, or raw transport errors.
96
+ throw new FleetError('control_unavailable', 'supervisor tool request failed; its outcome may be unknown, do not retry a mutation automatically', { retryable: false });
97
+ }
98
+ finally {
99
+ await client.close().catch(() => { });
100
+ await transport.close().catch(() => { });
101
+ }
102
+ }
103
+ list(role) {
104
+ return this.withClient(role, async (client, identity) => ({
105
+ agent: role, identity, ...(await client.listTools()),
106
+ }));
107
+ }
108
+ call(role, request) {
109
+ if (!request || typeof request.tool !== 'string'
110
+ || (request.arguments !== undefined && (!request.arguments || typeof request.arguments !== 'object' || Array.isArray(request.arguments))))
111
+ throw new FleetError('invalid_request', 'tool and object arguments are required');
112
+ return this.withClient(role, async (client, identity, verify) => {
113
+ const { tools } = await client.listTools();
114
+ if (!tools.some(tool => tool.name === request.tool))
115
+ throw new FleetError('forbidden', 'tool is not exposed by the fixed-identity supervisor');
116
+ await verify();
117
+ const result = await client.callTool({ name: request.tool, arguments: request.arguments ?? {} });
118
+ return { agent: role, identity, result };
119
+ });
120
+ }
121
+ }
@@ -1,3 +1,4 @@
1
+ import { type LiveReadinessDeps } from '../rooms-tasks/live-readiness.js';
1
2
  import { type FleetConfig } from '../config.js';
2
3
  import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
3
4
  import { provisionMembers } from '../rooms-tasks/provision.js';
@@ -57,9 +58,9 @@ export interface TaskProvisioningOutcome {
57
58
  /** Repairable terminal observation derived entirely from durable Task/Room state. */
58
59
  export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
59
60
  export declare class TaskRoomApplicationError extends Error {
60
- readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
61
+ readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready';
61
62
  readonly fields: Readonly<Record<string, string>>;
62
- constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found', message: string, fields?: Readonly<Record<string, string>>);
63
+ constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready', message: string, fields?: Readonly<Record<string, string>>);
63
64
  }
64
65
  export interface CreateTaskRequest {
65
66
  actor: TaskRoomActor;
@@ -87,6 +88,7 @@ export interface CreateRoomRequest {
87
88
  }
88
89
  export declare function resolveRoomLaunchPolicy(template: TemplateSnapshot | undefined, override: boolean | undefined): RoomLaunchPolicy;
89
90
  export interface TaskRoomServiceDeps {
91
+ liveReadiness?: LiveReadinessDeps;
90
92
  loadConfiguration?(path?: string): FleetConfig;
91
93
  cowork?(config: FleetConfig): CoworkAdapter;
92
94
  binPath?(): string;
@@ -151,6 +153,7 @@ export declare class TaskRoomApplicationService {
151
153
  task: TaskRecord;
152
154
  orchestration: RoomOrchestrationRecord | undefined;
153
155
  };
156
+ /** Durable provisioning progress only; use active task start/work for a live readiness check. */
154
157
  taskProvisioningOutcome(taskId: string): TaskProvisioningOutcome;
155
158
  awaitTaskProvisioning(input: {
156
159
  actor: TaskRoomActor;
@@ -300,6 +303,7 @@ export declare class TaskRoomApplicationService {
300
303
  taskId: string;
301
304
  outcome?: TaskOutcome;
302
305
  }): Promise<TaskSettlementPlan>;
306
+ private assertTaskLiveReadiness;
303
307
  ensureTaskWork(input: {
304
308
  actor: TaskRoomActor;
305
309
  taskId: string;
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
2
3
  import { ConfigError, loadConfig } from '../config.js';
3
4
  import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
4
5
  import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
@@ -136,7 +137,7 @@ export class TaskRoomApplicationService {
136
137
  }
137
138
  catch (error) {
138
139
  if (error instanceof CoworkUnavailableError)
139
- persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
140
+ persistBlockTask(task.task_id, 'Cowork management is unavailable');
140
141
  // Once the durable Room exists, provisioning errors are resumable
141
142
  // saga state. Return that explicit state so the command can launch a
142
143
  // continuation and report the durable in-progress outcome.
@@ -223,6 +224,7 @@ export class TaskRoomApplicationService {
223
224
  const task = readTask(taskId);
224
225
  return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
225
226
  }
227
+ /** Durable provisioning progress only; use active task start/work for a live readiness check. */
226
228
  taskProvisioningOutcome(taskId) {
227
229
  const task = readTask(taskId);
228
230
  const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
@@ -234,17 +236,23 @@ export class TaskRoomApplicationService {
234
236
  const ready = task.state === 'active' && room?.state === 'active'
235
237
  && active === expected && launched === expected;
236
238
  const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
237
- const nextAction = room?.provisioning_detail === 'waiting_owner_authorization'
238
- ? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
239
- : room?.provisioning_detail === 'waiting_owner_invite'
240
- ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
241
- : room?.provisioning_detail === 'owner_cid_mismatch'
242
- ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
243
- : room?.provisioning_detail === 'waiting_cowork'
244
- ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
245
- : failed
246
- ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
247
- : undefined;
239
+ const nextAction = task.terminal_intent
240
+ ? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
241
+ : room?.state === 'closing' || room?.state === 'closed'
242
+ ? `Complete room cleanup; do not restart provisioning.`
243
+ : room?.provisioning_detail === 'member_failed'
244
+ ? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
245
+ : room?.provisioning_detail === 'waiting_owner_authorization'
246
+ ? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
247
+ : room?.provisioning_detail === 'waiting_owner_invite'
248
+ ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
249
+ : room?.provisioning_detail === 'owner_cid_mismatch'
250
+ ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
251
+ : room?.provisioning_detail === 'waiting_cowork'
252
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
253
+ : failed
254
+ ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
255
+ : undefined;
248
256
  return {
249
257
  kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
250
258
  launch: {
@@ -265,6 +273,10 @@ export class TaskRoomApplicationService {
265
273
  const deadline = now() + (input.waitMs ?? 60_000);
266
274
  for (;;) {
267
275
  const outcome = this.taskProvisioningOutcome(input.taskId);
276
+ if (outcome.kind === 'ready') {
277
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
278
+ await this.assertTaskLiveReadiness(outcome.task, outcome.room, cfg);
279
+ }
268
280
  if (outcome.kind !== 'in_progress' || now() >= deadline) {
269
281
  recordTaskProvisioningOutcome(outcome);
270
282
  return outcome;
@@ -410,7 +422,9 @@ export class TaskRoomApplicationService {
410
422
  let task = readTask(input.taskId);
411
423
  let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
412
424
  const issues = [];
413
- if (task.state !== 'provisioning')
425
+ if (task.state !== 'provisioning' || task.terminal_intent
426
+ || room?.state === 'closing' || room?.state === 'closed'
427
+ || room?.provisioning_detail === 'member_failed')
414
428
  return {
415
429
  kind: 'no_op', task, room, issues,
416
430
  };
@@ -649,6 +663,14 @@ export class TaskRoomApplicationService {
649
663
  (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
650
664
  return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
651
665
  }
666
+ async assertTaskLiveReadiness(task, room, cfg) {
667
+ const cowork = this.deps.cowork ? this.deps.cowork(cfg)
668
+ : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config });
669
+ const issue = await taskLiveReadiness(task, room, cowork, this.deps.liveReadiness);
670
+ if (issue)
671
+ throw new TaskRoomApplicationError('task_not_ready', `Task readiness is ${issue.state}: ${issue.reason}. Ask Fleet Coordinator to inspect the room and member status and choose supported recovery; do not blindly relaunch or reuse invites.`, { task: task.task_id, room: task.room_id ?? 'missing', readiness: issue.state,
672
+ reason: issue.reason, ...(issue.member ? { member: issue.member } : {}) });
673
+ }
652
674
  async ensureTaskWork(input) {
653
675
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
654
676
  let task = readTask(input.taskId);
@@ -673,8 +695,10 @@ export class TaskRoomApplicationService {
673
695
  const pinned = durableTemplate;
674
696
  if (!pinned)
675
697
  throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
676
- if (durableRequestMatches)
698
+ if (durableRequestMatches) {
699
+ await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
677
700
  return { task, status: 'already_active' };
701
+ }
678
702
  const templateName = input.template ?? pinned.name;
679
703
  const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
680
704
  if (!definition)
@@ -687,6 +711,7 @@ export class TaskRoomApplicationService {
687
711
  provisioned: `${pinned.name}@${pinned.version}`
688
712
  });
689
713
  }
714
+ await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
690
715
  return { task, status: 'already_active' };
691
716
  }
692
717
  let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
@@ -784,7 +809,7 @@ export class TaskRoomApplicationService {
784
809
  }
785
810
  catch (error) {
786
811
  if (error instanceof CoworkUnavailableError)
787
- persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
812
+ persistBlockTask(task.task_id, 'Cowork management is unavailable');
788
813
  const current = readTask(task.task_id);
789
814
  if (!current.room_id || !getRoomRecord(current.room_id))
790
815
  throw error;
@@ -802,7 +827,7 @@ export class TaskRoomApplicationService {
802
827
  }
803
828
  catch (error) {
804
829
  if (error instanceof CoworkUnavailableError)
805
- persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
830
+ persistBlockTask(task.task_id, 'Cowork management is unavailable');
806
831
  task = readTask(task.task_id);
807
832
  return { task, status: 'in_progress' };
808
833
  }
@@ -820,12 +845,16 @@ export class TaskRoomApplicationService {
820
845
  }
821
846
  catch (error) {
822
847
  if (error instanceof CoworkUnavailableError)
823
- persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
848
+ persistBlockTask(task.task_id, 'Cowork management is unavailable');
824
849
  task = readTask(task.task_id);
825
850
  }
826
851
  }
827
852
  const resultingRoom = getRoomRecord(task.room_id);
828
- return { task, status: task.state === 'active' && resultingRoom?.state === 'active' ? 'ready' : 'in_progress' };
853
+ if (task.state === 'active' && resultingRoom?.state === 'active') {
854
+ await this.assertTaskLiveReadiness(task, resultingRoom, cfg);
855
+ return { task, status: 'ready' };
856
+ }
857
+ return { task, status: 'in_progress' };
829
858
  }
830
859
  createTemplate(cfg, requested, noRoom) {
831
860
  if (noRoom)
@@ -864,6 +893,7 @@ export class TaskRoomApplicationService {
864
893
  const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
865
894
  let launchTemplate;
866
895
  let room;
896
+ let reusedRoom = false;
867
897
  try {
868
898
  launchTemplate = template ? (template.launch_snapshot_hash ? template
869
899
  : sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
@@ -877,19 +907,45 @@ export class TaskRoomApplicationService {
877
907
  const fresh = readTask(task.task_id);
878
908
  if (fresh.deletion?.status === 'pending')
879
909
  throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
910
+ // A concurrent caller or a restart after local Room publication must
911
+ // reuse the task's one durable room, never create a second workspace/room.
912
+ const candidates = listRoomRecords().filter(record => record.task_id === task.task_id);
913
+ if (candidates.length > 1)
914
+ throw new Error('ambiguous task room ownership');
915
+ const existing = fresh.room_id ? getRoomRecord(fresh.room_id) : candidates[0];
916
+ if (fresh.room_id && !existing)
917
+ throw new Error('recorded task room is missing; refusing duplicate creation');
918
+ if (existing) {
919
+ reusedRoom = true;
920
+ // Heal a crash between durable room publication and task linkage.
921
+ // Return a resumable phase, leaving owner admission/member launch to
922
+ // the existing continuation rather than racing the original creator.
923
+ const resumable = ['persist_intent', 'create_room'].includes(existing.saga.phase)
924
+ ? advanceSaga(existing.room_id, attachOwner ? 'attach_owner' : 'create_members', attachOwner ? 2 : 3)
925
+ : existing;
926
+ onCreated(resumable);
927
+ return resumable;
928
+ }
880
929
  }
930
+ const requiredRoles = new Map();
931
+ if (attachOwner)
932
+ requiredRoles.set(rooms.owner.role, 1);
933
+ for (const member of launchTemplate?.members ?? [])
934
+ requiredRoles.set(member.role, (requiredRoles.get(member.role) ?? 0) + member.count);
881
935
  const created = await cowork.createRoom({
882
936
  room_name: roomName, goal: task.goal?.trim() || task.title,
883
937
  briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
884
938
  quiet_membership: launchTemplate?.room?.quiet_membership,
885
939
  anonymous: policy.anonymous,
940
+ activation_requirements: [...requiredRoles].map(([role, count]) => ({ role, count })),
886
941
  });
887
942
  const record = createRoomRecord({
888
943
  room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
889
944
  task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
890
945
  });
891
- onCreated(record);
892
- return record;
946
+ const resumable = advanceSaga(record.room_id, attachOwner ? 'attach_owner' : 'create_members', attachOwner ? 2 : 3);
947
+ onCreated(resumable);
948
+ return resumable;
893
949
  };
894
950
  room = task.task_id
895
951
  ? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
@@ -902,7 +958,8 @@ export class TaskRoomApplicationService {
902
958
  throw error;
903
959
  }
904
960
  unlockSnapshot?.();
905
- room = advanceSaga(room.room_id, 'create_room', 1);
961
+ if (reusedRoom)
962
+ return room;
906
963
  if (attachOwner) {
907
964
  room = advanceSaga(room.room_id, 'attach_owner', 2);
908
965
  await this.setOwnerRoomCommands(cowork, room.room_id, rooms.owner.role);