@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.
- package/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/src/adapters/claude/claude-runner.js +388 -0
- package/dist/src/adapters/claude/prompt-templates.js +57 -0
- package/dist/src/adapters/codex/codex-runner.js +391 -0
- package/dist/src/adapters/cursor/cursor-runner.js +352 -0
- package/dist/src/adapters/docker/docker-cli.js +97 -0
- package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
- package/dist/src/adapters/fake/fake-resource-index.js +21 -0
- package/dist/src/adapters/fake/fake-runner.js +22 -0
- package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
- package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
- package/dist/src/adapters/fs/resource-index.js +84 -0
- package/dist/src/adapters/fs/self-update-ledger.js +17 -0
- package/dist/src/adapters/fs/state-store.js +364 -0
- package/dist/src/adapters/git/git-workspace-manager.js +168 -0
- package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
- package/dist/src/adapters/github/github-auth.js +16 -0
- package/dist/src/adapters/github/github-client.js +100 -0
- package/dist/src/adapters/github/github-issues-work-source.js +410 -0
- package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
- package/dist/src/adapters/http/ui-assets.js +302 -0
- package/dist/src/adapters/http/ui-data.js +389 -0
- package/dist/src/adapters/http/ui-server.js +151 -0
- package/dist/src/adapters/runner/cli-command.js +43 -0
- package/dist/src/adapters/runner/prompt-templates.js +76 -0
- package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
- package/dist/src/adapters/runner/runner-registry.js +141 -0
- package/dist/src/adapters/runner/stage-prompt.js +256 -0
- package/dist/src/adapters/runner/transcripts.js +25 -0
- package/dist/src/cli/correlate-command.js +62 -0
- package/dist/src/cli/init-command.js +11 -0
- package/dist/src/cli/locks-command.js +19 -0
- package/dist/src/cli/sandbox-command.js +191 -0
- package/dist/src/cli/sandbox-logging.js +30 -0
- package/dist/src/cli/sandbox-resume.js +77 -0
- package/dist/src/cli/scaffold-assets.js +173 -0
- package/dist/src/cli/self-update-command.js +158 -0
- package/dist/src/cli/startup-preflight.js +127 -0
- package/dist/src/cli/stop-command.js +42 -0
- package/dist/src/cli/ui-command.js +27 -0
- package/dist/src/config/defaults.js +11 -0
- package/dist/src/config/load-config.js +26 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/control-plane.js +127 -0
- package/dist/src/core/lifecycle-service.js +8 -0
- package/dist/src/core/policy-engine.js +200 -0
- package/dist/src/core/projection-updater.js +438 -0
- package/dist/src/core/quota-backoff.js +69 -0
- package/dist/src/core/sink-router.js +85 -0
- package/dist/src/core/tick-runner.js +1347 -0
- package/dist/src/domain/branch-naming.js +14 -0
- package/dist/src/domain/resource-uri.js +38 -0
- package/dist/src/domain/runner-routing.js +108 -0
- package/dist/src/domain/schema.js +685 -0
- package/dist/src/domain/sources.js +16 -0
- package/dist/src/domain/stages.js +39 -0
- package/dist/src/domain/types.js +1 -0
- package/dist/src/domain/workflows.js +83 -0
- package/dist/src/lib/clock.js +5 -0
- package/dist/src/lib/event-log.js +21 -0
- package/dist/src/lib/json-file.js +23 -0
- package/dist/src/lib/lock.js +118 -0
- package/dist/src/lib/paths.js +44 -0
- package/dist/src/lib/work-id.js +19 -0
- package/dist/src/main.js +523 -0
- package/dist/src/version.js +1 -0
- package/docker/Dockerfile +54 -0
- package/docker/entrypoint.sh +75 -0
- package/docker/log-command.sh +107 -0
- package/docker/setup.sh +72 -0
- package/package.json +52 -0
- package/prompts/implement.md +49 -0
- package/prompts/refine.md +44 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { configuredTicketSource } from '../../domain/sources.js';
|
|
3
|
+
import { indexHtml } from './ui-assets.js';
|
|
4
|
+
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
5
|
+
function sendJson(res, status, body) {
|
|
6
|
+
const payload = JSON.stringify(body, null, 2);
|
|
7
|
+
res.writeHead(status, {
|
|
8
|
+
'content-type': 'application/json; charset=utf-8',
|
|
9
|
+
'content-length': Buffer.byteLength(payload),
|
|
10
|
+
});
|
|
11
|
+
res.end(payload);
|
|
12
|
+
}
|
|
13
|
+
function extractBearerToken(req) {
|
|
14
|
+
const header = req.headers.authorization;
|
|
15
|
+
if (typeof header === 'string' && header.startsWith('Bearer ')) {
|
|
16
|
+
return header.slice('Bearer '.length);
|
|
17
|
+
}
|
|
18
|
+
const cookie = req.headers.cookie;
|
|
19
|
+
if (typeof cookie === 'string') {
|
|
20
|
+
const match = cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith('wake_ui_token='));
|
|
21
|
+
if (match !== undefined) {
|
|
22
|
+
return match.slice('wake_ui_token='.length);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parses `/items/<repo-with-slashes>/<issueNumber>[/events]` where repo itself
|
|
29
|
+
* may contain a `/` (e.g. `owner/name`), so the split can't assume a fixed arity.
|
|
30
|
+
*/
|
|
31
|
+
function parseItemPath(segments) {
|
|
32
|
+
const trailingIsEvents = segments.at(-1) === 'events';
|
|
33
|
+
const numberIndex = trailingIsEvents ? segments.length - 2 : segments.length - 1;
|
|
34
|
+
const issueNumberRaw = segments[numberIndex];
|
|
35
|
+
const issueNumber = issueNumberRaw === undefined ? Number.NaN : Number(issueNumberRaw);
|
|
36
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0 || numberIndex < 1) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const repo = segments.slice(0, numberIndex).join('/');
|
|
40
|
+
return {
|
|
41
|
+
repo,
|
|
42
|
+
issueNumber,
|
|
43
|
+
...(trailingIsEvents ? { suffix: 'events' } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function createUiServer(options) {
|
|
47
|
+
const now = options.now ?? (() => new Date());
|
|
48
|
+
return createServer((req, res) => {
|
|
49
|
+
void handleRequest(req, res, options, now).catch((error) => {
|
|
50
|
+
sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function handleRequest(req, res, options, now) {
|
|
55
|
+
const url = new URL(req.url ?? '/', 'http://internal');
|
|
56
|
+
// Whether a token is required is a bind-time decision — the caller (see
|
|
57
|
+
// ui-command.ts) only ever supplies a token when it configured a
|
|
58
|
+
// non-loopback --host, so once set it gates every request rather than
|
|
59
|
+
// trusting a per-connection remote-address check that docker's NAT/port
|
|
60
|
+
// publishing can make unreliable.
|
|
61
|
+
if (options.token !== undefined) {
|
|
62
|
+
const provided = extractBearerToken(req);
|
|
63
|
+
if (provided !== options.token) {
|
|
64
|
+
sendJson(res, 401, { error: 'missing or invalid token' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!url.pathname.startsWith('/api/v1/')) {
|
|
69
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
70
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
71
|
+
res.end(indexHtml);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
res.writeHead(404).end('not found');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (req.method !== 'GET') {
|
|
78
|
+
sendJson(res, 405, { error: 'this build only serves read endpoints; mutations are not implemented' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { stateStore, resourceIndex, config } = options;
|
|
82
|
+
const segments = url.pathname.slice('/api/v1/'.length).split('/').filter((part) => part.length > 0).map((s) => decodeURIComponent(s));
|
|
83
|
+
const resource = segments[0];
|
|
84
|
+
if (resource === 'status' && segments.length === 1) {
|
|
85
|
+
sendJson(res, 200, await buildStatus({ stateStore, config, now: now() }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (resource === 'board' && segments.length === 1) {
|
|
89
|
+
sendJson(res, 200, await buildBoard({ stateStore, config, now: now() }));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (resource === 'items' && segments.length >= 3) {
|
|
93
|
+
const parsed = parseItemPath(segments.slice(1));
|
|
94
|
+
if (parsed === null) {
|
|
95
|
+
sendJson(res, 400, { error: 'expected /items/<repo>/<issueNumber>' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const itemDetailInput = {
|
|
99
|
+
stateStore,
|
|
100
|
+
resourceIndex,
|
|
101
|
+
provider: configuredTicketSource(config),
|
|
102
|
+
repo: parsed.repo,
|
|
103
|
+
issueNumber: parsed.issueNumber,
|
|
104
|
+
};
|
|
105
|
+
if (parsed.suffix === 'events') {
|
|
106
|
+
const detail = await buildItemDetail(itemDetailInput);
|
|
107
|
+
sendJson(res, 200, detail?.events ?? []);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const detail = await buildItemDetail(itemDetailInput);
|
|
111
|
+
if (detail === null) {
|
|
112
|
+
sendJson(res, 404, { error: 'item not found' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
sendJson(res, 200, detail);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (resource === 'runs' && segments.length === 1) {
|
|
119
|
+
sendJson(res, 200, await buildRuns({
|
|
120
|
+
stateStore,
|
|
121
|
+
status: url.searchParams.get('status') ?? undefined,
|
|
122
|
+
action: url.searchParams.get('action') ?? undefined,
|
|
123
|
+
repo: url.searchParams.get('repo') ?? undefined,
|
|
124
|
+
}));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (resource === 'events' && segments.length === 1) {
|
|
128
|
+
const limitParam = url.searchParams.get('limit');
|
|
129
|
+
sendJson(res, 200, await buildEventsFeed({
|
|
130
|
+
stateStore,
|
|
131
|
+
workItemKey: url.searchParams.get('workItemKey') ?? undefined,
|
|
132
|
+
direction: url.searchParams.get('direction') ?? undefined,
|
|
133
|
+
type: url.searchParams.get('type') ?? undefined,
|
|
134
|
+
limit: limitParam === null ? undefined : Number(limitParam),
|
|
135
|
+
}));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (resource === 'config' && segments.length === 1) {
|
|
139
|
+
sendJson(res, 200, await buildConfigView({ config, stateStore, now: now() }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (resource === 'health' && segments.length === 1) {
|
|
143
|
+
sendJson(res, 200, await buildHealth({ stateStore, config, now: now() }));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (resource === 'workspaces' && segments.length === 1) {
|
|
147
|
+
sendJson(res, 200, await buildWorkspaces({ stateStore }));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
sendJson(res, 404, { error: `unknown endpoint: ${url.pathname}` });
|
|
151
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const TIMEOUT_KILL_GRACE_MS = 5_000;
|
|
3
|
+
export function runAgentCliCommand(input) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(input.command, input.args, {
|
|
6
|
+
cwd: input.cwd,
|
|
7
|
+
env: process.env,
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
});
|
|
10
|
+
let stdout = '';
|
|
11
|
+
let stderr = '';
|
|
12
|
+
let timedOut = false;
|
|
13
|
+
let killTimer;
|
|
14
|
+
const timeoutTimer = input.timeoutMs === undefined
|
|
15
|
+
? undefined
|
|
16
|
+
: setTimeout(() => {
|
|
17
|
+
timedOut = true;
|
|
18
|
+
child.kill('SIGTERM');
|
|
19
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), TIMEOUT_KILL_GRACE_MS);
|
|
20
|
+
}, input.timeoutMs);
|
|
21
|
+
child.stdout.on('data', (chunk) => {
|
|
22
|
+
stdout += chunk.toString();
|
|
23
|
+
});
|
|
24
|
+
child.stderr.on('data', (chunk) => {
|
|
25
|
+
stderr += chunk.toString();
|
|
26
|
+
});
|
|
27
|
+
child.on('error', (error) => {
|
|
28
|
+
clearTimeout(timeoutTimer);
|
|
29
|
+
clearTimeout(killTimer);
|
|
30
|
+
reject(error);
|
|
31
|
+
});
|
|
32
|
+
child.on('close', (exitCode) => {
|
|
33
|
+
clearTimeout(timeoutTimer);
|
|
34
|
+
clearTimeout(killTimer);
|
|
35
|
+
resolve({
|
|
36
|
+
stdout,
|
|
37
|
+
stderr,
|
|
38
|
+
exitCode: exitCode ?? 1,
|
|
39
|
+
timedOut,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
import Handlebars from 'handlebars';
|
|
6
|
+
function findProjectRoot(startDir) {
|
|
7
|
+
let dir = startDir;
|
|
8
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
9
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
const parent = dirname(dir);
|
|
13
|
+
if (parent === dir) {
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Could not locate project root above ${startDir}`);
|
|
19
|
+
}
|
|
20
|
+
function parseFrontmatter(raw) {
|
|
21
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
22
|
+
if (match === null) {
|
|
23
|
+
return { frontmatter: {}, body: raw };
|
|
24
|
+
}
|
|
25
|
+
const [, frontmatterBlock, body] = match;
|
|
26
|
+
const frontmatter = {};
|
|
27
|
+
for (const line of (frontmatterBlock ?? '').split(/\r?\n/)) {
|
|
28
|
+
const lineMatch = /^([\w.-]+):\s*(.*)$/.exec(line);
|
|
29
|
+
if (lineMatch === null) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const [, key, value] = lineMatch;
|
|
33
|
+
if (key !== undefined) {
|
|
34
|
+
frontmatter[key] = (value ?? '').trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { frontmatter, body: body ?? '' };
|
|
38
|
+
}
|
|
39
|
+
export function promptsRoot(explicitRoot) {
|
|
40
|
+
return explicitRoot ?? join(findProjectRoot(dirname(fileURLToPath(import.meta.url))), 'prompts');
|
|
41
|
+
}
|
|
42
|
+
export async function loadPromptTemplate(stage, mode, options) {
|
|
43
|
+
const root = promptsRoot(options?.promptsRoot);
|
|
44
|
+
const combinedFilePath = join(root, `${stage}.md`);
|
|
45
|
+
const modeFilePath = join(root, `${stage}.${mode}.md`);
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = await readFile(combinedFilePath, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const code = error.code;
|
|
52
|
+
if (code !== 'ENOENT') {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
raw = await readFile(modeFilePath, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
return parseFrontmatter(raw);
|
|
58
|
+
}
|
|
59
|
+
export function renderPromptTemplate(template, context) {
|
|
60
|
+
const renderContext = Object.fromEntries(Object.entries(context).map(([key, value]) => {
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return [key, Object.assign([...value], { toString: () => JSON.stringify(value, null, 2) })];
|
|
63
|
+
}
|
|
64
|
+
if (value !== null && typeof value === 'object') {
|
|
65
|
+
return [
|
|
66
|
+
key,
|
|
67
|
+
Object.assign({ ...value }, { toString: () => JSON.stringify(value, null, 2) }),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
return [key, value];
|
|
71
|
+
}));
|
|
72
|
+
const compiled = Handlebars.compile(template.body, {
|
|
73
|
+
noEscape: true,
|
|
74
|
+
});
|
|
75
|
+
return compiled(renderContext);
|
|
76
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createClaudeRunner } from '../claude/claude-runner.js';
|
|
2
|
+
import { createCodexRunner } from '../codex/codex-runner.js';
|
|
3
|
+
import { createCursorRunner } from '../cursor/cursor-runner.js';
|
|
4
|
+
function withoutKind(entry) {
|
|
5
|
+
const { kind: _kind, ...settings } = entry;
|
|
6
|
+
return settings;
|
|
7
|
+
}
|
|
8
|
+
export function createRunnerCliAdapter(input) {
|
|
9
|
+
if (input.entry.kind === 'claude') {
|
|
10
|
+
const settings = withoutKind(input.entry);
|
|
11
|
+
const runner = createClaudeRunner({
|
|
12
|
+
command: settings.command,
|
|
13
|
+
cwd: input.cwd,
|
|
14
|
+
settings,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
mode: 'claude',
|
|
18
|
+
cliName: 'Claude',
|
|
19
|
+
runner,
|
|
20
|
+
async smoke(args) {
|
|
21
|
+
if (args.includes('--remote-control')) {
|
|
22
|
+
const result = await runner.startRemoteControlSmoke();
|
|
23
|
+
return {
|
|
24
|
+
mode: 'remote-control',
|
|
25
|
+
exitCode: result.exitCode,
|
|
26
|
+
stdout: result.stdout.trim(),
|
|
27
|
+
stderr: result.stderr.trim(),
|
|
28
|
+
command: result.command,
|
|
29
|
+
args: result.args,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const result = await runner.smoke();
|
|
33
|
+
return {
|
|
34
|
+
mode: 'print-json',
|
|
35
|
+
exitCode: result.exitCode,
|
|
36
|
+
text: result.text,
|
|
37
|
+
sessionId: result.sessionId,
|
|
38
|
+
stdout: result.stdout.trim(),
|
|
39
|
+
stderr: result.stderr.trim(),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
buildResumeCommand({ sessionId }) {
|
|
43
|
+
return ['claude', '--resume', sessionId];
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (input.entry.kind === 'cursor') {
|
|
48
|
+
const settings = withoutKind(input.entry);
|
|
49
|
+
const runner = createCursorRunner({
|
|
50
|
+
command: settings.command,
|
|
51
|
+
cwd: input.cwd,
|
|
52
|
+
settings,
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
mode: 'cursor',
|
|
56
|
+
cliName: 'Cursor',
|
|
57
|
+
runner,
|
|
58
|
+
async smoke() {
|
|
59
|
+
const result = await runner.smoke();
|
|
60
|
+
return {
|
|
61
|
+
mode: 'print-json',
|
|
62
|
+
exitCode: result.exitCode,
|
|
63
|
+
text: result.text,
|
|
64
|
+
sessionId: result.sessionId,
|
|
65
|
+
stdout: result.stdout.trim(),
|
|
66
|
+
stderr: result.stderr.trim(),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
buildResumeCommand({ sessionId }) {
|
|
70
|
+
return ['cursor', 'agent', `--resume=${sessionId}`];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const settings = withoutKind(input.entry);
|
|
75
|
+
const runner = createCodexRunner({
|
|
76
|
+
command: settings.command,
|
|
77
|
+
cwd: input.cwd,
|
|
78
|
+
settings,
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
mode: 'codex',
|
|
82
|
+
cliName: 'Codex',
|
|
83
|
+
runner,
|
|
84
|
+
async smoke() {
|
|
85
|
+
const result = await runner.smoke();
|
|
86
|
+
return {
|
|
87
|
+
mode: 'jsonl',
|
|
88
|
+
exitCode: result.exitCode,
|
|
89
|
+
text: result.text,
|
|
90
|
+
sessionId: result.sessionId,
|
|
91
|
+
stdout: result.stdout.trim(),
|
|
92
|
+
stderr: result.stderr.trim(),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
buildResumeCommand({ sessionId }) {
|
|
96
|
+
return ['codex', 'resume', sessionId];
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function buildResumeCommandForCli(input) {
|
|
101
|
+
const normalizedCli = input.cli.trim().toLowerCase();
|
|
102
|
+
if (normalizedCli === 'codex') {
|
|
103
|
+
return ['codex', 'resume', input.sessionId];
|
|
104
|
+
}
|
|
105
|
+
if (normalizedCli === 'claude') {
|
|
106
|
+
return ['claude', '--resume', input.sessionId];
|
|
107
|
+
}
|
|
108
|
+
if (normalizedCli === 'cursor') {
|
|
109
|
+
return ['cursor', 'agent', `--resume=${input.sessionId}`];
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createClaudeRunner } from '../claude/claude-runner.js';
|
|
2
|
+
import { createCodexRunner } from '../codex/codex-runner.js';
|
|
3
|
+
import { createCursorRunner } from '../cursor/cursor-runner.js';
|
|
4
|
+
import { createFakeRunner } from '../fake/fake-runner.js';
|
|
5
|
+
import { resolveRunnerRouting } from '../../domain/runner-routing.js';
|
|
6
|
+
import { chooseAction, workflowForProjection, workflowNameForProjection } from '../../domain/workflows.js';
|
|
7
|
+
export { resolveRunnerRouting } from '../../domain/runner-routing.js';
|
|
8
|
+
function withoutKind(entry) {
|
|
9
|
+
const { kind: _kind, ...settings } = entry;
|
|
10
|
+
return settings;
|
|
11
|
+
}
|
|
12
|
+
function createRunnerForEntry(input) {
|
|
13
|
+
if (input.entry.kind === 'fake') {
|
|
14
|
+
return createFakeRunner(undefined, { cli: input.entry.cli });
|
|
15
|
+
}
|
|
16
|
+
if (input.entry.kind === 'claude') {
|
|
17
|
+
const settings = withoutKind(input.entry);
|
|
18
|
+
return createClaudeRunner({
|
|
19
|
+
command: settings.command,
|
|
20
|
+
cwd: input.cwd,
|
|
21
|
+
settings,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (input.entry.kind === 'cursor') {
|
|
25
|
+
const settings = withoutKind(input.entry);
|
|
26
|
+
return createCursorRunner({
|
|
27
|
+
command: settings.command,
|
|
28
|
+
cwd: input.cwd,
|
|
29
|
+
settings,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const settings = withoutKind(input.entry);
|
|
33
|
+
return createCodexRunner({
|
|
34
|
+
command: settings.command,
|
|
35
|
+
cwd: input.cwd,
|
|
36
|
+
settings,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function runWithRouting(input) {
|
|
40
|
+
const result = await input.runner.run({
|
|
41
|
+
...input.runInput,
|
|
42
|
+
routing: input.routing,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
...result,
|
|
46
|
+
routing: input.routing,
|
|
47
|
+
metadata: {
|
|
48
|
+
...result.metadata,
|
|
49
|
+
routing: input.routing,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function createRegistryRunner(input) {
|
|
54
|
+
if (input.override === 'fake') {
|
|
55
|
+
const runner = createFakeRunner();
|
|
56
|
+
return {
|
|
57
|
+
run(runInput) {
|
|
58
|
+
return runWithRouting({
|
|
59
|
+
runner,
|
|
60
|
+
runInput,
|
|
61
|
+
routing: {
|
|
62
|
+
runnerName: 'fake',
|
|
63
|
+
runnerKind: 'fake',
|
|
64
|
+
reason: '--runner fake override',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (input.override !== undefined) {
|
|
71
|
+
const entry = input.config.runners[input.override];
|
|
72
|
+
if (entry === undefined) {
|
|
73
|
+
throw new Error(`Unsupported runner override: ${input.override}`);
|
|
74
|
+
}
|
|
75
|
+
const runner = createRunnerForEntry({
|
|
76
|
+
name: input.override,
|
|
77
|
+
entry,
|
|
78
|
+
config: input.config,
|
|
79
|
+
cwd: input.cwd,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
run(runInput) {
|
|
83
|
+
return runWithRouting({
|
|
84
|
+
runner,
|
|
85
|
+
runInput,
|
|
86
|
+
routing: {
|
|
87
|
+
runnerName: input.override,
|
|
88
|
+
runnerKind: entry.kind,
|
|
89
|
+
reason: `--runner ${input.override} override`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const cache = new Map();
|
|
96
|
+
return {
|
|
97
|
+
async run(runInput) {
|
|
98
|
+
// Callers that already resolved routing against the ledger (tick-runner,
|
|
99
|
+
// so it can skip claiming a run when every candidate is paused) pass it
|
|
100
|
+
// in; only fall back to a ledger-less resolve for direct callers (smoke
|
|
101
|
+
// tests, sandbox exec) that never see quota state.
|
|
102
|
+
const routing = runInput.routing ??
|
|
103
|
+
(() => {
|
|
104
|
+
const workflow = workflowForProjection(runInput.projection, runInput.config);
|
|
105
|
+
const workflowAction = workflow === null
|
|
106
|
+
? null
|
|
107
|
+
: chooseAction(runInput.projection, workflow);
|
|
108
|
+
return resolveRunnerRouting({
|
|
109
|
+
config: runInput.config,
|
|
110
|
+
stage: workflowAction?.stage ?? runInput.projection.wake.stage,
|
|
111
|
+
action: runInput.action,
|
|
112
|
+
workflowName: workflowNameForProjection(runInput.projection, runInput.config),
|
|
113
|
+
});
|
|
114
|
+
})();
|
|
115
|
+
if (routing === null) {
|
|
116
|
+
throw new Error('No runner available: every candidate in the resolved tier is quota-paused.');
|
|
117
|
+
}
|
|
118
|
+
const cacheKey = `${routing.runnerKind}:${routing.runnerName}`;
|
|
119
|
+
const existing = cache.get(cacheKey);
|
|
120
|
+
const runner = existing ??
|
|
121
|
+
createRunnerForEntry({
|
|
122
|
+
name: routing.runnerName,
|
|
123
|
+
entry: runInput.config.runners[routing.runnerName],
|
|
124
|
+
config: runInput.config,
|
|
125
|
+
cwd: input.cwd,
|
|
126
|
+
});
|
|
127
|
+
cache.set(cacheKey, runner);
|
|
128
|
+
return runWithRouting({
|
|
129
|
+
runner,
|
|
130
|
+
routing,
|
|
131
|
+
runInput,
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function runnerKindForOverride(config, override) {
|
|
137
|
+
if (override === 'fake') {
|
|
138
|
+
return 'fake';
|
|
139
|
+
}
|
|
140
|
+
return config.runners[override]?.kind ?? null;
|
|
141
|
+
}
|