@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.
- package/README.md +3 -0
- package/dist/src/activities/agent/agent-activity.js +9 -3
- package/dist/src/activities/contracts/activity.d.ts +14 -0
- package/dist/src/artifacts/application/artifact-projection.d.ts +3 -0
- package/dist/src/artifacts/application/artifact-projection.js +15 -0
- package/dist/src/artifacts/application/artifact-repository.d.ts +13 -0
- package/dist/src/artifacts/application/artifact-repository.js +19 -0
- package/dist/src/artifacts/application/artifact-service.d.ts +40 -0
- package/dist/src/artifacts/application/artifact-service.js +128 -0
- package/dist/src/artifacts/contracts/config.d.ts +6 -0
- package/dist/src/artifacts/contracts/config.js +19 -0
- package/dist/src/artifacts/contracts/event-factory.d.ts +6 -0
- package/dist/src/artifacts/contracts/event-factory.js +10 -0
- package/dist/src/artifacts/contracts/events.d.ts +33 -0
- package/dist/src/artifacts/contracts/events.js +59 -0
- package/dist/src/artifacts/contracts/identifiers.d.ts +5 -0
- package/dist/src/artifacts/contracts/identifiers.js +8 -0
- package/dist/src/artifacts/contracts/paths.d.ts +5 -0
- package/dist/src/artifacts/contracts/paths.js +9 -0
- package/dist/src/artifacts/contracts/streams.d.ts +7 -0
- package/dist/src/artifacts/contracts/streams.js +4 -0
- package/dist/src/artifacts/contracts/views.d.ts +20 -0
- package/dist/src/artifacts/contracts/views.js +1 -0
- package/dist/src/artifacts/domain/artifact-work-item.d.ts +4 -0
- package/dist/src/artifacts/domain/artifact-work-item.js +42 -0
- package/dist/src/artifacts/index.d.ts +14 -0
- package/dist/src/artifacts/index.js +14 -0
- package/dist/src/artifacts/infrastructure/file-artifact-store.d.ts +22 -0
- package/dist/src/artifacts/infrastructure/file-artifact-store.js +51 -0
- package/dist/src/artifacts/infrastructure/mcp-server.d.ts +18 -0
- package/dist/src/artifacts/infrastructure/mcp-server.js +135 -0
- package/dist/src/artifacts/infrastructure/mcp-session-store.d.ts +25 -0
- package/dist/src/artifacts/infrastructure/mcp-session-store.js +61 -0
- package/dist/src/bootstrap/artifact-mcp.d.ts +23 -0
- package/dist/src/bootstrap/artifact-mcp.js +119 -0
- package/dist/src/bootstrap/composition-root.d.ts +2 -0
- package/dist/src/bootstrap/composition-root.js +19 -0
- package/dist/src/bootstrap/config/root-schema.d.ts +6 -0
- package/dist/src/bootstrap/config/root-schema.js +5 -0
- package/dist/src/bootstrap/paths.d.ts +1 -0
- package/dist/src/bootstrap/paths.js +1 -0
- package/dist/src/bootstrap/projection-runtime.js +2 -0
- package/dist/src/bootstrap/runner-registry.js +22 -4
- package/dist/src/bootstrap/surface-api-work-applications.js +39 -1
- package/dist/src/bootstrap/surface-cli-applications.js +12 -1
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/execution/application/execution-activity.d.ts +11 -0
- package/dist/src/execution/application/execution-activity.js +26 -10
- package/dist/src/execution/application/execution-service.js +1 -1
- package/dist/src/execution/application/execution-validation.js +17 -3
- package/dist/src/execution/contracts/runner.d.ts +10 -0
- package/dist/src/execution/contracts/workspace.d.ts +11 -1
- package/dist/src/execution/infrastructure/runners/claude.d.ts +1 -0
- package/dist/src/execution/infrastructure/runners/claude.js +21 -0
- package/dist/src/execution/infrastructure/runners/codex.js +12 -0
- package/dist/src/execution/infrastructure/runners/fake.d.ts +1 -0
- package/dist/src/execution/infrastructure/runners/fake.js +1 -0
- package/dist/src/execution/infrastructure/runners/registry.d.ts +3 -1
- package/dist/src/execution/infrastructure/runners/registry.js +5 -3
- package/dist/src/execution/infrastructure/workspace/git-workspace.d.ts +10 -5
- package/dist/src/execution/infrastructure/workspace/git-workspace.js +270 -38
- package/dist/src/surfaces/api/contracts/work.d.ts +10 -0
- package/dist/src/surfaces/api/routes/applications.d.ts +5 -0
- package/dist/src/surfaces/api/routes/read.js +34 -3
- package/dist/src/surfaces/api/routes/work.d.ts +1 -1
- package/dist/src/surfaces/api/routes/work.js +1 -0
- package/dist/src/surfaces/cli/main.d.ts +8 -0
- package/dist/src/surfaces/cli/main.js +31 -0
- package/dist/src/surfaces/cli/usage.js +1 -0
- package/dist/src/surfaces/index.d.ts +1 -0
- package/dist/src/surfaces/index.js +1 -0
- package/dist/src/surfaces/mcp/stdio.d.ts +3 -0
- package/dist/src/surfaces/mcp/stdio.js +5 -0
- package/dist/src/surfaces/web-assets/assets/{index-CuAZYm7r.js → index-CuIiUKuh.js} +2 -2
- package/dist/src/surfaces/web-assets/index.html +1 -1
- 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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
182
|
+
const failures = [...releaseFailures];
|
|
183
|
+
for (const workspaceId of workspaceIds) {
|
|
81
184
|
if (await options.isPaused?.())
|
|
82
185
|
break;
|
|
83
|
-
const result = await
|
|
84
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
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
|
|
112
|
-
if (
|
|
113
|
-
return
|
|
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,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
|
+
}
|