@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,388 @@
1
+ import { parseClaudePrintResult, parseRunnerResult } from '../../domain/schema.js';
2
+ import { runAgentCliCommand } from '../runner/cli-command.js';
3
+ import { buildStagePrompt } from '../runner/stage-prompt.js';
4
+ import { writeRunnerTranscript } from '../runner/transcripts.js';
5
+ export { buildStagePrompt } from '../runner/stage-prompt.js';
6
+ function slugify(value, maxLength = 40) {
7
+ return value
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9]+/g, '-')
10
+ .replace(/^-+|-+$/g, '')
11
+ .slice(0, maxLength)
12
+ .replace(/-+$/g, '');
13
+ }
14
+ export function buildWakeSessionName(input) {
15
+ return [
16
+ input.sessionName,
17
+ `issue-${input.issueNumber}`,
18
+ slugify(input.title),
19
+ input.runId,
20
+ ]
21
+ .filter((part) => part.length > 0)
22
+ .join('-');
23
+ }
24
+ function compactLogValue(value) {
25
+ return value.replace(/\s+/g, ' ').trim();
26
+ }
27
+ export function formatClaudeRunLogLine(input) {
28
+ const parts = [
29
+ '[claude-run]',
30
+ `phase=${input.phase}`,
31
+ `runId=${input.runId}`,
32
+ `cli=Claude`,
33
+ ...(input.model === undefined ? [] : [`model=${input.model}`]),
34
+ `repo=${input.repo}`,
35
+ `issueNumber=${input.issueNumber}`,
36
+ `action=${input.action}`,
37
+ `recentEventIds=${input.recentEventIds.length > 0 ? input.recentEventIds.join(',') : '(none)'}`,
38
+ ...(input.workspacePath === undefined
39
+ ? []
40
+ : [`workspacePath=${compactLogValue(input.workspacePath)}`]),
41
+ ...(input.sessionId === undefined ? [] : [`sessionId=${input.sessionId}`]),
42
+ ...(input.exitCode === undefined ? [] : [`exitCode=${input.exitCode}`]),
43
+ ];
44
+ return parts.join(' ');
45
+ }
46
+ export function buildClaudePrintArgs(options) {
47
+ return [
48
+ '-p',
49
+ '--output-format',
50
+ 'json',
51
+ '--model',
52
+ options.model,
53
+ '--name',
54
+ options.sessionName,
55
+ ...(options.resumeSessionId === undefined ? [] : ['--resume', options.resumeSessionId]),
56
+ ...(options.effort === undefined ? [] : ['--effort', options.effort]),
57
+ ...(options.systemPrompt === undefined
58
+ ? []
59
+ : ['--append-system-prompt', options.systemPrompt]),
60
+ ...(options.maxTurns === undefined ? [] : ['--max-turns', String(options.maxTurns)]),
61
+ ...(options.permissionMode === undefined
62
+ ? []
63
+ : ['--permission-mode', options.permissionMode]),
64
+ ...(options.allowedTools === undefined || options.allowedTools.length === 0
65
+ ? []
66
+ : ['--allowedTools', options.allowedTools.join(' ')]),
67
+ ...(options.remoteControlName === undefined
68
+ ? []
69
+ : ['--remote-control', options.remoteControlName]),
70
+ // Generic escape hatch for any other CLI flag a stage template needs
71
+ // (e.g. --dangerously-skip-permissions) without bespoke code per flag.
72
+ ...(options.extraArgs ?? []),
73
+ // Terminate option parsing so the prompt is never swallowed by a
74
+ // variadic/optional-value flag above (e.g. --allowedTools, --remote-control,
75
+ // or anything in extraArgs with an optional value).
76
+ '--',
77
+ options.prompt,
78
+ ];
79
+ }
80
+ export function buildClaudeRemoteControlArgs(options) {
81
+ return [
82
+ '--bg',
83
+ '--remote-control',
84
+ options.remoteControlName,
85
+ '--model',
86
+ options.model,
87
+ '--name',
88
+ options.sessionName,
89
+ options.prompt,
90
+ ];
91
+ }
92
+ export function runClaudeCommand(input) {
93
+ return runAgentCliCommand(input);
94
+ }
95
+ function resolveModel(options) {
96
+ const { models, model } = options.settings;
97
+ const actionSpecificModel = models[options.action];
98
+ if (actionSpecificModel !== undefined) {
99
+ return actionSpecificModel;
100
+ }
101
+ if (models.default !== undefined) {
102
+ return models.default;
103
+ }
104
+ return model;
105
+ }
106
+ function parseClaudePrintOutput(stdout) {
107
+ return parseClaudePrintResult(JSON.parse(stdout));
108
+ }
109
+ function extractTokenUsage(parsed) {
110
+ const usage = parsed.usage;
111
+ if (usage === undefined) {
112
+ return undefined;
113
+ }
114
+ const inputTokens = typeof usage.input_tokens === 'number' ? usage.input_tokens : 0;
115
+ const outputTokens = typeof usage.output_tokens === 'number' ? usage.output_tokens : 0;
116
+ const cacheCreationInputTokens = typeof usage.cache_creation_input_tokens === 'number'
117
+ ? usage.cache_creation_input_tokens
118
+ : undefined;
119
+ const cacheReadInputTokens = typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : undefined;
120
+ return {
121
+ inputTokens,
122
+ outputTokens,
123
+ ...(cacheCreationInputTokens === undefined ? {} : { cacheCreationInputTokens }),
124
+ ...(cacheReadInputTokens === undefined ? {} : { cacheReadInputTokens }),
125
+ ...(parsed.total_cost_usd === undefined ? {} : { costUsd: parsed.total_cost_usd }),
126
+ ...(parsed.num_turns === undefined ? {} : { turns: parsed.num_turns }),
127
+ };
128
+ }
129
+ const CLAUDE_CLI_NAME = 'Claude';
130
+ export function classifyClaudeCliFailure(input) {
131
+ if (input.timedOut) {
132
+ return 'infra';
133
+ }
134
+ const text = `${input.stderr}\n${input.stdout}`.toLowerCase();
135
+ if (text.includes('rate limit') ||
136
+ text.includes('quota') ||
137
+ text.includes('billing') ||
138
+ text.includes('credit balance') ||
139
+ text.includes('spend limit') ||
140
+ text.includes('usage limit') ||
141
+ text.includes('session limit') ||
142
+ text.includes('too many requests') ||
143
+ text.includes('authentication') ||
144
+ text.includes('unauthorized') ||
145
+ text.includes('permission denied')) {
146
+ return 'quota';
147
+ }
148
+ return 'infra';
149
+ }
150
+ function readSandboxLogBreadcrumb() {
151
+ const containerName = process.env.WAKE_SANDBOX_CONTAINER_NAME;
152
+ if (containerName === undefined || containerName.length === 0) {
153
+ return null;
154
+ }
155
+ return {
156
+ text: `Sandbox logs: docker logs --tail 200 ${containerName}`,
157
+ metadata: {
158
+ sandboxContainerName: containerName,
159
+ },
160
+ };
161
+ }
162
+ export function createClaudeRunner(options) {
163
+ return {
164
+ async run(input) {
165
+ const sessionName = buildWakeSessionName({
166
+ sessionName: options.settings.sessionName,
167
+ issueNumber: input.projection.issue.number,
168
+ title: input.projection.issue.title,
169
+ runId: input.runId,
170
+ });
171
+ // Resume an in-progress session when the projection carries a session ID
172
+ // that was created by this same CLI. This happens when the previous run
173
+ // ended with BLOCKED and the same action is being retried after a human
174
+ // reply. Any forward-stage transition or FAILED run clears the stored
175
+ // session ID so that the next action always starts fresh.
176
+ const priorSessionId = input.projection.wake.sessionId;
177
+ const priorSessionCli = input.projection.wake.sessionCli;
178
+ const isResume = priorSessionId !== undefined && priorSessionCli === CLAUDE_CLI_NAME;
179
+ const stagePrompt = await buildStagePrompt({
180
+ action: input.action,
181
+ projection: input.projection,
182
+ mode: isResume ? 'resume' : 'start',
183
+ config: input.config,
184
+ ...(input.mergeConflictDetected === true ? { mergeConflictDetected: true } : {}),
185
+ ...(input.upstreamChanges === undefined ? {} : { upstreamChanges: input.upstreamChanges }),
186
+ });
187
+ const model = resolveModel({
188
+ action: input.action,
189
+ settings: options.settings,
190
+ });
191
+ const args = buildClaudePrintArgs({
192
+ model,
193
+ prompt: stagePrompt.prompt,
194
+ systemPrompt: stagePrompt.harnessPrompt,
195
+ sessionName,
196
+ allowedTools: stagePrompt.allowedTools,
197
+ extraArgs: stagePrompt.extraArgs,
198
+ maxTurns: stagePrompt.maxTurns,
199
+ ...(stagePrompt.permissionMode === undefined
200
+ ? {}
201
+ : { permissionMode: stagePrompt.permissionMode }),
202
+ ...(options.settings.remoteControl.enabled
203
+ ? { remoteControlName: sessionName }
204
+ : {}),
205
+ ...(options.settings.effort === undefined ? {} : { effort: options.settings.effort }),
206
+ ...(isResume ? { resumeSessionId: priorSessionId } : {}),
207
+ });
208
+ const promptTranscriptPath = await writeRunnerTranscript({
209
+ config: input.config,
210
+ projection: input.projection,
211
+ runId: input.runId,
212
+ action: input.action,
213
+ cli: CLAUDE_CLI_NAME,
214
+ kind: 'prompt',
215
+ text: stagePrompt.prompt,
216
+ });
217
+ console.log(formatClaudeRunLogLine({
218
+ phase: 'start',
219
+ runId: input.runId,
220
+ action: input.action,
221
+ issueNumber: input.projection.issue.number,
222
+ repo: input.projection.issue.repo,
223
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
224
+ model,
225
+ ...(input.workspacePath === undefined
226
+ ? {}
227
+ : { workspacePath: input.workspacePath }),
228
+ }));
229
+ const result = await runClaudeCommand({
230
+ command: options.command,
231
+ args,
232
+ cwd: input.workspacePath ?? options.cwd,
233
+ timeoutMs: options.settings.timeoutMs,
234
+ });
235
+ const responseTranscriptPath = await writeRunnerTranscript({
236
+ config: input.config,
237
+ projection: input.projection,
238
+ runId: input.runId,
239
+ action: input.action,
240
+ cli: CLAUDE_CLI_NAME,
241
+ kind: 'response',
242
+ text: result.stdout,
243
+ });
244
+ if (result.exitCode !== 0 || result.timedOut || result.stdout.trim().length === 0) {
245
+ const sandboxLog = readSandboxLogBreadcrumb();
246
+ const failureClass = classifyClaudeCliFailure({
247
+ stdout: result.stdout,
248
+ stderr: result.stderr,
249
+ timedOut: result.timedOut,
250
+ });
251
+ console.error(formatClaudeRunLogLine({
252
+ phase: 'failure',
253
+ runId: input.runId,
254
+ action: input.action,
255
+ issueNumber: input.projection.issue.number,
256
+ repo: input.projection.issue.repo,
257
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
258
+ model,
259
+ ...(input.workspacePath === undefined
260
+ ? {}
261
+ : { workspacePath: input.workspacePath }),
262
+ exitCode: result.exitCode,
263
+ }));
264
+ let parsedReason;
265
+ let stdoutFailureDetail;
266
+ const trimmedStdout = result.stdout.trim();
267
+ try {
268
+ if (trimmedStdout.length > 0) {
269
+ parsedReason = parseClaudePrintOutput(result.stdout).result;
270
+ }
271
+ }
272
+ catch {
273
+ // stdout wasn't valid JSON, so it is likely a CLI-level error
274
+ // message rather than a Claude print result.
275
+ stdoutFailureDetail = trimmedStdout;
276
+ }
277
+ return {
278
+ result: [
279
+ result.timedOut
280
+ ? `Claude runner timed out after ${options.settings.timeoutMs}ms and was killed`
281
+ : trimmedStdout.length === 0
282
+ ? 'Claude runner produced no output'
283
+ : parsedReason !== undefined
284
+ ? `Claude runner failed: ${parsedReason}`
285
+ : 'Claude runner failed',
286
+ result.stderr,
287
+ stdoutFailureDetail,
288
+ sandboxLog?.text,
289
+ 'FAILED',
290
+ ]
291
+ .filter((part) => part !== undefined && part.length > 0)
292
+ .join('\n'),
293
+ model,
294
+ cli: CLAUDE_CLI_NAME,
295
+ failureClass,
296
+ metadata: {
297
+ stdout: result.stdout,
298
+ stderr: result.stderr,
299
+ exitCode: result.exitCode,
300
+ failureClass,
301
+ ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
302
+ ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
303
+ ...(sandboxLog?.metadata ?? {}),
304
+ },
305
+ };
306
+ }
307
+ const parsed = parseClaudePrintOutput(result.stdout);
308
+ const sandboxLog = readSandboxLogBreadcrumb();
309
+ console.log(formatClaudeRunLogLine({
310
+ phase: 'success',
311
+ runId: input.runId,
312
+ action: input.action,
313
+ issueNumber: input.projection.issue.number,
314
+ repo: input.projection.issue.repo,
315
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
316
+ model,
317
+ ...(input.workspacePath === undefined
318
+ ? {}
319
+ : { workspacePath: input.workspacePath }),
320
+ ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
321
+ }));
322
+ const tokenUsage = extractTokenUsage(parsed);
323
+ return {
324
+ result: parsed.result,
325
+ model,
326
+ cli: CLAUDE_CLI_NAME,
327
+ ...(parseRunnerResult(parsed.result).status === 'FAILED'
328
+ ? { failureClass: 'task' }
329
+ : {}),
330
+ ...(parsed.session_id === undefined
331
+ ? {}
332
+ : { session_id: parsed.session_id }),
333
+ ...(tokenUsage === undefined ? {} : { tokenUsage }),
334
+ metadata: {
335
+ stdout: result.stdout,
336
+ stderr: result.stderr,
337
+ raw: parsed,
338
+ skipApproval: stagePrompt.skipApproval,
339
+ ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
340
+ ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
341
+ ...(sandboxLog?.metadata ?? {}),
342
+ },
343
+ };
344
+ },
345
+ async smoke() {
346
+ const args = buildClaudePrintArgs({
347
+ model: options.settings.smokeModel,
348
+ prompt: options.settings.smokePrompt,
349
+ sessionName: options.settings.sessionName,
350
+ });
351
+ const result = await runClaudeCommand({
352
+ command: options.command,
353
+ args,
354
+ cwd: options.cwd,
355
+ });
356
+ const parsed = result.exitCode === 0 && result.stdout.trim().length > 0
357
+ ? parseClaudePrintOutput(result.stdout)
358
+ : undefined;
359
+ return {
360
+ text: parsed?.result ?? '',
361
+ ...(parsed?.session_id === undefined
362
+ ? {}
363
+ : { sessionId: parsed.session_id }),
364
+ stdout: result.stdout,
365
+ stderr: result.stderr,
366
+ exitCode: result.exitCode,
367
+ };
368
+ },
369
+ async startRemoteControlSmoke() {
370
+ const args = buildClaudeRemoteControlArgs({
371
+ model: options.settings.smokeModel,
372
+ prompt: options.settings.smokePrompt,
373
+ remoteControlName: options.settings.remoteControlName,
374
+ sessionName: options.settings.sessionName,
375
+ });
376
+ const result = await runClaudeCommand({
377
+ command: options.command,
378
+ args,
379
+ cwd: options.cwd,
380
+ });
381
+ return {
382
+ ...result,
383
+ command: options.command,
384
+ args,
385
+ };
386
+ },
387
+ };
388
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ function findProjectRoot(startDir) {
6
+ let dir = startDir;
7
+ for (let depth = 0; depth < 8; depth += 1) {
8
+ if (existsSync(join(dir, 'package.json'))) {
9
+ return dir;
10
+ }
11
+ const parent = dirname(dir);
12
+ if (parent === dir) {
13
+ break;
14
+ }
15
+ dir = parent;
16
+ }
17
+ throw new Error(`Could not locate project root above ${startDir}`);
18
+ }
19
+ function parseFrontmatter(raw) {
20
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
21
+ if (match === null) {
22
+ return { frontmatter: {}, body: raw };
23
+ }
24
+ const [, frontmatterBlock, body] = match;
25
+ const frontmatter = {};
26
+ for (const line of (frontmatterBlock ?? '').split(/\r?\n/)) {
27
+ const lineMatch = /^([\w.-]+):\s*(.*)$/.exec(line);
28
+ if (lineMatch === null) {
29
+ continue;
30
+ }
31
+ const [, key, value] = lineMatch;
32
+ if (key !== undefined) {
33
+ frontmatter[key] = (value ?? '').trim();
34
+ }
35
+ }
36
+ return { frontmatter, body: body ?? '' };
37
+ }
38
+ export function promptsRoot(explicitRoot) {
39
+ return explicitRoot ?? join(findProjectRoot(dirname(fileURLToPath(import.meta.url))), 'prompts');
40
+ }
41
+ export async function loadPromptTemplate(stage, mode, options) {
42
+ const filePath = join(promptsRoot(options?.promptsRoot), `${stage}.${mode}.md`);
43
+ const raw = await readFile(filePath, 'utf8');
44
+ return parseFrontmatter(raw);
45
+ }
46
+ export function renderPromptTemplate(template, context) {
47
+ return template.body.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (fullMatch, token) => {
48
+ if (!(token in context)) {
49
+ return fullMatch;
50
+ }
51
+ const value = context[token];
52
+ if (value === undefined) {
53
+ return '';
54
+ }
55
+ return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
56
+ });
57
+ }