@ours.network/fleet 1.0.4 → 1.0.5

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.
@@ -24,7 +24,7 @@ export interface TaskMemberRole {
24
24
  cowork_role: string;
25
25
  }
26
26
  export interface TaskOrigin {
27
- type: 'cli' | 'owner_channel';
27
+ type: 'cli' | 'owner_channel' | 'web';
28
28
  owner_cid?: string;
29
29
  }
30
30
  export interface TaskOutcome {
@@ -46,6 +46,10 @@ export interface TaskTerminalIntent {
46
46
  }
47
47
  export interface TaskRecord {
48
48
  task_id: string;
49
+ /** Stable organizational list identifier. Missing legacy values mean `default`. */
50
+ list_id: string;
51
+ /** Derived presentation field; repositories must never persist it. */
52
+ list_name: string;
49
53
  title: string;
50
54
  brief?: string;
51
55
  brief_file?: string;
@@ -64,6 +68,12 @@ export interface TaskRecord {
64
68
  outcome?: TaskOutcome;
65
69
  terminal_intent?: TaskTerminalIntent;
66
70
  }
71
+ export interface TaskListRecord {
72
+ list_id: string;
73
+ name: string;
74
+ built_in: boolean;
75
+ created_at: string;
76
+ }
67
77
  export type RoomOrchestrationState = 'provisioning' | 'active' | 'closing' | 'closed';
68
78
  export type SagaPhase = 'persist_intent' | 'create_room' | 'attach_owner' | 'create_members' | 'join_role_groups' | 'wait_seats' | 'launch_work' | 'activate' | 'completed' | 'failed';
69
79
  export interface SagaCursor {
@@ -31,6 +31,7 @@ import { startWebControlServer } from './control.js';
31
31
  import { WebAccessStore, validatePublicOrigin } from './access.js';
32
32
  import { buildWatchdogFindings, cachedWatchdogFindingsProvider, WatchdogQueryService } from '../watchdog/query.js';
33
33
  import { latestReport } from '../watchdog/store.js';
34
+ import { TaskRoomApplicationService } from '../application/task-room-service.js';
34
35
  const CONFIG_CACHE_TTL_MS = 5_000;
35
36
  /**
36
37
  * loadConfig re-parses YAML from disk on every call; the watchdog list/reports
@@ -159,6 +160,7 @@ export async function startWebConsole(options) {
159
160
  try {
160
161
  server = await buildWebServer({
161
162
  query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration,
163
+ taskRooms: new TaskRoomApplicationService(options.configPath),
162
164
  topology: readTopology, topologyDrafts, topologyPromote,
163
165
  terminalUpgrade: terminalAvailable
164
166
  ? async (socket, _request, roleId, _ticket, hello) => terminals.connect(socket, roleId, hello)
@@ -15,6 +15,7 @@ import type { MergedTopology } from './topology-model.js';
15
15
  import type { TopologyDraftStore } from './topology-draft-store.js';
16
16
  import type { TopologyPromoteService } from './topology-promote.js';
17
17
  import type { RoleRemovalService } from '../application/role-removal-service.js';
18
+ import type { TaskRoomApplicationService } from '../application/task-room-service.js';
18
19
  export interface WebServices {
19
20
  query: FleetQueryService;
20
21
  repository: RoleRepository;
@@ -30,6 +31,7 @@ export interface WebServices {
30
31
  topologyDrafts?: TopologyDraftStore;
31
32
  topologyPromote?: TopologyPromoteService;
32
33
  removal?: RoleRemovalService;
34
+ taskRooms?: TaskRoomApplicationService;
33
35
  terminalUpgrade?: (socket: WebSocket, request: FastifyRequest, roleId: string, ticket: string, hello: Record<string, unknown>) => Promise<void>;
34
36
  }
35
37
  export interface WebServer {
@@ -11,8 +11,10 @@ import { AuditSink } from './audit.js';
11
11
  import { WebAuth } from './auth.js';
12
12
  import { FleetEventBus } from './events.js';
13
13
  import { ROLE_NAME_RE } from '../config.js';
14
+ import { TaskListError } from '../rooms-tasks/task-lists.js';
15
+ import { TaskStateError } from '../rooms-tasks/task-state.js';
14
16
  const statusFor = (code) => ({
15
- role_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
17
+ role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
16
18
  idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
17
19
  invalid_request: 400, capability_unavailable: 409, prerequisite_unavailable: 503,
18
20
  }[code] ?? 500);
@@ -128,6 +130,89 @@ export async function buildWebServer(services, boundary, options = {}) {
128
130
  auth.authenticate(request);
129
131
  return { roles: await services.query.list() };
130
132
  });
133
+ const taskApi = async (fn) => {
134
+ try {
135
+ return await fn();
136
+ }
137
+ catch (error) {
138
+ if (error instanceof TaskListError) {
139
+ const code = error.code === 'list_not_found' ? 'resource_not_found'
140
+ : ['duplicate_name', 'destination_required', 'same_destination', 'default_immutable'].includes(error.code)
141
+ ? 'conflict' : 'invalid_request';
142
+ throw new FleetError(code, error.message);
143
+ }
144
+ if (error instanceof TaskStateError) {
145
+ throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'conflict', error.message);
146
+ }
147
+ throw error;
148
+ }
149
+ };
150
+ const requireTaskRooms = () => {
151
+ if (!services.taskRooms)
152
+ throw new FleetError('capability_unavailable', 'task operations are unavailable');
153
+ return services.taskRooms;
154
+ };
155
+ app.get('/api/v1/task-lists', async (request) => {
156
+ auth.authenticate(request);
157
+ return { lists: requireTaskRooms().listTaskLists() };
158
+ });
159
+ app.post('/api/v1/task-lists', async (request, reply) => {
160
+ auth.authenticate(request, true);
161
+ const name = String(request.body?.name ?? '');
162
+ const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name }));
163
+ reply.code(201);
164
+ return { list };
165
+ });
166
+ app.patch('/api/v1/task-lists/:name', async (request) => {
167
+ auth.authenticate(request, true);
168
+ const name = request.params.name;
169
+ const newName = String(request.body?.name ?? '');
170
+ return { list: await taskApi(() => requireTaskRooms().renameTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, newName })) };
171
+ });
172
+ app.delete('/api/v1/task-lists/:name', async (request) => {
173
+ auth.authenticate(request, true);
174
+ const name = request.params.name;
175
+ const destination = request.query.destination;
176
+ return taskApi(() => requireTaskRooms().deleteTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, destination }));
177
+ });
178
+ app.get('/api/v1/tasks', async (request) => {
179
+ auth.authenticate(request);
180
+ const query = request.query;
181
+ const states = ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'];
182
+ if (query.state && query.state !== 'all' && !states.includes(query.state))
183
+ throw new FleetError('invalid_request', 'invalid task state filter');
184
+ if (query.groupByList !== undefined && !['true', 'false'].includes(query.groupByList))
185
+ throw new FleetError('invalid_request', 'groupByList must be true or false');
186
+ const state = query.state && query.state !== 'all' ? query.state : undefined;
187
+ const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}) };
188
+ return taskApi(() => query.groupByList === 'true'
189
+ ? { groups: requireTaskRooms().groupedTasks(filter) }
190
+ : { tasks: requireTaskRooms().listTasks(filter) });
191
+ });
192
+ app.post('/api/v1/tasks', async (request, reply) => {
193
+ auth.authenticate(request, true);
194
+ const body = request.body;
195
+ if (typeof body?.title !== 'string' || !body.title)
196
+ throw new FleetError('invalid_request', 'title is required');
197
+ const task = await taskApi(() => requireTaskRooms().createTask({
198
+ actor: { kind: 'local_control', surface: 'web' }, title: body.title,
199
+ brief: typeof body.brief === 'string' ? body.brief : undefined,
200
+ template: typeof body.template === 'string' ? body.template : undefined,
201
+ backlog: body.backlog === true, noRoom: body.noRoom === true,
202
+ list: typeof body.list === 'string' ? body.list : undefined,
203
+ idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
204
+ ? request.headers['idempotency-key'] : undefined,
205
+ origin: { type: 'web' },
206
+ }));
207
+ reply.code(201);
208
+ return { task };
209
+ });
210
+ app.patch('/api/v1/tasks/:id/list', async (request) => {
211
+ auth.authenticate(request, true);
212
+ const taskId = request.params.id;
213
+ const list = String(request.body?.list ?? '');
214
+ return { task: await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list })) };
215
+ });
131
216
  app.get('/api/v1/configuration', async (request) => {
132
217
  auth.authenticate(request);
133
218
  if (!services.configuration)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",