@atolis-hq/wake 0.4.48 → 0.4.50

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 (76) hide show
  1. package/README.md +3 -0
  2. package/dist/src/activities/agent/agent-activity.js +9 -3
  3. package/dist/src/activities/contracts/activity.d.ts +14 -0
  4. package/dist/src/artifacts/application/artifact-projection.d.ts +3 -0
  5. package/dist/src/artifacts/application/artifact-projection.js +15 -0
  6. package/dist/src/artifacts/application/artifact-repository.d.ts +13 -0
  7. package/dist/src/artifacts/application/artifact-repository.js +19 -0
  8. package/dist/src/artifacts/application/artifact-service.d.ts +40 -0
  9. package/dist/src/artifacts/application/artifact-service.js +128 -0
  10. package/dist/src/artifacts/contracts/config.d.ts +6 -0
  11. package/dist/src/artifacts/contracts/config.js +19 -0
  12. package/dist/src/artifacts/contracts/event-factory.d.ts +6 -0
  13. package/dist/src/artifacts/contracts/event-factory.js +10 -0
  14. package/dist/src/artifacts/contracts/events.d.ts +33 -0
  15. package/dist/src/artifacts/contracts/events.js +59 -0
  16. package/dist/src/artifacts/contracts/identifiers.d.ts +5 -0
  17. package/dist/src/artifacts/contracts/identifiers.js +8 -0
  18. package/dist/src/artifacts/contracts/paths.d.ts +5 -0
  19. package/dist/src/artifacts/contracts/paths.js +9 -0
  20. package/dist/src/artifacts/contracts/streams.d.ts +7 -0
  21. package/dist/src/artifacts/contracts/streams.js +4 -0
  22. package/dist/src/artifacts/contracts/views.d.ts +20 -0
  23. package/dist/src/artifacts/contracts/views.js +1 -0
  24. package/dist/src/artifacts/domain/artifact-work-item.d.ts +4 -0
  25. package/dist/src/artifacts/domain/artifact-work-item.js +42 -0
  26. package/dist/src/artifacts/index.d.ts +14 -0
  27. package/dist/src/artifacts/index.js +14 -0
  28. package/dist/src/artifacts/infrastructure/file-artifact-store.d.ts +22 -0
  29. package/dist/src/artifacts/infrastructure/file-artifact-store.js +51 -0
  30. package/dist/src/artifacts/infrastructure/mcp-server.d.ts +18 -0
  31. package/dist/src/artifacts/infrastructure/mcp-server.js +135 -0
  32. package/dist/src/artifacts/infrastructure/mcp-session-store.d.ts +25 -0
  33. package/dist/src/artifacts/infrastructure/mcp-session-store.js +61 -0
  34. package/dist/src/bootstrap/artifact-mcp.d.ts +23 -0
  35. package/dist/src/bootstrap/artifact-mcp.js +119 -0
  36. package/dist/src/bootstrap/composition-root.d.ts +2 -0
  37. package/dist/src/bootstrap/composition-root.js +19 -0
  38. package/dist/src/bootstrap/config/root-schema.d.ts +6 -0
  39. package/dist/src/bootstrap/config/root-schema.js +5 -0
  40. package/dist/src/bootstrap/paths.d.ts +1 -0
  41. package/dist/src/bootstrap/paths.js +1 -0
  42. package/dist/src/bootstrap/projection-runtime.js +2 -0
  43. package/dist/src/bootstrap/runner-registry.js +22 -4
  44. package/dist/src/bootstrap/surface-api-work-applications.js +39 -1
  45. package/dist/src/bootstrap/surface-cli-applications.js +12 -1
  46. package/dist/src/bootstrap/version.js +1 -1
  47. package/dist/src/execution/application/execution-activity.d.ts +11 -0
  48. package/dist/src/execution/application/execution-activity.js +26 -10
  49. package/dist/src/execution/application/execution-service.js +1 -1
  50. package/dist/src/execution/application/execution-validation.js +17 -3
  51. package/dist/src/execution/contracts/runner.d.ts +10 -0
  52. package/dist/src/execution/contracts/workspace.d.ts +11 -1
  53. package/dist/src/execution/infrastructure/runners/claude.d.ts +1 -0
  54. package/dist/src/execution/infrastructure/runners/claude.js +21 -0
  55. package/dist/src/execution/infrastructure/runners/codex.js +12 -0
  56. package/dist/src/execution/infrastructure/runners/fake.d.ts +1 -0
  57. package/dist/src/execution/infrastructure/runners/fake.js +1 -0
  58. package/dist/src/execution/infrastructure/runners/registry.d.ts +3 -1
  59. package/dist/src/execution/infrastructure/runners/registry.js +5 -3
  60. package/dist/src/execution/infrastructure/workspace/git-workspace.d.ts +10 -5
  61. package/dist/src/execution/infrastructure/workspace/git-workspace.js +270 -38
  62. package/dist/src/surfaces/api/contracts/work.d.ts +10 -0
  63. package/dist/src/surfaces/api/routes/applications.d.ts +5 -0
  64. package/dist/src/surfaces/api/routes/read.js +34 -3
  65. package/dist/src/surfaces/api/routes/work.d.ts +1 -1
  66. package/dist/src/surfaces/api/routes/work.js +1 -0
  67. package/dist/src/surfaces/cli/main.d.ts +8 -0
  68. package/dist/src/surfaces/cli/main.js +31 -0
  69. package/dist/src/surfaces/cli/usage.js +1 -0
  70. package/dist/src/surfaces/index.d.ts +1 -0
  71. package/dist/src/surfaces/index.js +1 -0
  72. package/dist/src/surfaces/mcp/stdio.d.ts +3 -0
  73. package/dist/src/surfaces/mcp/stdio.js +5 -0
  74. package/dist/src/surfaces/web-assets/assets/{index-CuAZYm7r.js → index-CuIiUKuh.js} +2 -2
  75. package/dist/src/surfaces/web-assets/index.html +1 -1
  76. package/package.json +3 -1
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { execFile } from 'node:child_process';
3
- import { access, mkdir, readdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, readdir, readFile, realpath, rename, rm, rmdir, unlink, writeFile, } from 'node:fs/promises';
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { isActiveRunStatus, RunStatus, WorkspaceMode } from '../../contracts/vocabulary.js';
@@ -9,18 +9,21 @@ const exec = promisify(execFile);
9
9
  export class GitWorkspaceProvider {
10
10
  root;
11
11
  resolver;
12
+ locks;
12
13
  git;
13
14
  prepareHook;
14
15
  markerRoot;
15
16
  recoveryFileSystem;
16
- constructor(root, resolver, git = async (arguments_, signal) => {
17
- await exec('git', arguments_, { signal });
17
+ pendingReleases = new Map();
18
+ constructor(root, resolver, locks, git = async (arguments_, signal) => {
19
+ return (await exec('git', arguments_, { signal })).stdout.trim();
18
20
  }, recoveryFileSystem = {
19
21
  remove: async (path) => rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }),
20
22
  canonicalize: realpath,
21
23
  }, prepareHook) {
22
24
  this.root = root;
23
25
  this.resolver = resolver;
26
+ this.locks = locks;
24
27
  this.git = git;
25
28
  this.prepareHook = prepareHook;
26
29
  this.markerRoot = join(this.root, '.wake-workspace-ownership');
@@ -34,59 +37,161 @@ export class GitWorkspaceProvider {
34
37
  const name = `${request.workItemId}-${request.mode}-${slug(locator)}`;
35
38
  const path = resolve(this.root, name);
36
39
  const markerPath = join(this.markerRoot, `${name}.json`);
40
+ const lockPath = join(this.markerRoot, `${name}.lease`);
37
41
  await mkdir(dirname(markerPath), { recursive: true });
42
+ const scope = await recoveryScope(this.root, this.markerRoot);
43
+ if (scope === null)
44
+ throw new Error('Workspace ownership directory is outside the managed root');
45
+ await validateLeasePaths(scope, lockPath);
46
+ const lock = await acquireWorkspaceLock(this.locks, lockPath, signal);
47
+ try {
48
+ const branch = await this.prepareCheckout(request, locator, name, path, markerPath);
49
+ if (this.prepareHook !== undefined)
50
+ await prepareWorkspace(path, this.prepareHook, signal);
51
+ signal.throwIfAborted();
52
+ let released = false;
53
+ return {
54
+ workspaceId: name,
55
+ path,
56
+ mode: request.mode,
57
+ ...(branch === undefined ? {} : { branch }),
58
+ release: async () => {
59
+ if (released)
60
+ return;
61
+ await this.releaseWorkspaceLock(lockPath, lock);
62
+ released = true;
63
+ },
64
+ };
65
+ }
66
+ catch (error) {
67
+ await this.releaseWorkspaceLock(lockPath, lock);
68
+ throw error;
69
+ }
70
+ }
71
+ async releaseWorkspaceLock(path, lock) {
72
+ try {
73
+ await lock.release();
74
+ if (this.pendingReleases.get(path) === lock)
75
+ this.pendingReleases.delete(path);
76
+ }
77
+ catch (error) {
78
+ this.pendingReleases.set(path, lock);
79
+ throw error;
80
+ }
81
+ }
82
+ async prepareCheckout(request, locator, name, path, markerPath) {
83
+ const { signal } = request;
84
+ const previousMarker = await readMarker(markerPath);
85
+ if (await exists(join(this.markerRoot, `${name}.lock`), signal))
86
+ throw new Error('Legacy workspace lock requires recovery before acquisition');
87
+ let initialRevision = previousMarker?.initialRevision;
88
+ let checkoutReady;
38
89
  signal.throwIfAborted();
39
- await writeFile(markerPath, JSON.stringify({
90
+ const writeMarker = () => writeOwnershipMarker(markerPath, {
40
91
  runId: request.runId,
41
92
  workItemId: request.workItemId,
42
93
  repositoryResourceId: request.repositoryResource.resourceId,
43
94
  mode: request.mode,
44
95
  workspaceId: name,
45
96
  path,
46
- }), { encoding: 'utf8', signal });
47
- const existingWorkspace = await exists(join(path, '.git'), signal);
97
+ checkoutReady,
98
+ ...(initialRevision === undefined ? {} : { initialRevision }),
99
+ }, signal);
100
+ const existingWorkspace = await this.reuseCheckout(previousMarker, markerPath, path, request.mode, signal);
101
+ checkoutReady = previousMarker?.checkoutReady ?? existingWorkspace;
102
+ if (!existingWorkspace) {
103
+ initialRevision = undefined;
104
+ checkoutReady = false;
105
+ }
106
+ await writeMarker();
48
107
  if (!existingWorkspace) {
49
108
  await mkdir(dirname(path), { recursive: true });
50
109
  signal.throwIfAborted();
51
110
  await this.git(['clone', locator, path], signal);
52
111
  }
112
+ if (initialRevision === undefined)
113
+ initialRevision = await readCheckoutRevision(path, this.git, signal);
53
114
  const branch = request.mode === WorkspaceMode.Branch ? request.workItemId : undefined;
54
- if (branch !== undefined)
115
+ if (branch !== undefined) {
55
116
  await this.git(['-C', path, 'switch', ...(existingWorkspace ? [] : ['--create']), branch], signal);
56
- if (this.prepareHook !== undefined)
57
- await prepareWorkspace(path, this.prepareHook, signal);
58
- signal.throwIfAborted();
59
- return {
60
- workspaceId: name,
61
- path,
62
- mode: request.mode,
63
- ...(branch === undefined ? {} : { branch }),
64
- release: async () => {
65
- if (request.mode !== WorkspaceMode.ReadOnly)
66
- return;
67
- await rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
68
- await rm(markerPath, { force: true });
69
- },
70
- };
117
+ }
118
+ checkoutReady = true;
119
+ await writeMarker();
120
+ if (branch === undefined) {
121
+ await restoreReadOnlyWorkspace(path, request.checkoutRevision, initialRevision, this.git, signal);
122
+ }
123
+ return branch;
124
+ }
125
+ async reuseCheckout(marker, markerPath, path, mode, signal) {
126
+ if (!(await exists(path, signal)))
127
+ return false;
128
+ const scope = await recoveryScope(this.root, this.markerRoot);
129
+ if (scope === null ||
130
+ marker === null ||
131
+ !(await isOwnedWorkspace(scope, markerPath, marker, this.recoveryFileSystem)) ||
132
+ !(await canRemoveOwnedWorkspace(scope, path)))
133
+ throw new Error('Cannot acquire an unvalidated existing workspace');
134
+ if (marker.checkoutReady === false) {
135
+ await this.recoveryFileSystem.remove(path);
136
+ return false;
137
+ }
138
+ if (!(await exists(join(path, '.git'), signal))) {
139
+ if (mode === WorkspaceMode.Branch)
140
+ throw new Error('Cannot reuse a branch checkout without Git metadata');
141
+ await this.recoveryFileSystem.remove(path);
142
+ return false;
143
+ }
144
+ return this.validateLegacyCheckout(marker, path, mode, signal);
145
+ }
146
+ async validateLegacyCheckout(marker, path, mode, signal) {
147
+ if (marker.checkoutReady !== undefined || mode !== WorkspaceMode.ReadOnly)
148
+ return true;
149
+ try {
150
+ await readCheckoutRevision(path, this.git, signal);
151
+ return true;
152
+ }
153
+ catch {
154
+ signal.throwIfAborted();
155
+ // Legacy read-only clones may predate the durable completion flag.
156
+ await this.recoveryFileSystem.remove(path);
157
+ return false;
158
+ }
71
159
  }
72
160
  async recover(runs, options = {}) {
73
161
  const scope = await recoveryScope(this.root, this.markerRoot);
74
162
  if (scope === null)
75
163
  return emptyRecovery();
76
- const markers = await recoveryMarkerNames(this.markerRoot);
164
+ const releaseFailures = [];
165
+ for (const [path, lock] of this.pendingReleases) {
166
+ if (await options.isPaused?.())
167
+ break;
168
+ try {
169
+ await this.releaseWorkspaceLock(path, lock);
170
+ }
171
+ catch (error) {
172
+ releaseFailures.push({ markerPath: path, path, message: errorMessage(error) });
173
+ }
174
+ }
175
+ const names = await recoveryMarkerNames(this.markerRoot);
176
+ const workspaceIds = new Set(names.flatMap((name) => {
177
+ const match = /^(.*)\.(?:json(?:\.pending)?|lock|lease(?:\.owners)?)$/.exec(name);
178
+ return match !== null && isSafeWorkspaceId(match[1]) ? [match[1]] : [];
179
+ }));
77
180
  let reclaimed = 0;
78
181
  const reclaimedWorkItemIds = [];
79
- const failures = [];
80
- for (const filename of markers) {
182
+ const failures = [...releaseFailures];
183
+ for (const workspaceId of workspaceIds) {
81
184
  if (await options.isPaused?.())
82
185
  break;
83
- const result = await recoverMarker({
84
- filename,
186
+ const result = await recoverWorkspace({
187
+ workspaceId,
85
188
  markerRoot: this.markerRoot,
86
189
  scope,
87
190
  runs,
88
191
  fileSystem: this.recoveryFileSystem,
89
192
  options,
193
+ locks: this.locks,
194
+ releaseLock: (path, lock) => this.releaseWorkspaceLock(path, lock),
90
195
  });
91
196
  reclaimed += result.reclaimed;
92
197
  reclaimedWorkItemIds.push(...(result.reclaimedWorkItemIds ?? []));
@@ -113,13 +218,40 @@ async function recoveryMarkerNames(markerRoot) {
113
218
  return [];
114
219
  }
115
220
  }
116
- async function recoverMarker(input) {
117
- if (!input.filename.endsWith('.json'))
118
- return emptyRecovery();
119
- const markerPath = join(input.markerRoot, input.filename);
120
- const marker = await readMarker(markerPath);
221
+ async function validateLeasePaths(scope, path) {
222
+ for (const target of [
223
+ path,
224
+ `${path}.owners`,
225
+ `${path.slice(0, -'.lease'.length)}.lock`,
226
+ `${path.slice(0, -'.lease'.length)}.json.pending`,
227
+ ]) {
228
+ const canonical = await canonicalPath(target);
229
+ if (canonical !== null && canonical !== join(scope.markerRoot, basename(target)))
230
+ throw new Error('Workspace lease artifacts must not be symbolic links');
231
+ }
232
+ }
233
+ async function recoverWorkspace(input) {
234
+ const markerPath = join(input.markerRoot, `${input.workspaceId}.json`);
235
+ let marker = null;
121
236
  try {
122
- return await reclaimOwnedMarker(input, markerPath, marker);
237
+ const lockPath = join(input.markerRoot, `${input.workspaceId}.lease`);
238
+ await validateLeasePaths(input.scope, lockPath);
239
+ const lock = await input.locks.tryAcquire(lockPath);
240
+ if (!lock.acquired)
241
+ return emptyRecovery();
242
+ try {
243
+ if (!(await recoverLegacyLock(join(input.markerRoot, `${input.workspaceId}.lock`), input.runs)))
244
+ return emptyRecovery();
245
+ await rm(`${markerPath}.pending`, { force: true });
246
+ marker = await readMarker(markerPath);
247
+ if (marker === null ||
248
+ !(await isOwnedWorkspace(input.scope, markerPath, marker, input.fileSystem)))
249
+ return emptyRecovery();
250
+ return await reclaimUnlockedMarker(input, markerPath, marker);
251
+ }
252
+ finally {
253
+ await input.releaseLock(lockPath, lock);
254
+ }
123
255
  }
124
256
  catch (error) {
125
257
  return {
@@ -128,10 +260,35 @@ async function recoverMarker(input) {
128
260
  };
129
261
  }
130
262
  }
131
- async function reclaimOwnedMarker(input, markerPath, marker) {
132
- if (marker === null ||
133
- !(await isOwnedWorkspace(input.scope, markerPath, marker, input.fileSystem)))
134
- return emptyRecovery();
263
+ // Migration only: new acquisitions never create directory locks.
264
+ async function recoverLegacyLock(path, runs) {
265
+ if ((await canonicalPath(path)) === null)
266
+ return true;
267
+ const owner = await readLockOwner(path);
268
+ if (owner === undefined)
269
+ return false;
270
+ if (owner === null && runs.some(protectsWorkspace))
271
+ return false;
272
+ if (owner !== null && runs.some((run) => run.runId === owner && protectsWorkspace(run)))
273
+ return false;
274
+ if (owner !== null)
275
+ await unlink(join(path, owner));
276
+ try {
277
+ await rmdir(path);
278
+ return true;
279
+ }
280
+ catch (error) {
281
+ if (isNotFound(error))
282
+ return true;
283
+ if (isNotEmpty(error))
284
+ return false;
285
+ throw error;
286
+ }
287
+ }
288
+ function protectsWorkspace(run) {
289
+ return isActiveRunStatus(run.status) || run.status === RunStatus.Ambiguous;
290
+ }
291
+ async function reclaimUnlockedMarker(input, markerPath, marker) {
135
292
  if (input.runs.some((run) => (isActiveRunStatus(run.status) || run.status === RunStatus.Ambiguous) &&
136
293
  (run.runId === marker.runId || run.workspace?.path === marker.path)))
137
294
  return emptyRecovery();
@@ -143,6 +300,7 @@ async function reclaimOwnedMarker(input, markerPath, marker) {
143
300
  await input.fileSystem.remove(marker.path);
144
301
  if ((await input.options.onWorkspaceReclaimed?.(marker.workItemId)) === false)
145
302
  return emptyRecovery();
303
+ await rm(`${markerPath}.pending`, { force: true });
146
304
  await rm(markerPath, { force: true });
147
305
  return {
148
306
  reclaimed: 1,
@@ -182,7 +340,12 @@ function isOwnershipMarker(value) {
182
340
  typeof marker.repositoryResourceId === 'string' &&
183
341
  (marker.mode === WorkspaceMode.ReadOnly || marker.mode === WorkspaceMode.Branch) &&
184
342
  typeof marker.workspaceId === 'string' &&
185
- typeof marker.path === 'string');
343
+ typeof marker.path === 'string' &&
344
+ hasValidCheckoutState(marker));
345
+ }
346
+ function hasValidCheckoutState(marker) {
347
+ return ((marker.initialRevision === undefined || typeof marker.initialRevision === 'string') &&
348
+ (marker.checkoutReady === undefined || typeof marker.checkoutReady === 'boolean'));
186
349
  }
187
350
  async function isOwnedWorkspace(scope, markerPath, marker, fileSystem) {
188
351
  if (!isSafeWorkspaceId(marker.workspaceId))
@@ -248,6 +411,75 @@ async function exists(path, signal) {
248
411
  return false;
249
412
  }
250
413
  }
414
+ async function restoreReadOnlyWorkspace(path, revision, initialRevision, git, signal) {
415
+ if (revision !== undefined) {
416
+ await git(['-C', path, 'fetch', 'origin', revision], signal);
417
+ const fetched = await readGitRevision(path, 'FETCH_HEAD^{commit}', git, signal);
418
+ await git(['-C', path, 'checkout', '--detach', '--force', fetched], signal);
419
+ await git(['-C', path, 'reset', '--hard', fetched], signal);
420
+ return;
421
+ }
422
+ await git(['-C', path, 'reset', '--hard', initialRevision], signal);
423
+ }
424
+ async function readCheckoutRevision(path, git, signal) {
425
+ return readGitRevision(path, 'HEAD^{commit}', git, signal);
426
+ }
427
+ async function readGitRevision(path, reference, git, signal) {
428
+ const revision = await git(['-C', path, 'rev-parse', '--verify', reference], signal);
429
+ if (typeof revision !== 'string' || revision.trim() === '')
430
+ throw new Error('Git runner did not attest a valid workspace HEAD');
431
+ return revision.trim();
432
+ }
433
+ async function writeOwnershipMarker(path, marker, signal) {
434
+ const pending = `${path}.pending`;
435
+ try {
436
+ await writeFile(pending, JSON.stringify(marker), { encoding: 'utf8', signal });
437
+ signal.throwIfAborted();
438
+ await rename(pending, path);
439
+ }
440
+ finally {
441
+ await rm(pending, { force: true });
442
+ }
443
+ }
444
+ async function acquireWorkspaceLock(locks, path, signal) {
445
+ while (true) {
446
+ signal.throwIfAborted();
447
+ const lock = await locks.tryAcquire(path);
448
+ if (lock.acquired)
449
+ return lock;
450
+ await waitForLock(signal);
451
+ }
452
+ }
453
+ async function readLockOwner(path) {
454
+ try {
455
+ const owners = await readdir(path);
456
+ if (owners.length === 0)
457
+ return null;
458
+ return owners.length === 1 ? owners[0] : undefined;
459
+ }
460
+ catch (error) {
461
+ if (isNotFound(error))
462
+ return null;
463
+ throw error;
464
+ }
465
+ }
466
+ function isNotEmpty(error) {
467
+ return (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOTEMPTY');
468
+ }
469
+ function waitForLock(signal) {
470
+ return new Promise((resolve, reject) => {
471
+ const timer = setTimeout(done, 25);
472
+ const abort = () => {
473
+ clearTimeout(timer);
474
+ reject(signal.reason);
475
+ };
476
+ function done() {
477
+ signal.removeEventListener('abort', abort);
478
+ resolve();
479
+ }
480
+ signal.addEventListener('abort', abort, { once: true });
481
+ });
482
+ }
251
483
  function slug(value) {
252
484
  return value
253
485
  .toLowerCase()
@@ -35,6 +35,16 @@ export interface WorkDetailResponse {
35
35
  readonly activities: {
36
36
  readonly pullRequest?: PullRequestResponse;
37
37
  };
38
+ readonly artifacts: readonly {
39
+ readonly revisionId: string;
40
+ readonly producer: string;
41
+ readonly path: string;
42
+ readonly occurredAt: string;
43
+ readonly mediaType?: string;
44
+ readonly byteLength?: number;
45
+ readonly digest?: string;
46
+ readonly href: string;
47
+ }[];
38
48
  readonly conversation: {
39
49
  /** Whether this authenticated surface may create control-plane entries. */
40
50
  readonly canCreateEntries: boolean;
@@ -56,6 +56,11 @@ export interface ApiApplications {
56
56
  list(query: CollectionQuery): Promise<ApiCollectionPage<WorkItemResponse>>;
57
57
  detail(key: string): Promise<ApiResourceResult<WorkDetailResponse> | undefined>;
58
58
  transcript?(key: string, groupId: string): Promise<ApiResourceResult<WorkItemTranscriptResponse> | undefined>;
59
+ artifact?(key: string, revisionId: string): Promise<{
60
+ readonly bytes: Uint8Array;
61
+ readonly mediaType: string;
62
+ readonly filename: string;
63
+ } | undefined>;
59
64
  freeze?(key: string, command: ApiCommandRequest): Promise<ApiCommandResult>;
60
65
  unfreeze?(key: string, command: ApiCommandRequest): Promise<ApiCommandResult>;
61
66
  delete?(key: string, command: ApiCommandRequest): Promise<ApiCommandResult>;
@@ -108,9 +108,9 @@ async function collectionResult(applications, pathname, query) {
108
108
  }
109
109
  }
110
110
  async function readDetail(applications, pathname) {
111
- const workTranscript = /^\/api\/v1\/work-items\/([^/]+)\/transcripts\/([^/]+)$/.exec(pathname);
112
- if (workTranscript?.[1] !== undefined && workTranscript[2] !== undefined)
113
- return readWorkTranscript(applications, workTranscript[1], workTranscript[2]);
111
+ const workSubresource = await readWorkSubresource(applications, pathname);
112
+ if (workSubresource !== undefined)
113
+ return workSubresource;
114
114
  const transcript = /^\/api\/v1\/runs\/([^/]+)\/transcript$/.exec(pathname)?.[1];
115
115
  if (transcript !== undefined)
116
116
  return readTranscript(applications, transcript);
@@ -120,6 +120,37 @@ async function readDetail(applications, pathname) {
120
120
  const run = /^\/api\/v1\/runs\/([^/]+)$/.exec(pathname)?.[1];
121
121
  return run === undefined ? undefined : readRun(applications, run);
122
122
  }
123
+ async function readWorkSubresource(applications, pathname) {
124
+ const workTranscript = /^\/api\/v1\/work-items\/([^/]+)\/transcripts\/([^/]+)$/.exec(pathname);
125
+ if (workTranscript?.[1] !== undefined && workTranscript[2] !== undefined)
126
+ return readWorkTranscript(applications, workTranscript[1], workTranscript[2]);
127
+ const artifact = /^\/api\/v1\/work-items\/([^/]+)\/artifacts\/([^/]+)$/.exec(pathname);
128
+ if (artifact?.[1] !== undefined && artifact[2] !== undefined)
129
+ return readWorkArtifact(applications, artifact[1], artifact[2]);
130
+ return undefined;
131
+ }
132
+ async function readWorkArtifact(applications, keySegment, revisionSegment) {
133
+ const key = decodePathSegment(keySegment);
134
+ const revisionId = decodePathSegment(revisionSegment);
135
+ if (key instanceof ApiPathError)
136
+ return invalidPath(key.message);
137
+ if (revisionId instanceof ApiPathError)
138
+ return invalidPath(revisionId.message);
139
+ if (applications.work.artifact === undefined)
140
+ return unavailable('artifact');
141
+ const artifact = await applications.work.artifact(key, revisionId);
142
+ if (artifact === undefined)
143
+ return found(undefined);
144
+ return {
145
+ status: 200,
146
+ body: artifact.bytes,
147
+ contentType: artifact.mediaType,
148
+ headers: {
149
+ 'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(artifact.filename)}`,
150
+ 'content-length': String(artifact.bytes.byteLength),
151
+ },
152
+ };
153
+ }
123
154
  async function readWorkTranscript(applications, keySegment, groupSegment) {
124
155
  const key = decodePathSegment(keySegment);
125
156
  const groupId = decodePathSegment(groupSegment);
@@ -1 +1 @@
1
- export declare const workRoutes: readonly ["/api/v1/work-items", "/api/v1/work-items/:workItemKey", "/api/v1/work-items/:workItemKey/transcripts/:groupId", "/api/v1/work-items/:workItemKey/commands/freeze", "/api/v1/work-items/:workItemKey/commands/unfreeze", "/api/v1/work-items/:workItemKey/commands/delete", "/api/v1/work-items/:workItemKey/commands/retry", "/api/v1/work-items/:workItemKey/commands/extend", "/api/v1/work-items/:workItemKey/commands/message"];
1
+ export declare const workRoutes: readonly ["/api/v1/work-items", "/api/v1/work-items/:workItemKey", "/api/v1/work-items/:workItemKey/transcripts/:groupId", "/api/v1/work-items/:workItemKey/artifacts/:revisionId", "/api/v1/work-items/:workItemKey/commands/freeze", "/api/v1/work-items/:workItemKey/commands/unfreeze", "/api/v1/work-items/:workItemKey/commands/delete", "/api/v1/work-items/:workItemKey/commands/retry", "/api/v1/work-items/:workItemKey/commands/extend", "/api/v1/work-items/:workItemKey/commands/message"];
@@ -2,6 +2,7 @@ export const workRoutes = [
2
2
  '/api/v1/work-items',
3
3
  '/api/v1/work-items/:workItemKey',
4
4
  '/api/v1/work-items/:workItemKey/transcripts/:groupId',
5
+ '/api/v1/work-items/:workItemKey/artifacts/:revisionId',
5
6
  '/api/v1/work-items/:workItemKey/commands/freeze',
6
7
  '/api/v1/work-items/:workItemKey/commands/unfreeze',
7
8
  '/api/v1/work-items/:workItemKey/commands/delete',
@@ -15,6 +15,11 @@ export type WakeCommand = ({
15
15
  }) | ({
16
16
  readonly kind: 'api' | 'ui';
17
17
  } & HostOptions) | ({
18
+ readonly kind: 'mcp';
19
+ } & {
20
+ readonly sessionPath: string;
21
+ readonly wakeRoot?: string;
22
+ }) | ({
18
23
  readonly kind: 'ui-token';
19
24
  readonly accessKey?: string;
20
25
  } & Pick<HostOptions, 'wakeRoot'>) | {
@@ -84,6 +89,9 @@ export interface WakeCliApplications {
84
89
  readonly ui: {
85
90
  start(options?: HostOptions): Promise<void>;
86
91
  };
92
+ readonly mcp?: {
93
+ serve(sessionPath: string, token: string): Promise<void>;
94
+ };
87
95
  readonly auth?: {
88
96
  token(accessKey?: string): Promise<string>;
89
97
  };
@@ -27,6 +27,8 @@ export function parseWakeCommand(arguments_) {
27
27
  return parseUiCommand(arguments_.slice(1));
28
28
  case 'api':
29
29
  return { kind: command, ...parseHostOptions(arguments_.slice(1)) };
30
+ case 'mcp':
31
+ return parseMcpCommand(arguments_.slice(1));
30
32
  case 'tick':
31
33
  case 'start':
32
34
  case 'stop':
@@ -44,6 +46,27 @@ export function parseWakeCommand(arguments_) {
44
46
  throw new Error(`Unknown wake command: ${command ?? ''}`);
45
47
  }
46
48
  }
49
+ function parseMcpCommand(arguments_) {
50
+ if (arguments_[0] !== 'serve')
51
+ throw new Error(`Unknown mcp command: ${arguments_[0] ?? ''}`);
52
+ let sessionPath;
53
+ let wakeRoot;
54
+ for (let index = 1; index < arguments_.length; index += 2) {
55
+ const flag = arguments_[index];
56
+ const value = requiredArgument(arguments_[index + 1], `value for ${flag ?? 'option'}`);
57
+ if (flag === '--session' && sessionPath === undefined)
58
+ sessionPath = value;
59
+ else if (flag === '--wake-root' && wakeRoot === undefined)
60
+ wakeRoot = value;
61
+ else
62
+ throw new Error(`Unknown or duplicate mcp option: ${flag ?? ''}`);
63
+ }
64
+ return {
65
+ kind: 'mcp',
66
+ sessionPath: requiredArgument(sessionPath, 'mcp --session'),
67
+ ...(wakeRoot === undefined ? {} : { wakeRoot }),
68
+ };
69
+ }
47
70
  function parseUiCommand(arguments_) {
48
71
  if (arguments_[0] !== 'token')
49
72
  return { kind: 'ui', ...parseHostOptions(arguments_) };
@@ -194,6 +217,9 @@ export async function runWakeCommand(command, applications, output, signal) {
194
217
  case 'ui-token':
195
218
  output.write(await formatUiTokenOutput(auth(applications).token(command.accessKey)));
196
219
  return;
220
+ case 'mcp':
221
+ await mcp(applications).serve(command.sessionPath, process.env.WAKE_MCP_TOKEN ?? '');
222
+ return;
197
223
  case 'audit':
198
224
  for (const record of await applications.audit.read(command.workItemId))
199
225
  output.write(`${JSON.stringify(record)}\n`);
@@ -247,6 +273,11 @@ function auth(applications) {
247
273
  throw new Error('UI auth CLI application was not composed');
248
274
  return applications.auth;
249
275
  }
276
+ function mcp(applications) {
277
+ if (applications.mcp === undefined)
278
+ throw new Error('MCP CLI application was not composed');
279
+ return applications.mcp;
280
+ }
250
281
  function writeResult(output, value) {
251
282
  if (value !== undefined)
252
283
  output.write(`${JSON.stringify(value)}\n`);
@@ -14,6 +14,7 @@ export const usage = [
14
14
  ' wake ui token set <key> Replace the UI login access key',
15
15
  ' wake audit Show autonomous decision audit history',
16
16
  ' wake correlate Manually correlate a resource to a work item',
17
+ ' wake mcp serve Run the private run-scoped Wake MCP transport',
17
18
  ' wake run resolve Resolve an escalated ambiguous run',
18
19
  ' wake doctor Diagnose config/GitHub/Docker/sandbox setup problems',
19
20
  ' wake maintenance clear Clear an abandoned failed self-update maintenance lease',
@@ -45,6 +45,7 @@ export * from './cli/infrastructure/process-log.js';
45
45
  export * from './cli/infrastructure/sandbox-startup-failure.js';
46
46
  export * from './cli/main.js';
47
47
  export * from './cli/usage.js';
48
+ export * from './mcp/stdio.js';
48
49
  export * from './contracts/config.js';
49
50
  export * from './web-host/asset-source.js';
50
51
  export * from './web-host/packaged-assets.js';
@@ -45,6 +45,7 @@ export * from './cli/infrastructure/process-log.js';
45
45
  export * from './cli/infrastructure/sandbox-startup-failure.js';
46
46
  export * from './cli/main.js';
47
47
  export * from './cli/usage.js';
48
+ export * from './mcp/stdio.js';
48
49
  export * from './contracts/config.js';
49
50
  export * from './web-host/asset-source.js';
50
51
  export * from './web-host/packaged-assets.js';
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /** Surface transport only: Bootstrap supplies the already-authorized tool server. */
3
+ export declare function serveMcpOverStdio(server: McpServer): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ /** Surface transport only: Bootstrap supplies the already-authorized tool server. */
3
+ export async function serveMcpOverStdio(server) {
4
+ await server.connect(new StdioServerTransport());
5
+ }