@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,173 @@
1
+ import { chmod, copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { createDefaultWakeConfig } from '../config/defaults.js';
4
+ import { writeJsonFile } from '../lib/json-file.js';
5
+ const runtimeDirectoryNames = [
6
+ 'events',
7
+ 'state',
8
+ 'runs',
9
+ 'workspaces',
10
+ 'repos',
11
+ 'sources',
12
+ 'locks',
13
+ 'logs',
14
+ ];
15
+ const promptFileNames = ['refine.md', 'implement.md'];
16
+ const dockerAssetNames = ['Dockerfile', 'setup.sh', 'log-command.sh'];
17
+ export async function assertEmptyDirectory(targetDir) {
18
+ try {
19
+ const entries = await readdir(targetDir);
20
+ if (entries.length > 0) {
21
+ throw new Error(`wake init requires an empty directory: ${targetDir}`);
22
+ }
23
+ }
24
+ catch (error) {
25
+ const code = error.code;
26
+ if (code === 'ENOENT') {
27
+ await mkdir(targetDir, { recursive: true });
28
+ return;
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ async function copyAssets(repoRoot, sourceDir, targetDir, fileNames) {
34
+ await mkdir(targetDir, { recursive: true });
35
+ await Promise.all(fileNames.map(async (fileName) => {
36
+ const sourceFile = join(repoRoot, sourceDir, fileName);
37
+ const targetFile = join(targetDir, fileName);
38
+ await mkdir(dirname(targetFile), { recursive: true });
39
+ if (fileName.endsWith('.sh')) {
40
+ const script = await readFile(sourceFile, 'utf8');
41
+ await writeFile(targetFile, script.replaceAll('\r\n', '\n'), 'utf8');
42
+ await chmod(targetFile, 0o755);
43
+ return;
44
+ }
45
+ await copyFile(sourceFile, targetFile);
46
+ }));
47
+ }
48
+ async function writeLaunchers(wakeRoot, repoRoot) {
49
+ const posixRepoRoot = repoRoot.replaceAll('\\', '/');
50
+ const shellLauncher = [
51
+ '#!/usr/bin/env bash',
52
+ 'set -euo pipefail',
53
+ '',
54
+ `WAKE_REPO="${posixRepoRoot}"`,
55
+ 'DIST_MAIN="$WAKE_REPO/dist/src/main.js"',
56
+ 'SOURCE_MAIN="$WAKE_REPO/src/main.ts"',
57
+ 'CONTAINER_MAIN="/app/dist/src/main.js"',
58
+ '',
59
+ 'run_local_wake() {',
60
+ ' if [[ -f "$DIST_MAIN" ]]; then',
61
+ ' exec env MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL=\'*\' node "$DIST_MAIN" "$@"',
62
+ ' fi',
63
+ ' exec env MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL=\'*\' npx tsx "$SOURCE_MAIN" "$@"',
64
+ '}',
65
+ '',
66
+ 'rewrite_runtime_args() {',
67
+ ' local rewritten_args=()',
68
+ ' local saw_wake_root=0',
69
+ ' while (($# > 0)); do',
70
+ ' if [[ "$1" == "--wake-root" ]]; then',
71
+ ' saw_wake_root=1',
72
+ ' shift',
73
+ ' if (($# > 0)); then',
74
+ ' shift',
75
+ ' fi',
76
+ ' rewritten_args+=("--wake-root" "/wake")',
77
+ ' continue',
78
+ ' fi',
79
+ ' rewritten_args+=("$1")',
80
+ ' shift',
81
+ ' done',
82
+ ' if [[ $saw_wake_root -eq 0 ]]; then',
83
+ ' rewritten_args+=("--wake-root" "/wake")',
84
+ ' fi',
85
+ ' printf \'%s\\0\' "${rewritten_args[@]}"',
86
+ '}',
87
+ '',
88
+ 'case "${1:-}" in',
89
+ ' init|sandbox|stop)',
90
+ ' run_local_wake "$@"',
91
+ ' ;;',
92
+ ' *)',
93
+ ' mapfile -d \'\' -t rewritten_args < <(rewrite_runtime_args "$@")',
94
+ ' run_local_wake sandbox exec -- node "$CONTAINER_MAIN" "${rewritten_args[@]}"',
95
+ ' ;;',
96
+ 'esac',
97
+ '',
98
+ ].join('\n');
99
+ const powerShellLauncher = [
100
+ '$ErrorActionPreference = "Stop"',
101
+ '',
102
+ `$wakeRepo = "${repoRoot.replaceAll('"', '""')}"`,
103
+ '$distMain = Join-Path $wakeRepo "dist/src/main.js"',
104
+ '$sourceMain = Join-Path $wakeRepo "src/main.ts"',
105
+ '$containerMain = "/app/dist/src/main.js"',
106
+ '$command = if ($Args.Count -gt 0) { [string] $Args[0] } else { "" }',
107
+ '',
108
+ 'function Invoke-WakeLocal {',
109
+ ' param([string[]] $WakeArgs)',
110
+ ' if (Test-Path $distMain) {',
111
+ ' & node $distMain @WakeArgs',
112
+ ' exit $LASTEXITCODE',
113
+ ' }',
114
+ ' & npx tsx $sourceMain @WakeArgs',
115
+ ' exit $LASTEXITCODE',
116
+ '}',
117
+ '',
118
+ 'switch ($command) {',
119
+ ' "init" {',
120
+ ' Invoke-WakeLocal -WakeArgs $Args',
121
+ ' }',
122
+ ' "sandbox" {',
123
+ ' Invoke-WakeLocal -WakeArgs $Args',
124
+ ' }',
125
+ ' "stop" {',
126
+ ' Invoke-WakeLocal -WakeArgs $Args',
127
+ ' }',
128
+ ' default {',
129
+ ' $rewrittenArgs = New-Object System.Collections.Generic.List[string]',
130
+ ' $sawWakeRoot = $false',
131
+ ' for ($index = 0; $index -lt $Args.Count; $index++) {',
132
+ ' $arg = [string] $Args[$index]',
133
+ ' if ($arg -eq "--wake-root") {',
134
+ ' $sawWakeRoot = $true',
135
+ ' if ($index + 1 -lt $Args.Count) {',
136
+ ' $index++',
137
+ ' }',
138
+ " $rewrittenArgs.Add('--wake-root')",
139
+ " $rewrittenArgs.Add('/wake')",
140
+ ' continue',
141
+ ' }',
142
+ ' $rewrittenArgs.Add($arg)',
143
+ ' }',
144
+ ' if (-not $sawWakeRoot) {',
145
+ " $rewrittenArgs.Add('--wake-root')",
146
+ " $rewrittenArgs.Add('/wake')",
147
+ ' }',
148
+ " Invoke-WakeLocal -WakeArgs (@('sandbox', 'exec', '--', 'node', $containerMain) + $rewrittenArgs.ToArray())",
149
+ ' }',
150
+ '}',
151
+ '',
152
+ ].join('\n');
153
+ await writeFile(join(wakeRoot, 'wake.sh'), shellLauncher, 'utf8');
154
+ await writeFile(join(wakeRoot, 'wake.ps1'), powerShellLauncher, 'utf8');
155
+ await chmod(join(wakeRoot, 'wake.sh'), 0o755);
156
+ }
157
+ export async function scaffoldWakeHome(input) {
158
+ const wakeRoot = resolve(input.wakeRoot);
159
+ const repoRoot = resolve(input.repoRoot);
160
+ const config = {
161
+ ...createDefaultWakeConfig(wakeRoot),
162
+ dev: {
163
+ repoRoot,
164
+ },
165
+ };
166
+ await Promise.all(runtimeDirectoryNames.map((directoryName) => mkdir(join(wakeRoot, directoryName), { recursive: true })));
167
+ await Promise.all([
168
+ copyAssets(repoRoot, 'prompts', join(wakeRoot, 'prompts'), promptFileNames),
169
+ copyAssets(repoRoot, 'docker', join(wakeRoot, 'docker'), dockerAssetNames),
170
+ writeJsonFile(join(wakeRoot, 'config.json'), config),
171
+ writeLaunchers(wakeRoot, repoRoot),
172
+ ]);
173
+ }
@@ -0,0 +1,158 @@
1
+ import { waitForActiveRuns } from './stop-command.js';
2
+ const HEALTHCHECK_WAKE_ROOT = '/tmp/wake-self-update-healthcheck';
3
+ const START_PROCESS_CHECK = [
4
+ 'sh',
5
+ '-lc',
6
+ [
7
+ 'pid="$(cat /wake/logs/start.pid)"',
8
+ 'test -n "$pid"',
9
+ 'kill -0 "$pid"',
10
+ 'tr "\\0" " " < "/proc/$pid/cmdline" | grep -F "node /app/dist/src/main.js start --wake-root /wake" >/dev/null',
11
+ ].join(' && '),
12
+ ];
13
+ function readFlag(name, args) {
14
+ const index = args.indexOf(name);
15
+ return index === -1 ? undefined : args[index + 1];
16
+ }
17
+ function hasFlag(name, args) {
18
+ return args.includes(name);
19
+ }
20
+ function readNumberFlag(name, args) {
21
+ const raw = readFlag(name, args);
22
+ const parsed = raw === undefined ? Number.NaN : Number(raw);
23
+ return Number.isFinite(parsed) ? parsed : undefined;
24
+ }
25
+ const DEFAULT_LOOP_INTERVAL_MS = 5 * 60 * 1000;
26
+ async function verifyResidentStart(input) {
27
+ if (input.start?.enabled !== true) {
28
+ input.logger.error(`[self-update] wake start auto-start is disabled; resident loop will not be running after ${input.context}`);
29
+ return;
30
+ }
31
+ await input.docker.exec(input.containerName, START_PROCESS_CHECK);
32
+ input.logger.info(`[self-update] verified wake start is running after ${input.context}`);
33
+ }
34
+ export async function runSelfUpdateCommand(input) {
35
+ const force = hasFlag('--force', input.args);
36
+ const explicitTag = readFlag('--tag', input.args);
37
+ const ledger = await input.readLedger();
38
+ const tag = explicitTag ?? (await input.git.latestTag());
39
+ if (!force && tag === ledger.lastAppliedTag) {
40
+ input.logger.info(`[self-update] already on ${tag}; nothing to do`);
41
+ return;
42
+ }
43
+ if (!force && ledger.badTags.some((bad) => bad.tag === tag)) {
44
+ input.logger.info(`[self-update] ${tag} is recorded as a bad tag; skipping (use --force to retry)`);
45
+ return;
46
+ }
47
+ if (!(await input.git.isWorkingTreeClean())) {
48
+ throw new Error(`[self-update] repo working tree has local changes; refusing to update to ${tag}`);
49
+ }
50
+ await waitForActiveRuns({
51
+ listRunRecords: input.stateStore.listRunRecords,
52
+ sleep: input.sleep,
53
+ logger: input.logger,
54
+ });
55
+ const newImage = `${input.imageRepository}:${tag}`;
56
+ const updateInput = {
57
+ containerName: input.containerName,
58
+ wakeRoot: input.wakeRoot,
59
+ containerHomeRoot: input.containerHomeRoot,
60
+ containerMountPath: input.containerMountPath,
61
+ containerHomeMountPath: input.containerHomeMountPath,
62
+ ui: input.ui,
63
+ start: input.start,
64
+ };
65
+ try {
66
+ await input.git.checkoutTag(tag);
67
+ await input.docker.build({
68
+ image: newImage,
69
+ dockerfile: input.dockerfilePath,
70
+ contextDir: input.repoRoot,
71
+ });
72
+ await input.docker.update({ ...updateInput, image: newImage });
73
+ input.logger.info('[self-update] recreated container; entrypoint will keep wake start running');
74
+ await verifyResidentStart({
75
+ docker: input.docker,
76
+ containerName: input.containerName,
77
+ start: input.start,
78
+ logger: input.logger,
79
+ context: 'rollout',
80
+ });
81
+ await input.docker.exec(input.containerName, [
82
+ 'node',
83
+ '/app/dist/src/main.js',
84
+ 'tick',
85
+ '--wake-root',
86
+ HEALTHCHECK_WAKE_ROOT,
87
+ ]);
88
+ }
89
+ catch (error) {
90
+ const reason = error instanceof Error ? error.message : String(error);
91
+ input.logger.error(`[self-update] rollout of ${tag} failed: ${reason}`);
92
+ if (ledger.lastKnownGoodTag !== null) {
93
+ const rollbackImage = `${input.imageRepository}:${ledger.lastKnownGoodTag}`;
94
+ await input.git.checkoutTag(ledger.lastKnownGoodTag);
95
+ await input.docker.update({ ...updateInput, image: rollbackImage });
96
+ input.logger.info('[self-update] recreated rollback container; entrypoint will keep wake start running');
97
+ await verifyResidentStart({
98
+ docker: input.docker,
99
+ containerName: input.containerName,
100
+ start: input.start,
101
+ logger: input.logger,
102
+ context: 'rollback',
103
+ });
104
+ input.logger.info(`[self-update] rolled back to ${ledger.lastKnownGoodTag}`);
105
+ }
106
+ else {
107
+ input.logger.error('[self-update] no previous known-good tag to roll back to');
108
+ }
109
+ await input.writeLedger({
110
+ lastAppliedTag: ledger.lastKnownGoodTag,
111
+ lastKnownGoodTag: ledger.lastKnownGoodTag,
112
+ badTags: [...ledger.badTags, { tag, reason, recordedAt: new Date().toISOString() }],
113
+ });
114
+ try {
115
+ await input.issueReporter.createIssue({
116
+ title: `Self-update to ${tag} failed and was rolled back`,
117
+ body: [
118
+ `Automated update to \`${tag}\` failed during rollout and was rolled back to \`${ledger.lastKnownGoodTag ?? 'unknown'}\`.`,
119
+ '',
120
+ '```',
121
+ reason,
122
+ '```',
123
+ ].join('\n'),
124
+ });
125
+ }
126
+ catch (issueError) {
127
+ // Rollback already succeeded above; a failure to file the notification
128
+ // issue must not be reported as an overall self-update failure.
129
+ input.logger.error(`[self-update] rolled back successfully, but failed to file a GitHub issue: ${issueError instanceof Error ? issueError.message : String(issueError)}`);
130
+ }
131
+ return;
132
+ }
133
+ await input.writeLedger({
134
+ lastAppliedTag: tag,
135
+ lastKnownGoodTag: tag,
136
+ badTags: ledger.badTags,
137
+ });
138
+ input.logger.info(`[self-update] ${tag} is live and healthy`);
139
+ }
140
+ // Runs runSelfUpdateCommand forever, polling for a new tag every
141
+ // --loop-interval-ms (default 5 minutes). One failed iteration (e.g. a
142
+ // transient git/docker error) is logged and does not stop the loop — the
143
+ // next iteration retries. Exits only if `sleep` itself rejects (e.g. the
144
+ // process is being torn down), matching the resident-loop shape in
145
+ // core/control-plane.ts.
146
+ export async function runSelfUpdateLoop(input) {
147
+ const loopIntervalMs = readNumberFlag('--loop-interval-ms', input.args) ?? DEFAULT_LOOP_INTERVAL_MS;
148
+ for (;;) {
149
+ try {
150
+ await runSelfUpdateCommand(input);
151
+ }
152
+ catch (error) {
153
+ input.logger.error(`[self-update] loop iteration failed: ${error instanceof Error ? error.message : String(error)}`);
154
+ }
155
+ input.logger.info(`[self-update] next check in ${loopIntervalMs}ms`);
156
+ await input.sleep(loopIntervalMs);
157
+ }
158
+ }
@@ -0,0 +1,127 @@
1
+ import { execFile as nodeExecFile } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import { promisify } from 'node:util';
4
+ import { loadPromptTemplate } from '../adapters/runner/prompt-templates.js';
5
+ const execFile = promisify(nodeExecFile);
6
+ // Some CLIs (observed with Cursor's `agent --version`) take 5-6s to exit
7
+ // when stdout isn't a TTY, likely due to a background update/telemetry
8
+ // check — keep this comfortably above that or the probe kills a CLI that
9
+ // is actually fine.
10
+ const runnerVersionProbeTimeoutMs = 20_000;
11
+ function activeRunnerNames(config, runnerOverride) {
12
+ const names = new Set();
13
+ if (runnerOverride !== undefined) {
14
+ names.add(runnerOverride);
15
+ return names;
16
+ }
17
+ for (const candidates of Object.values(config.tiers)) {
18
+ for (const candidate of candidates) {
19
+ names.add(candidate);
20
+ }
21
+ }
22
+ for (const workflow of Object.values(config.workflows)) {
23
+ for (const route of Object.values(workflow.stages)) {
24
+ if (route.runner !== undefined) {
25
+ names.add(route.runner);
26
+ }
27
+ }
28
+ }
29
+ return names;
30
+ }
31
+ function formatPreflightFailures(failures) {
32
+ return new Error([
33
+ 'Wake startup preflight failed:',
34
+ ...failures.map((failure) => `- ${failure}`),
35
+ '',
36
+ 'Fix the configuration or environment, then run `wake start` again.',
37
+ ].join('\n'));
38
+ }
39
+ async function defaultLoadPrompt(action, mode, promptsRoot) {
40
+ await loadPromptTemplate(action, mode, {
41
+ ...(promptsRoot === undefined ? {} : { promptsRoot }),
42
+ });
43
+ }
44
+ async function defaultCheckRunnerCommand(runnerName, entry) {
45
+ try {
46
+ await execFile(entry.command, ['--version'], {
47
+ env: process.env,
48
+ timeout: runnerVersionProbeTimeoutMs,
49
+ windowsHide: true,
50
+ maxBuffer: 1024 * 1024,
51
+ });
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ throw new Error(`runner "${runnerName}" (${entry.kind}) command "${entry.command}" is not invocable: ${message}`);
56
+ }
57
+ }
58
+ async function assertPromptsRootAccessible(promptsRoot) {
59
+ if (promptsRoot === undefined) {
60
+ return;
61
+ }
62
+ await access(promptsRoot);
63
+ }
64
+ export async function runStartupPreflight(config, deps = {}) {
65
+ const failures = [];
66
+ const loadPrompt = deps.loadPrompt ?? defaultLoadPrompt;
67
+ const checkRunnerCommand = deps.checkRunnerCommand ?? defaultCheckRunnerCommand;
68
+ try {
69
+ await assertPromptsRootAccessible(config.paths.promptsRoot);
70
+ }
71
+ catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ failures.push(`promptsRoot "${config.paths.promptsRoot}" is not readable: ${message}`);
74
+ }
75
+ const actions = new Set(Object.values(config.workflows).flatMap((workflow) => Object.entries(workflow.stages).map(([stageName, stage]) => stage.action ?? stageName)));
76
+ for (const action of actions) {
77
+ for (const mode of ['start', 'resume']) {
78
+ try {
79
+ await loadPrompt(action, mode, config.paths.promptsRoot);
80
+ }
81
+ catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ const root = config.paths.promptsRoot ?? '(bundled prompts)';
84
+ failures.push(`prompt template ${action}.md or ${action}.${mode}.md under ${root} is not readable: ${message}`);
85
+ }
86
+ }
87
+ }
88
+ const activeRunners = activeRunnerNames(config, deps.runnerOverride);
89
+ let usesRealRunner = false;
90
+ for (const runnerName of activeRunners) {
91
+ const entry = config.runners[runnerName];
92
+ if (entry === undefined) {
93
+ failures.push(`routing references unknown runner "${runnerName}"`);
94
+ continue;
95
+ }
96
+ if (entry.kind === 'fake') {
97
+ continue;
98
+ }
99
+ usesRealRunner = true;
100
+ try {
101
+ await checkRunnerCommand(runnerName, entry);
102
+ }
103
+ catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ failures.push(message);
106
+ }
107
+ }
108
+ if (usesRealRunner && config.sources.github.enabled && config.sources.github.repos.length > 0) {
109
+ if (deps.workspaceManager === undefined) {
110
+ failures.push('canonical clone health could not be checked: no workspace manager was provided');
111
+ }
112
+ else {
113
+ for (const repo of config.sources.github.repos) {
114
+ try {
115
+ await deps.workspaceManager.prepareReadOnlyClone({ repo });
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ failures.push(`canonical clone for ${repo} is not healthy or clone-able: ${message}`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (failures.length > 0) {
125
+ throw formatPreflightFailures(failures);
126
+ }
127
+ }
@@ -0,0 +1,42 @@
1
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
2
+ const STOP_GRACE_PERIOD_SECONDS = 60;
3
+ export async function waitForActiveRuns(input) {
4
+ const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
5
+ const startedAt = Date.now();
6
+ for (;;) {
7
+ const records = await input.listRunRecords();
8
+ const activeRuns = records.filter((record) => record.status === 'running');
9
+ if (activeRuns.length === 0) {
10
+ return;
11
+ }
12
+ if (input.timeoutMs !== undefined && Date.now() - startedAt >= input.timeoutMs) {
13
+ throw new Error(`Timed out after ${input.timeoutMs}ms waiting for active runs to finish: ${activeRuns
14
+ .map((record) => record.runId)
15
+ .join(', ')}`);
16
+ }
17
+ input.logger.info(`[wake stop] waiting for ${activeRuns.length} active run(s) to finish: ${activeRuns
18
+ .map((record) => record.runId)
19
+ .join(', ')}`);
20
+ await input.sleep(pollIntervalMs);
21
+ }
22
+ }
23
+ function readNumberFlag(name, args) {
24
+ const index = args.indexOf(name);
25
+ if (index === -1) {
26
+ return undefined;
27
+ }
28
+ const raw = args[index + 1];
29
+ const parsed = raw === undefined ? Number.NaN : Number(raw);
30
+ return Number.isFinite(parsed) ? parsed : undefined;
31
+ }
32
+ export async function runStopCommand(input) {
33
+ await waitForActiveRuns({
34
+ listRunRecords: input.stateStore.listRunRecords,
35
+ sleep: input.sleep,
36
+ pollIntervalMs: readNumberFlag('--poll-interval-ms', input.args),
37
+ timeoutMs: readNumberFlag('--timeout-ms', input.args),
38
+ logger: input.logger,
39
+ });
40
+ input.logger.info(`[wake stop] no active runs; stopping ${input.containerName}`);
41
+ await input.docker.down(input.containerName, { timeoutSeconds: STOP_GRACE_PERIOD_SECONDS });
42
+ }
@@ -0,0 +1,27 @@
1
+ import { createUiServer } from '../adapters/http/ui-server.js';
2
+ export async function runUiCommand(input) {
3
+ const log = input.log ?? console.log;
4
+ const portFlag = input.readFlag('--port', input.args);
5
+ const port = portFlag !== undefined ? Number(portFlag) : input.config.ui.port;
6
+ if (!Number.isInteger(port) || port <= 0) {
7
+ throw new Error(`Invalid --port: ${portFlag}`);
8
+ }
9
+ const host = input.readFlag('--host', input.args) ?? '127.0.0.1';
10
+ const token = input.readFlag('--token', input.args) ?? input.config.ui.token ?? process.env.WAKE_UI_TOKEN;
11
+ const server = createUiServer({
12
+ stateStore: input.stateStore,
13
+ resourceIndex: input.resourceIndex,
14
+ config: input.config,
15
+ ...(token === undefined ? {} : { token }),
16
+ });
17
+ await new Promise((resolveListen, reject) => {
18
+ server.once('error', reject);
19
+ server.listen(port, host, () => resolveListen());
20
+ });
21
+ log(`Wake UI listening on http://${host}:${port}`);
22
+ return {
23
+ close: () => new Promise((resolveClose) => {
24
+ server.close(() => resolveClose());
25
+ }),
26
+ };
27
+ }
@@ -0,0 +1,11 @@
1
+ import { resolve } from 'node:path';
2
+ import { defaultAgentIdentity, defaultSmokePrompt, parseWakeConfig } from '../domain/schema.js';
3
+ export { defaultAgentIdentity, defaultSmokePrompt };
4
+ export function createDefaultWakeConfig(wakeRoot = resolve(process.cwd(), '.wake')) {
5
+ return parseWakeConfig({
6
+ paths: {
7
+ wakeRoot,
8
+ promptsRoot: resolve(wakeRoot, 'prompts'),
9
+ },
10
+ });
11
+ }
@@ -0,0 +1,26 @@
1
+ import { resolve } from 'node:path';
2
+ import { access } from 'node:fs/promises';
3
+ import { readJsonFile } from '../lib/json-file.js';
4
+ import { parseWakeConfig } from '../domain/schema.js';
5
+ export async function loadWakeConfig(options) {
6
+ const wakeRoot = options?.wakeRoot ?? resolve(process.cwd(), '.wake');
7
+ const configFile = options?.configFile;
8
+ let raw = {};
9
+ if (configFile !== undefined) {
10
+ try {
11
+ await access(configFile);
12
+ raw = await readJsonFile(configFile);
13
+ }
14
+ catch {
15
+ // no config file — schema defaults apply
16
+ }
17
+ }
18
+ const rawPaths = raw.paths ?? {};
19
+ return parseWakeConfig({
20
+ ...raw,
21
+ paths: {
22
+ wakeRoot,
23
+ ...rawPaths,
24
+ },
25
+ });
26
+ }
@@ -0,0 +1 @@
1
+ export {};