@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 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/`, and `workspaces/` at the top level for
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 docker/entrypoint.sh; the UI binds 0.0.0.0 inside the
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 { closeSync, existsSync, openSync } from 'node:fs';
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 logFd = options?.logFile !== undefined ? openSync(options.logFile, 'a') : 'ignore';
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', logFd, logFd],
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
- if (logFd !== 'ignore') {
268
- closeSync(logFd);
269
- }
279
+ logSink?.close().catch(() => { });
270
280
  });
271
281
  return { pid: child.pid ?? -1 };
272
282
  },
@@ -1 +1 @@
1
- export const wakeVersion = "0.2.5+ga2cf757";
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.5",
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