@atolis-hq/wake 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/src/adapters/docker/docker-cli.js +7 -1
- package/dist/src/cli/scaffold-assets.js +1 -0
- package/dist/src/lib/detached-process-logging.js +46 -0
- package/dist/src/lib/log-rotation.js +42 -0
- package/dist/src/main.js +16 -6
- package/dist/src/version.js +1 -1
- package/package.json +2 -1
- package/templates/SETUP.md +115 -0
package/README.md
CHANGED
|
@@ -97,7 +97,8 @@ For more detail, see [docs/vision.md](docs/vision.md) and
|
|
|
97
97
|
log; projections can be rebuilt, and the loop can crash and resume without
|
|
98
98
|
losing its place.
|
|
99
99
|
- **Local and inspectable.** Everything lives in a plain-file Wake home
|
|
100
|
-
directory: `config.yaml`, `config.workflows.yaml`, `prompts/`,
|
|
100
|
+
directory: `config.yaml`, `config.workflows.yaml`, `prompts/`, `SETUP.md`, and
|
|
101
|
+
`workspaces/` at the top level for
|
|
101
102
|
what you edit or browse day-to-day, with durable/internal state (events,
|
|
102
103
|
projections, runs, logs, sandbox auth) nested under a hidden `.wake/`.
|
|
103
104
|
- **Sandbox-oriented execution.** Wake can run from a persistent Docker sandbox
|
|
@@ -150,6 +151,8 @@ npm install -g @atolis-hq/wake
|
|
|
150
151
|
cd ~/
|
|
151
152
|
wake init ./wake-home
|
|
152
153
|
cd ./wake-home
|
|
154
|
+
# point your agent CLI at the scaffolded SETUP.md to finish configuring
|
|
155
|
+
# (e.g. "read SETUP.md and help me configure this")
|
|
153
156
|
wake sandbox build
|
|
154
157
|
wake sandbox up
|
|
155
158
|
wake sandbox setup
|
|
@@ -7,10 +7,16 @@ function buildStopArgs(containerName, timeoutSeconds) {
|
|
|
7
7
|
containerName,
|
|
8
8
|
];
|
|
9
9
|
}
|
|
10
|
+
const DOCKER_LOG_MAX_SIZE = '10m';
|
|
11
|
+
const DOCKER_LOG_MAX_FILE = '3';
|
|
10
12
|
function buildRunArgs(input) {
|
|
11
13
|
return [
|
|
12
14
|
'run',
|
|
13
15
|
'-d',
|
|
16
|
+
'--log-opt',
|
|
17
|
+
`max-size=${DOCKER_LOG_MAX_SIZE}`,
|
|
18
|
+
'--log-opt',
|
|
19
|
+
`max-file=${DOCKER_LOG_MAX_FILE}`,
|
|
14
20
|
'--name',
|
|
15
21
|
input.containerName,
|
|
16
22
|
'-v',
|
|
@@ -21,7 +27,7 @@ function buildRunArgs(input) {
|
|
|
21
27
|
'-v',
|
|
22
28
|
`${mount.source}:${mount.target}${mount.readOnly === true ? ':ro' : ''}`,
|
|
23
29
|
]),
|
|
24
|
-
// Auto-started by
|
|
30
|
+
// Auto-started by wake sandbox-entrypoint; the UI binds 0.0.0.0 inside the
|
|
25
31
|
// container so this published port can reach it (127.0.0.1 inside the
|
|
26
32
|
// container would not be reachable via docker's port-forwarding NAT).
|
|
27
33
|
...(input.ui?.enabled === true
|
|
@@ -93,6 +93,7 @@ export async function scaffoldWakeHome(input) {
|
|
|
93
93
|
const promptFileNames = await listPromptFileNames(repoRoot);
|
|
94
94
|
await Promise.all([
|
|
95
95
|
copyAssets(repoRoot, 'prompts', join(wakeRoot, 'prompts'), promptFileNames),
|
|
96
|
+
copyFile(join(repoRoot, 'templates', 'SETUP.md'), join(wakeRoot, 'SETUP.md')),
|
|
96
97
|
writeYamlFile(join(wakeRoot, 'config.yaml'), infra),
|
|
97
98
|
writeYamlFile(join(wakeRoot, 'config.workflows.yaml'), workflow),
|
|
98
99
|
]);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { DEFAULT_LOG_MAX_BYTES, DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS, rotateLogFileIfNeeded, } from './log-rotation.js';
|
|
3
|
+
export function createDetachedProcessLogSink(logFile, options) {
|
|
4
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_LOG_MAX_BYTES;
|
|
5
|
+
const rotateCheckIntervalMs = options?.rotateCheckIntervalMs ?? DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS;
|
|
6
|
+
const stdout = options?.stdout ?? process.stdout;
|
|
7
|
+
let closed = false;
|
|
8
|
+
let fileStream = openLogFile(logFile, maxBytes);
|
|
9
|
+
const rotateInterval = setInterval(() => {
|
|
10
|
+
if (closed) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const stream = fileStream;
|
|
14
|
+
stream.end(() => {
|
|
15
|
+
if (closed) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
rotateLogFileIfNeeded(logFile, maxBytes);
|
|
19
|
+
fileStream = openLogFile(logFile, maxBytes);
|
|
20
|
+
});
|
|
21
|
+
}, rotateCheckIntervalMs);
|
|
22
|
+
rotateInterval.unref();
|
|
23
|
+
return {
|
|
24
|
+
write: (chunk) => {
|
|
25
|
+
if (closed) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
fileStream.write(chunk);
|
|
29
|
+
stdout.write(chunk);
|
|
30
|
+
},
|
|
31
|
+
close: () => {
|
|
32
|
+
if (closed) {
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
closed = true;
|
|
36
|
+
clearInterval(rotateInterval);
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
fileStream.end(resolve);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function openLogFile(logFile, maxBytes) {
|
|
44
|
+
rotateLogFileIfNeeded(logFile, maxBytes);
|
|
45
|
+
return createWriteStream(logFile, { flags: 'a' });
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { renameSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
export const DEFAULT_LOG_MAX_BYTES = 20 * 1024 * 1024;
|
|
3
|
+
export const DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
4
|
+
export function resolveLogMaxBytes(env = process.env) {
|
|
5
|
+
const raw = env.WAKE_LOG_MAX_BYTES;
|
|
6
|
+
if (raw === undefined || raw.trim() === '') {
|
|
7
|
+
return DEFAULT_LOG_MAX_BYTES;
|
|
8
|
+
}
|
|
9
|
+
const parsed = Number(raw);
|
|
10
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
11
|
+
return DEFAULT_LOG_MAX_BYTES;
|
|
12
|
+
}
|
|
13
|
+
return Math.floor(parsed);
|
|
14
|
+
}
|
|
15
|
+
export function resolveLogRotateCheckIntervalMs(env = process.env) {
|
|
16
|
+
const raw = env.WAKE_LOG_ROTATE_CHECK_INTERVAL_MS;
|
|
17
|
+
if (raw === undefined || raw.trim() === '') {
|
|
18
|
+
return DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS;
|
|
19
|
+
}
|
|
20
|
+
const parsed = Number(raw);
|
|
21
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
22
|
+
return DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS;
|
|
23
|
+
}
|
|
24
|
+
return Math.floor(parsed);
|
|
25
|
+
}
|
|
26
|
+
export function rotateLogFileIfNeeded(logFile, maxBytes = DEFAULT_LOG_MAX_BYTES) {
|
|
27
|
+
try {
|
|
28
|
+
if (statSync(logFile).size < maxBytes) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
const backupFile = `${logFile}.1`;
|
|
39
|
+
rmSync(backupFile, { force: true });
|
|
40
|
+
renameSync(logFile, backupFile);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
package/dist/src/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
4
|
import { access, chmod, copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -32,7 +32,9 @@ import { createControlPlane } from './core/control-plane.js';
|
|
|
32
32
|
import { createOutboundSinkRouter, createWorkSourceFanIn } from './core/sink-router.js';
|
|
33
33
|
import { createTickRunner } from './core/tick-runner.js';
|
|
34
34
|
import { systemClock } from './lib/clock.js';
|
|
35
|
+
import { createDetachedProcessLogSink } from './lib/detached-process-logging.js';
|
|
35
36
|
import { readJsonFile } from './lib/json-file.js';
|
|
37
|
+
import { resolveLogMaxBytes, resolveLogRotateCheckIntervalMs } from './lib/log-rotation.js';
|
|
36
38
|
import { configuredTicketSource } from './domain/sources.js';
|
|
37
39
|
import { wakeVersion } from './version.js';
|
|
38
40
|
function commandArgsBeforeTerminator(args) {
|
|
@@ -243,16 +245,26 @@ function createSandboxEntrypointDeps() {
|
|
|
243
245
|
return {
|
|
244
246
|
env: process.env,
|
|
245
247
|
spawnDetached: (command, args, options) => {
|
|
246
|
-
const
|
|
248
|
+
const logSink = options?.logFile !== undefined
|
|
249
|
+
? createDetachedProcessLogSink(options.logFile, {
|
|
250
|
+
maxBytes: resolveLogMaxBytes(process.env),
|
|
251
|
+
rotateCheckIntervalMs: resolveLogRotateCheckIntervalMs(process.env),
|
|
252
|
+
})
|
|
253
|
+
: undefined;
|
|
247
254
|
const child = spawn(command, args, {
|
|
248
255
|
cwd: process.cwd(),
|
|
249
256
|
env: process.env,
|
|
250
|
-
stdio: ['ignore',
|
|
257
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
251
258
|
detached: true,
|
|
252
259
|
});
|
|
253
260
|
if (typeof child.pid === 'number') {
|
|
254
261
|
children.set(child.pid, child);
|
|
255
262
|
}
|
|
263
|
+
const forwardOutput = (chunk) => {
|
|
264
|
+
logSink?.write(chunk);
|
|
265
|
+
};
|
|
266
|
+
child.stdout?.on('data', forwardOutput);
|
|
267
|
+
child.stderr?.on('data', forwardOutput);
|
|
256
268
|
// Registered here (at spawn time) so it runs before any exit listener
|
|
257
269
|
// waitForExit attaches later — though it wouldn't matter either way:
|
|
258
270
|
// waitForExit captures the ChildProcess reference synchronously from
|
|
@@ -264,9 +276,7 @@ function createSandboxEntrypointDeps() {
|
|
|
264
276
|
if (typeof child.pid === 'number') {
|
|
265
277
|
children.delete(child.pid);
|
|
266
278
|
}
|
|
267
|
-
|
|
268
|
-
closeSync(logFd);
|
|
269
|
-
}
|
|
279
|
+
logSink?.close().catch(() => { });
|
|
270
280
|
});
|
|
271
281
|
return { pid: child.pid ?? -1 };
|
|
272
282
|
},
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const wakeVersion = "0.2.
|
|
1
|
+
export const wakeVersion = "0.2.7+gd91da24";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolis-hq/wake",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Local autonomous agent control plane for software development",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"dist/src",
|
|
18
18
|
"docker",
|
|
19
19
|
"prompts",
|
|
20
|
+
"templates",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Wake Setup Guide (for the assisting agent)
|
|
2
|
+
|
|
3
|
+
You are reading this because a human just ran `wake init` and asked you to
|
|
4
|
+
help finish configuring this Wake home. This file is written as instructions
|
|
5
|
+
to you, the assisting agent — not as prose for a human to read top to bottom.
|
|
6
|
+
|
|
7
|
+
Read `config.yaml` and `config.workflows.yaml` in this directory now — both
|
|
8
|
+
already exist with working defaults from `wake init`. Everything below tells
|
|
9
|
+
you which fields in those two files to change. Edit them directly; don't
|
|
10
|
+
create a new `config.<label>.yaml` split unless the user asks for one.
|
|
11
|
+
|
|
12
|
+
Work through the three sections below in order, asking the user only what's
|
|
13
|
+
asked in each section. Each section links the relevant part of the hosted
|
|
14
|
+
`docs/configuration.md` for full field-by-field reference beyond what's
|
|
15
|
+
summarized here.
|
|
16
|
+
|
|
17
|
+
## 1. GitHub source
|
|
18
|
+
|
|
19
|
+
Ask the user:
|
|
20
|
+
|
|
21
|
+
- Which GitHub repo(s) should Wake monitor for issues? (`owner/repo` format)
|
|
22
|
+
- Should polling start immediately, or stay off until they're ready?
|
|
23
|
+
|
|
24
|
+
Edit in `config.yaml`:
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
sources:
|
|
28
|
+
github:
|
|
29
|
+
enabled: true # or leave false to configure now, enable later
|
|
30
|
+
repos: [owner/repo] # one or more, owner/repo format
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Tell the user: once enabled, Wake adds/removes a
|
|
34
|
+
`wake:status.pending|working|failed|completed` label on issues it works, and
|
|
35
|
+
preserves any other labels already on the issue.
|
|
36
|
+
|
|
37
|
+
Full reference:
|
|
38
|
+
https://github.com/atolis-hq/wake/blob/main/docs/configuration.md#sourcesgithub
|
|
39
|
+
|
|
40
|
+
## 2. Runner and tier
|
|
41
|
+
|
|
42
|
+
Ask the user which agent CLI(s) they have authenticated on this host:
|
|
43
|
+
Claude, Codex, and/or Cursor.
|
|
44
|
+
|
|
45
|
+
`config.workflows.yaml` already has example `runners` entries for
|
|
46
|
+
`claude-haiku`, `claude-opus`, `codex-mini`, `codex-flagship`, and
|
|
47
|
+
`cursor-composer`, but every tier (`light`/`standard`/`deep`, with
|
|
48
|
+
`defaultTier: standard`) still points at the placeholder `fake` runner — none
|
|
49
|
+
of them route to a real runner yet. Don't rewrite this from scratch — pick
|
|
50
|
+
which runner(s) the user actually has access to, and either:
|
|
51
|
+
|
|
52
|
+
- repoint `tiers` so each tier lists the real named runner(s) the user can
|
|
53
|
+
actually use instead of `fake`, or
|
|
54
|
+
- if the user has a runner not already listed (a different model, a
|
|
55
|
+
different CLI), add a new named entry under `runners` following the
|
|
56
|
+
existing pattern, then reference it from `tiers`.
|
|
57
|
+
- remove entries which are not needed.
|
|
58
|
+
|
|
59
|
+
Full reference:
|
|
60
|
+
https://github.com/atolis-hq/wake/blob/main/docs/configuration.md#runners
|
|
61
|
+
and
|
|
62
|
+
https://github.com/atolis-hq/wake/blob/main/docs/configuration.md#tiers
|
|
63
|
+
|
|
64
|
+
## 3. Credential mounts (check before asking)
|
|
65
|
+
|
|
66
|
+
Do not start by asking the user where their credentials are. First check the
|
|
67
|
+
host filesystem yourself for the files below, matching whichever runner(s)
|
|
68
|
+
were chosen in step 2:
|
|
69
|
+
|
|
70
|
+
- Claude: `~/.claude/.credentials.json` and `~/.claude/settings.json`
|
|
71
|
+
- Codex: `~/.codex/config.toml` and `~/.codex/auth.json`
|
|
72
|
+
- Cursor: `~/.config/cursor/auth.json`
|
|
73
|
+
|
|
74
|
+
For each file that exists, propose adding it to `sandbox.extraMounts` in
|
|
75
|
+
`config.yaml`, for example:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
sandbox:
|
|
79
|
+
extraMounts:
|
|
80
|
+
- source: /home/alice/.claude/.credentials.json
|
|
81
|
+
target: /home/wake/.claude/.credentials.json
|
|
82
|
+
readOnly: true
|
|
83
|
+
- source: /home/alice/.claude/settings.json
|
|
84
|
+
target: /home/wake/.claude/settings.json
|
|
85
|
+
readOnly: false
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`.credentials.json`/`auth.json` should be `readOnly: true` unless the user
|
|
89
|
+
wants the sandbox able to refresh tokens on the host's behalf. `settings.json`
|
|
90
|
+
must stay `readOnly: false` — Claude plugin commands write to it. Use the
|
|
91
|
+
actual host home directory path (resolve `~` yourself; don't write a literal
|
|
92
|
+
tilde into YAML).
|
|
93
|
+
|
|
94
|
+
Never mount the whole `~/.claude`, `~/.codex`, or `~/.cursor` directory —
|
|
95
|
+
only the specific files listed above. Mounting the whole directory leaks
|
|
96
|
+
OS-specific absolute paths (e.g. Windows plugin cache paths) into the Linux
|
|
97
|
+
sandbox and can cause the sandbox to overwrite the host's plugin bookkeeping.
|
|
98
|
+
|
|
99
|
+
Only if none of the expected files exist for the runner the user chose, ask
|
|
100
|
+
them directly where their credentials live (e.g. a custom `CODEX_HOME`).
|
|
101
|
+
|
|
102
|
+
Full reference:
|
|
103
|
+
https://github.com/atolis-hq/wake/blob/main/docs/configuration.md#sandbox
|
|
104
|
+
|
|
105
|
+
## After config looks right
|
|
106
|
+
|
|
107
|
+
Don't try to explain the sandbox lifecycle yourself — point the user at (or
|
|
108
|
+
fetch, if you have web access):
|
|
109
|
+
|
|
110
|
+
- https://github.com/atolis-hq/wake/blob/main/docs/getting-started.md —
|
|
111
|
+
`wake sandbox build` / `up` / `setup` / `exec` / `down`
|
|
112
|
+
- https://github.com/atolis-hq/wake/blob/main/docs/runner-comparison.md —
|
|
113
|
+
deeper comparison of runner tradeoffs if the user asks which to pick
|
|
114
|
+
- https://github.com/atolis-hq/wake/blob/main/docs/configuration.md — every
|
|
115
|
+
config field, if something here doesn't cover their situation
|