@atolis-hq/wake 0.1.0

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 (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +140 -0
  3. package/dist/src/adapters/claude/claude-runner.js +388 -0
  4. package/dist/src/adapters/claude/prompt-templates.js +57 -0
  5. package/dist/src/adapters/codex/codex-runner.js +391 -0
  6. package/dist/src/adapters/cursor/cursor-runner.js +352 -0
  7. package/dist/src/adapters/docker/docker-cli.js +97 -0
  8. package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
  9. package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
  10. package/dist/src/adapters/fake/fake-resource-index.js +21 -0
  11. package/dist/src/adapters/fake/fake-runner.js +22 -0
  12. package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
  13. package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
  14. package/dist/src/adapters/fs/resource-index.js +84 -0
  15. package/dist/src/adapters/fs/self-update-ledger.js +17 -0
  16. package/dist/src/adapters/fs/state-store.js +364 -0
  17. package/dist/src/adapters/git/git-workspace-manager.js +168 -0
  18. package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
  19. package/dist/src/adapters/github/github-auth.js +16 -0
  20. package/dist/src/adapters/github/github-client.js +100 -0
  21. package/dist/src/adapters/github/github-issues-work-source.js +410 -0
  22. package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
  23. package/dist/src/adapters/http/ui-assets.js +302 -0
  24. package/dist/src/adapters/http/ui-data.js +389 -0
  25. package/dist/src/adapters/http/ui-server.js +151 -0
  26. package/dist/src/adapters/runner/cli-command.js +43 -0
  27. package/dist/src/adapters/runner/prompt-templates.js +76 -0
  28. package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
  29. package/dist/src/adapters/runner/runner-registry.js +141 -0
  30. package/dist/src/adapters/runner/stage-prompt.js +256 -0
  31. package/dist/src/adapters/runner/transcripts.js +25 -0
  32. package/dist/src/cli/correlate-command.js +62 -0
  33. package/dist/src/cli/init-command.js +11 -0
  34. package/dist/src/cli/locks-command.js +19 -0
  35. package/dist/src/cli/sandbox-command.js +191 -0
  36. package/dist/src/cli/sandbox-logging.js +30 -0
  37. package/dist/src/cli/sandbox-resume.js +77 -0
  38. package/dist/src/cli/scaffold-assets.js +173 -0
  39. package/dist/src/cli/self-update-command.js +158 -0
  40. package/dist/src/cli/startup-preflight.js +127 -0
  41. package/dist/src/cli/stop-command.js +42 -0
  42. package/dist/src/cli/ui-command.js +27 -0
  43. package/dist/src/config/defaults.js +11 -0
  44. package/dist/src/config/load-config.js +26 -0
  45. package/dist/src/core/contracts.js +1 -0
  46. package/dist/src/core/control-plane.js +127 -0
  47. package/dist/src/core/lifecycle-service.js +8 -0
  48. package/dist/src/core/policy-engine.js +200 -0
  49. package/dist/src/core/projection-updater.js +438 -0
  50. package/dist/src/core/quota-backoff.js +69 -0
  51. package/dist/src/core/sink-router.js +85 -0
  52. package/dist/src/core/tick-runner.js +1347 -0
  53. package/dist/src/domain/branch-naming.js +14 -0
  54. package/dist/src/domain/resource-uri.js +38 -0
  55. package/dist/src/domain/runner-routing.js +108 -0
  56. package/dist/src/domain/schema.js +685 -0
  57. package/dist/src/domain/sources.js +16 -0
  58. package/dist/src/domain/stages.js +39 -0
  59. package/dist/src/domain/types.js +1 -0
  60. package/dist/src/domain/workflows.js +83 -0
  61. package/dist/src/lib/clock.js +5 -0
  62. package/dist/src/lib/event-log.js +21 -0
  63. package/dist/src/lib/json-file.js +23 -0
  64. package/dist/src/lib/lock.js +118 -0
  65. package/dist/src/lib/paths.js +44 -0
  66. package/dist/src/lib/work-id.js +19 -0
  67. package/dist/src/main.js +523 -0
  68. package/dist/src/version.js +1 -0
  69. package/docker/Dockerfile +54 -0
  70. package/docker/entrypoint.sh +75 -0
  71. package/docker/log-command.sh +107 -0
  72. package/docker/setup.sh +72 -0
  73. package/package.json +52 -0
  74. package/prompts/implement.md +49 -0
  75. package/prompts/refine.md +44 -0
@@ -0,0 +1,352 @@
1
+ import { parseRunnerResult } from '../../domain/schema.js';
2
+ import { buildStagePrompt } from '../runner/stage-prompt.js';
3
+ import { runAgentCliCommand } from '../runner/cli-command.js';
4
+ import { writeRunnerTranscript } from '../runner/transcripts.js';
5
+ const CURSOR_CLI_NAME = 'Cursor';
6
+ export function buildCursorAgentArgs(input) {
7
+ const fullPrompt = buildCursorPromptText(input);
8
+ return [
9
+ 'agent',
10
+ '-p',
11
+ '--output-format',
12
+ 'json',
13
+ '--model',
14
+ input.model,
15
+ '--trust',
16
+ ...(input.mode !== undefined ? ['--mode', input.mode] : []),
17
+ ...(input.force === true ? ['--force'] : []),
18
+ ...(input.resumeSessionId !== undefined ? [`--resume=${input.resumeSessionId}`] : []),
19
+ fullPrompt,
20
+ ];
21
+ }
22
+ export function buildCursorPromptText(input) {
23
+ return input.harnessPrompt === undefined
24
+ ? input.prompt
25
+ : `${input.harnessPrompt}\n\n${input.prompt}`;
26
+ }
27
+ export function buildCursorResumeArgs(input) {
28
+ return ['agent', '--resume=' + input.sessionId];
29
+ }
30
+ function compactLogValue(value) {
31
+ return value.replace(/\s+/g, ' ').trim();
32
+ }
33
+ export function formatCursorRunLogLine(input) {
34
+ const parts = [
35
+ '[cursor-run]',
36
+ `phase=${input.phase}`,
37
+ `runId=${input.runId}`,
38
+ `cli=Cursor`,
39
+ ...(input.model === undefined ? [] : [`model=${input.model}`]),
40
+ `repo=${input.repo}`,
41
+ `issueNumber=${input.issueNumber}`,
42
+ `action=${input.action}`,
43
+ `recentEventIds=${input.recentEventIds.length > 0 ? input.recentEventIds.join(',') : '(none)'}`,
44
+ ...(input.workspacePath === undefined
45
+ ? []
46
+ : [`workspacePath=${compactLogValue(input.workspacePath)}`]),
47
+ ...(input.sessionId === undefined ? [] : [`sessionId=${input.sessionId}`]),
48
+ ...(input.exitCode === undefined ? [] : [`exitCode=${input.exitCode}`]),
49
+ ];
50
+ return parts.join(' ');
51
+ }
52
+ // Verified against a live `cursor-agent agent -p --output-format json` call
53
+ // (run through the wake-sandbox container, since the host `cursor` binary
54
+ // here is the desktop app, not the CLI). Cursor's usage keys are camelCase
55
+ // and different names from both Claude's and Codex's snake_case fields, e.g.:
56
+ // {"usage":{"inputTokens":5945,"outputTokens":32,"cacheReadTokens":6020,"cacheWriteTokens":0}}
57
+ // No cost field was present in that response, so costUsd stays unset for Cursor.
58
+ function extractCursorTokenUsage(parsed) {
59
+ const usage = parsed.usage;
60
+ if (usage === undefined) {
61
+ return undefined;
62
+ }
63
+ const inputTokens = typeof usage.inputTokens === 'number' ? usage.inputTokens : undefined;
64
+ const outputTokens = typeof usage.outputTokens === 'number' ? usage.outputTokens : undefined;
65
+ if (inputTokens === undefined || outputTokens === undefined) {
66
+ return undefined;
67
+ }
68
+ const cacheReadInputTokens = typeof usage.cacheReadTokens === 'number' ? usage.cacheReadTokens : undefined;
69
+ const cacheCreationInputTokens = typeof usage.cacheWriteTokens === 'number' ? usage.cacheWriteTokens : undefined;
70
+ return {
71
+ inputTokens,
72
+ outputTokens,
73
+ ...(cacheCreationInputTokens === undefined ? {} : { cacheCreationInputTokens }),
74
+ ...(cacheReadInputTokens === undefined ? {} : { cacheReadInputTokens }),
75
+ };
76
+ }
77
+ export function extractCursorAgentResult(stdout) {
78
+ const trimmed = stdout.trim();
79
+ if (trimmed.length === 0) {
80
+ throw new Error('Cursor agent produced no output.');
81
+ }
82
+ const parsed = JSON.parse(trimmed);
83
+ const result = typeof parsed.result === 'string' ? parsed.result : undefined;
84
+ if (result === undefined) {
85
+ throw new Error('Cursor agent JSON output did not include a result field.');
86
+ }
87
+ const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id : undefined;
88
+ const isError = typeof parsed.is_error === 'boolean' ? parsed.is_error : undefined;
89
+ const tokenUsage = extractCursorTokenUsage(parsed);
90
+ return {
91
+ result,
92
+ ...(sessionId !== undefined ? { sessionId } : {}),
93
+ ...(isError !== undefined ? { isError } : {}),
94
+ ...(tokenUsage !== undefined ? { tokenUsage } : {}),
95
+ };
96
+ }
97
+ export function classifyCursorCliFailure(input) {
98
+ if (input.timedOut) {
99
+ return 'infra';
100
+ }
101
+ // Cursor JSON output signals a handled task-level error
102
+ if (input.isError === true) {
103
+ return 'task';
104
+ }
105
+ const text = `${input.stderr}\n${input.stdout}`.toLowerCase();
106
+ if (text.includes('rate limit') ||
107
+ text.includes('quota') ||
108
+ text.includes('billing') ||
109
+ text.includes('unauthorized') ||
110
+ text.includes('authentication') ||
111
+ text.includes('api key') ||
112
+ text.includes('too many requests')) {
113
+ return 'quota';
114
+ }
115
+ return 'infra';
116
+ }
117
+ // Cursor uses --mode ask for read-only stages, which already enforces
118
+ // the constraint at the CLI boundary. The capability note explains to the agent
119
+ // what tools are available in ask mode versus the default agent mode.
120
+ export function buildCursorToolCapabilityNote(input) {
121
+ if (input.workspaceMode !== 'read-only') {
122
+ return undefined;
123
+ }
124
+ const note = 'You are running in read-only ask mode. You can read and explore the repository but cannot modify files. ' +
125
+ 'Use your available file-reading and search tools to understand the codebase.';
126
+ return input.mode === 'resume' ? `Reminder: this is still a planning-only stage. ${note}` : note;
127
+ }
128
+ function resolveCursorMode(input) {
129
+ return input.workspaceMode === 'read-only' ? 'ask' : undefined;
130
+ }
131
+ function resolveModel(input) {
132
+ const { models, model } = input.settings;
133
+ const actionSpecificModel = models[input.action];
134
+ if (actionSpecificModel !== undefined) {
135
+ return actionSpecificModel;
136
+ }
137
+ if (models.default !== undefined) {
138
+ return models.default;
139
+ }
140
+ return model;
141
+ }
142
+ function readSandboxLogBreadcrumb() {
143
+ const containerName = process.env.WAKE_SANDBOX_CONTAINER_NAME;
144
+ if (containerName === undefined || containerName.length === 0) {
145
+ return null;
146
+ }
147
+ return {
148
+ text: `Sandbox logs: docker logs --tail 200 ${containerName}`,
149
+ metadata: {
150
+ sandboxContainerName: containerName,
151
+ },
152
+ };
153
+ }
154
+ export function createCursorRunner(options) {
155
+ return {
156
+ async run(input) {
157
+ const priorSessionId = input.projection.wake.sessionId;
158
+ const priorSessionCli = input.projection.wake.sessionCli;
159
+ const isResume = priorSessionId !== undefined && priorSessionCli === CURSOR_CLI_NAME;
160
+ const cursorMode = resolveCursorMode({
161
+ ...(input.workspaceMode === undefined ? {} : { workspaceMode: input.workspaceMode }),
162
+ });
163
+ const toolCapabilityNote = buildCursorToolCapabilityNote({
164
+ ...(input.workspaceMode === undefined ? {} : { workspaceMode: input.workspaceMode }),
165
+ mode: isResume ? 'resume' : 'start',
166
+ });
167
+ const stagePrompt = await buildStagePrompt({
168
+ action: input.action,
169
+ projection: input.projection,
170
+ mode: isResume ? 'resume' : 'start',
171
+ config: input.config,
172
+ ...(input.workspaceMode === undefined ? {} : { workspaceMode: input.workspaceMode }),
173
+ ...(input.mergeConflictDetected === true ? { mergeConflictDetected: true } : {}),
174
+ ...(input.upstreamChanges === undefined ? {} : { upstreamChanges: input.upstreamChanges }),
175
+ ...(toolCapabilityNote !== undefined ? { contextOverrides: { toolCapabilityNote } } : {}),
176
+ });
177
+ const model = resolveModel({ action: input.action, settings: options.settings });
178
+ const cwd = input.workspacePath ?? options.cwd;
179
+ const promptText = buildCursorPromptText({
180
+ prompt: stagePrompt.prompt,
181
+ harnessPrompt: stagePrompt.harnessPrompt,
182
+ });
183
+ const promptTranscriptPath = await writeRunnerTranscript({
184
+ config: input.config,
185
+ projection: input.projection,
186
+ runId: input.runId,
187
+ action: input.action,
188
+ cli: CURSOR_CLI_NAME,
189
+ kind: 'prompt',
190
+ text: promptText,
191
+ });
192
+ console.log(formatCursorRunLogLine({
193
+ phase: 'start',
194
+ runId: input.runId,
195
+ action: input.action,
196
+ issueNumber: input.projection.issue.number,
197
+ repo: input.projection.issue.repo,
198
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
199
+ model,
200
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
201
+ ...(isResume ? { sessionId: priorSessionId } : {}),
202
+ }));
203
+ const result = await runAgentCliCommand({
204
+ command: options.command,
205
+ args: buildCursorAgentArgs({
206
+ model,
207
+ prompt: promptText,
208
+ ...(cursorMode !== undefined ? { mode: cursorMode } : {}),
209
+ force: cursorMode === undefined,
210
+ ...(isResume ? { resumeSessionId: priorSessionId } : {}),
211
+ }),
212
+ cwd,
213
+ timeoutMs: options.settings.timeoutMs,
214
+ });
215
+ const responseTranscriptPath = await writeRunnerTranscript({
216
+ config: input.config,
217
+ projection: input.projection,
218
+ runId: input.runId,
219
+ action: input.action,
220
+ cli: CURSOR_CLI_NAME,
221
+ kind: 'response',
222
+ text: result.stdout,
223
+ });
224
+ if (result.exitCode !== 0 || result.timedOut || result.stdout.trim().length === 0) {
225
+ const sandboxLog = readSandboxLogBreadcrumb();
226
+ const failureClass = classifyCursorCliFailure({
227
+ stdout: result.stdout,
228
+ stderr: result.stderr,
229
+ timedOut: result.timedOut,
230
+ });
231
+ console.error(formatCursorRunLogLine({
232
+ phase: 'failure',
233
+ runId: input.runId,
234
+ action: input.action,
235
+ issueNumber: input.projection.issue.number,
236
+ repo: input.projection.issue.repo,
237
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
238
+ model,
239
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
240
+ exitCode: result.exitCode,
241
+ }));
242
+ return {
243
+ result: [
244
+ result.timedOut
245
+ ? `Cursor runner timed out after ${options.settings.timeoutMs}ms and was killed`
246
+ : result.stdout.trim().length === 0
247
+ ? 'Cursor runner produced no output'
248
+ : 'Cursor runner failed',
249
+ result.stderr,
250
+ sandboxLog?.text,
251
+ 'FAILED',
252
+ ]
253
+ .filter((part) => part !== undefined && part.length > 0)
254
+ .join('\n'),
255
+ model,
256
+ cli: CURSOR_CLI_NAME,
257
+ failureClass,
258
+ metadata: {
259
+ stdout: result.stdout,
260
+ stderr: result.stderr,
261
+ exitCode: result.exitCode,
262
+ failureClass,
263
+ ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
264
+ ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
265
+ ...(sandboxLog?.metadata ?? {}),
266
+ },
267
+ };
268
+ }
269
+ let parsed;
270
+ try {
271
+ parsed = extractCursorAgentResult(result.stdout);
272
+ }
273
+ catch (err) {
274
+ const sandboxLog = readSandboxLogBreadcrumb();
275
+ return {
276
+ result: [`Cursor runner output could not be parsed: ${String(err)}`, 'FAILED']
277
+ .filter((part) => part.length > 0)
278
+ .join('\n'),
279
+ model,
280
+ cli: CURSOR_CLI_NAME,
281
+ failureClass: 'infra',
282
+ metadata: {
283
+ stdout: result.stdout,
284
+ stderr: result.stderr,
285
+ parseError: String(err),
286
+ ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
287
+ ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
288
+ ...(sandboxLog?.metadata ?? {}),
289
+ },
290
+ };
291
+ }
292
+ const sandboxLog = readSandboxLogBreadcrumb();
293
+ console.log(formatCursorRunLogLine({
294
+ phase: 'success',
295
+ runId: input.runId,
296
+ action: input.action,
297
+ issueNumber: input.projection.issue.number,
298
+ repo: input.projection.issue.repo,
299
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
300
+ model,
301
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
302
+ ...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
303
+ }));
304
+ return {
305
+ result: parsed.result,
306
+ model,
307
+ cli: CURSOR_CLI_NAME,
308
+ ...(parseRunnerResult(parsed.result).status === 'FAILED'
309
+ ? { failureClass: 'task' }
310
+ : {}),
311
+ ...(parsed.sessionId === undefined ? {} : { session_id: parsed.sessionId }),
312
+ ...(parsed.tokenUsage === undefined ? {} : { tokenUsage: parsed.tokenUsage }),
313
+ metadata: {
314
+ stdout: result.stdout,
315
+ stderr: result.stderr,
316
+ raw: parsed,
317
+ skipApproval: stagePrompt.skipApproval,
318
+ ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
319
+ ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
320
+ ...(sandboxLog?.metadata ?? {}),
321
+ },
322
+ };
323
+ },
324
+ async smoke() {
325
+ const result = await runAgentCliCommand({
326
+ command: options.command,
327
+ args: buildCursorAgentArgs({
328
+ model: options.settings.smokeModel,
329
+ prompt: options.settings.smokePrompt,
330
+ force: true,
331
+ }),
332
+ cwd: options.cwd,
333
+ });
334
+ let parsed;
335
+ if (result.exitCode === 0 && result.stdout.trim().length > 0) {
336
+ try {
337
+ parsed = extractCursorAgentResult(result.stdout);
338
+ }
339
+ catch {
340
+ parsed = undefined;
341
+ }
342
+ }
343
+ return {
344
+ text: parsed?.result ?? '',
345
+ ...(parsed?.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
346
+ stdout: result.stdout,
347
+ stderr: result.stderr,
348
+ exitCode: result.exitCode,
349
+ };
350
+ },
351
+ };
352
+ }
@@ -0,0 +1,97 @@
1
+ function buildStopArgs(containerName, timeoutSeconds) {
2
+ return [
3
+ 'stop',
4
+ ...(timeoutSeconds !== undefined ? ['--time', String(timeoutSeconds)] : []),
5
+ containerName,
6
+ ];
7
+ }
8
+ function buildRunArgs(input) {
9
+ return [
10
+ 'run',
11
+ '-d',
12
+ '--name',
13
+ input.containerName,
14
+ '-v',
15
+ `${input.wakeRoot}:${input.containerMountPath}`,
16
+ '-v',
17
+ `${input.containerHomeRoot}:${input.containerHomeMountPath}`,
18
+ ...(input.extraMounts ?? []).flatMap((mount) => [
19
+ '-v',
20
+ `${mount.source}:${mount.target}${mount.readOnly === true ? ':ro' : ''}`,
21
+ ]),
22
+ // Auto-started by docker/entrypoint.sh; the UI binds 0.0.0.0 inside the
23
+ // container so this published port can reach it (127.0.0.1 inside the
24
+ // container would not be reachable via docker's port-forwarding NAT).
25
+ ...(input.ui?.enabled === true
26
+ ? [
27
+ '-p',
28
+ `127.0.0.1:${input.ui.port}:${input.ui.port}`,
29
+ '-e',
30
+ 'WAKE_UI_ENABLED=true',
31
+ '-e',
32
+ `WAKE_UI_PORT=${input.ui.port}`,
33
+ ...(input.ui.token !== undefined ? ['-e', `WAKE_UI_TOKEN=${input.ui.token}`] : []),
34
+ ...(input.ui.tunnel?.enabled === true
35
+ ? [
36
+ '-e',
37
+ 'WAKE_UI_TUNNEL_ENABLED=true',
38
+ '-e',
39
+ input.ui.tunnel.authToken !== undefined
40
+ ? `NGROK_AUTHTOKEN=${input.ui.tunnel.authToken}`
41
+ : 'NGROK_AUTHTOKEN',
42
+ ]
43
+ : []),
44
+ ]
45
+ : []),
46
+ ...(input.start?.enabled === true ? ['-e', 'WAKE_START_ENABLED=true'] : []),
47
+ input.image,
48
+ ];
49
+ }
50
+ export function createDockerCli(deps) {
51
+ return {
52
+ async build(input) {
53
+ await deps.run(['build', '-t', input.image, '-f', input.dockerfile, input.contextDir]);
54
+ },
55
+ async up(input) {
56
+ const imageExists = await deps.inspectImage(input.image);
57
+ if (!imageExists) {
58
+ throw new Error('Sandbox image not found. Run `wake sandbox build` first.');
59
+ }
60
+ const containerState = await deps.inspectContainer(input.containerName);
61
+ if (containerState === 'running') {
62
+ return;
63
+ }
64
+ if (containerState === 'stopped') {
65
+ await deps.run(['start', input.containerName]);
66
+ return;
67
+ }
68
+ await deps.run(buildRunArgs(input));
69
+ },
70
+ async update(input) {
71
+ const imageExists = await deps.inspectImage(input.image);
72
+ if (!imageExists) {
73
+ throw new Error('Sandbox image not found. Run `wake sandbox build` first.');
74
+ }
75
+ const containerState = await deps.inspectContainer(input.containerName);
76
+ if (containerState === 'running' || containerState === 'stopped') {
77
+ if (containerState === 'running') {
78
+ await deps.run(buildStopArgs(input.containerName, input.stopTimeoutSeconds));
79
+ }
80
+ await deps.run(['rm', input.containerName]);
81
+ }
82
+ await deps.run(buildRunArgs(input));
83
+ },
84
+ async down(containerName, options) {
85
+ await deps.run(buildStopArgs(containerName, options?.timeoutSeconds));
86
+ },
87
+ async exec(containerName, command, options) {
88
+ const interactive = options?.interactive ?? false;
89
+ await deps.run(command.length > 0
90
+ ? ['exec', interactive ? '-it' : '-i', containerName, ...command]
91
+ : ['exec', '-it', containerName, 'bash']);
92
+ },
93
+ async logs(containerName, tailLines) {
94
+ await deps.run(['logs', '--tail', String(tailLines), containerName]);
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Permanent test harness. `verifies` is the allowlist of URLs this fake
3
+ * treats as legitimate, mapped to the resourceUri they resolve to — anything
4
+ * not listed fails verification, exercising the "malformed claim" path.
5
+ */
6
+ export function createFakeArtifactVerifier(options) {
7
+ const byUrl = new Map(options.verifies.map((entry) => [entry.url, entry.resourceUri]));
8
+ return {
9
+ async verify(artifact) {
10
+ const resourceUri = byUrl.get(artifact.url);
11
+ return resourceUri === undefined ? null : { resourceUri };
12
+ },
13
+ };
14
+ }
@@ -0,0 +1,73 @@
1
+ import { createEventEnvelope, createUnkeyedEventEnvelope } from '../../lib/event-log.js';
2
+ /** Permanent test harness — zero-token equivalent of the real GitHub PR activity source. */
3
+ export function createFakeGitHubPullRequestActivitySource(options) {
4
+ const now = options.now ?? (() => new Date());
5
+ return {
6
+ async pollEvents(input) {
7
+ const nowIso = now().toISOString();
8
+ const watched = new Set((input?.watch ?? []).map((ref) => ref.resourceUri));
9
+ const events = [];
10
+ for (const pr of options.prs) {
11
+ const resourceUri = `github:pr:${pr.repo}#${pr.number}`;
12
+ if (!watched.has(resourceUri)) {
13
+ events.push(createUnkeyedEventEnvelope({
14
+ eventId: `fake-pr-seen-${pr.repo}-${pr.number}`,
15
+ streamScope: 'global-intake',
16
+ direction: 'inbound',
17
+ sourceSystem: 'fake-github-pr',
18
+ sourceEventType: 'pr.seen',
19
+ sourceRefs: { repo: pr.repo, resourceUri },
20
+ occurredAt: nowIso,
21
+ ingestedAt: nowIso,
22
+ trigger: 'context-only',
23
+ payload: { pr: { number: pr.number, author: pr.author, headRef: pr.headRef } },
24
+ }));
25
+ continue;
26
+ }
27
+ for (const comment of pr.comments) {
28
+ events.push(createUnkeyedEventEnvelope({
29
+ eventId: `fake-pr-comment-${pr.repo}-${pr.number}-${comment.id}`,
30
+ streamScope: 'work-item',
31
+ direction: 'inbound',
32
+ sourceSystem: 'fake-github-pr',
33
+ sourceEventType: 'pr.comment.created',
34
+ sourceRefs: { repo: pr.repo, commentId: comment.id, resourceUri },
35
+ occurredAt: nowIso,
36
+ ingestedAt: nowIso,
37
+ trigger: 'context-only',
38
+ payload: {
39
+ comment: {
40
+ id: comment.id,
41
+ body: comment.body,
42
+ author: { login: comment.author },
43
+ createdAt: nowIso,
44
+ updatedAt: nowIso,
45
+ resourceUri,
46
+ },
47
+ },
48
+ derivedHints: { botAuthoredComment: false },
49
+ }));
50
+ }
51
+ }
52
+ return events;
53
+ },
54
+ async deliverIntent(input) {
55
+ const publishedAt = now().toISOString();
56
+ return [
57
+ createEventEnvelope({
58
+ eventId: `${input.event.eventId}-published`,
59
+ workItemKey: input.event.workItemKey,
60
+ streamScope: 'work-item',
61
+ direction: 'outbound',
62
+ sourceSystem: 'fake-github-pr',
63
+ sourceEventType: 'pr.comment.reply.published',
64
+ sourceRefs: { ...input.event.sourceRefs, sink: 'fake-github-pr' },
65
+ occurredAt: publishedAt,
66
+ ingestedAt: publishedAt,
67
+ trigger: 'context-only',
68
+ payload: { intentEventId: input.event.eventId, kind: input.event.payload.kind, body: input.event.payload.body },
69
+ }),
70
+ ];
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * In-memory ResourceIndex test fake. Permanent test harness (per repo
3
+ * convention: fakes are passed on purpose, never a default that materializes
4
+ * when a caller forgets to wire one in), not a fallback baked into core/.
5
+ * Production always uses the disk-backed `createResourceIndex` from
6
+ * `src/adapters/fs/resource-index.ts`.
7
+ */
8
+ export function createFakeResourceIndex() {
9
+ const entries = new Map();
10
+ return {
11
+ async resolve(resourceUri) {
12
+ return entries.get(resourceUri);
13
+ },
14
+ async register(resourceUri, workItemKey) {
15
+ entries.set(resourceUri, workItemKey);
16
+ },
17
+ async retract(resourceUri) {
18
+ entries.delete(resourceUri);
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,22 @@
1
+ export function createFakeRunner(result, options) {
2
+ return {
3
+ async run(_) {
4
+ return result ?? {
5
+ result: [
6
+ 'Fake runner completed',
7
+ '',
8
+ '```wake-result',
9
+ '{ "status": "DONE" }',
10
+ '```',
11
+ 'DONE',
12
+ ].join('\n'),
13
+ model: 'fake',
14
+ cli: options?.cli ?? 'Fake',
15
+ session_id: 'fake-session-1',
16
+ metadata: {
17
+ source: 'fake-runner',
18
+ },
19
+ };
20
+ },
21
+ };
22
+ }