@atolis-hq/wake 0.1.21 → 0.1.23

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
@@ -1,4 +1,48 @@
1
- # Wake
1
+ <div align="center">
2
+
3
+
4
+ <img src="https://raw.githubusercontent.com/atolis-hq/wake/refs/heads/main/assets/wake-logo.svg" alt="logo" width="200" height="auto" />
5
+ <h1>Wake</h1>
6
+
7
+ <p>
8
+ Autonomous software engineering control plane
9
+ </p>
10
+
11
+
12
+ <!-- Badges -->
13
+ <p>
14
+ <a href="https://github.com/atolis-hq/wake/commits/main">
15
+ <img src="https://img.shields.io/github/last-commit/atolis-hq/wake" alt="last update" />
16
+ </a>
17
+ <a href="https://github.com/atolis-hq/wake/actions/workflows/ci-cd.yml">
18
+ <img src="https://github.com/atolis-hq/wake/actions/workflows/ci-cd.yml/badge.svg" alt="CI/CD status" />
19
+ </a>
20
+ <!-- <a href="https://github.com/atolis-hq/wake/network/members">
21
+ <img src="https://img.shields.io/github/forks/atolis-hq/wake" alt="forks" />
22
+ </a>
23
+ <a href="https://github.com/atolis-hq/wake/stargazers">
24
+ <img src="https://img.shields.io/github/stars/atolis-hq/wake" alt="stars" />
25
+ </a> -->
26
+ <a href="https://github.com/atolis-hq/wake/issues/">
27
+ <img src="https://img.shields.io/github/issues/atolis-hq/wake" alt="open issues" />
28
+ </a>
29
+ <a href="https://github.com/atolis-hq/wake/blob/main/LICENSE">
30
+ <img src="https://img.shields.io/github/license/atolis-hq/wake.svg" alt="license" />
31
+ </a>
32
+ <a href="https://github.com/atolis-hq/wake/tags">
33
+ <img src="https://img.shields.io/github/v/tag/atolis-hq/wake" alt="latest tag" />
34
+ </a>
35
+ <a href="https://www.npmjs.com/package/@atolis-hq/wake">
36
+ <img src="https://img.shields.io/npm/v/%40atolis-hq%2Fwake" alt="npm version" />
37
+ </a>
38
+ <a href="docker/Dockerfile">
39
+ <img src="https://img.shields.io/badge/sandbox-docker-2496ED?logo=docker&logoColor=white" alt="runs in docker" />
40
+ </a>
41
+ </p>
42
+ </div>
43
+
44
+ <br />
45
+
2
46
 
3
47
  Wake is a control plane for autonomous software engineering. It watches the
4
48
  channels your team already uses, coordinates agent activity and involves humans
@@ -10,7 +54,20 @@ asks for input when human judgment matters, proposes a plan, launches local
10
54
  coding-agent CLIs to implement changes, opens pull requests, and carries the
11
55
  conversation forward wherever the work is already happening.
12
56
 
13
- Wake owns coordination. Coding agents execute the work.
57
+
58
+ ## Table of Contents
59
+
60
+ - [The Problem](#the-problem)
61
+ - [Vision](#vision)
62
+ - [Feature Overview](#feature-overview)
63
+ - [Where the Work Happens](#where-the-work-happens)
64
+ - [Supported Agent CLIs](#supported-agent-clis)
65
+ - [Getting Started](#getting-started)
66
+ - [Development](#development)
67
+ - [Documentation](#documentation)
68
+ - [Issues & Feature Requests](#issues--feature-requests)
69
+ - [License](#license)
70
+
14
71
 
15
72
  ## The Problem
16
73
 
@@ -75,6 +132,34 @@ For more detail, see [docs/vision.md](docs/vision.md) and
75
132
  Current runner capability differences are documented in
76
133
  [docs/runner-comparison.md](docs/runner-comparison.md).
77
134
 
135
+ ## Where the Work Happens
136
+
137
+ Wake has no chat UI you need to check for status. Your ticketing system is the
138
+ interface: Wake posts progress updates, asks clarifying questions, and reports
139
+ results as comments on the ticket, and reflects stage and status as labels on
140
+ it. When it's ready, it opens a pull request against your repo the normal way.
141
+ Reviewing, approving, and merging happen exactly where they already do today —
142
+ nothing new to learn, no separate dashboard to babysit.
143
+
144
+ A local control-plane UI exists for operators who want to watch runs, inspect
145
+ events, or resume a session directly, but it's a window into the same state —
146
+ not a required part of the workflow.
147
+
148
+ ## Supported Agent CLIs
149
+
150
+ Wake wraps existing coding-agent CLIs rather than replacing them. Runner
151
+ adapters currently exist for:
152
+
153
+ - **[Claude Code](https://claude.com/claude-code)**
154
+ - **[Codex](https://openai.com/codex/)**
155
+ - **[Cursor](https://cursor.com/cli)**
156
+
157
+ Each runner sits behind the same `AgentRunner` contract, so Wake's routing,
158
+ lifecycle, and sandbox behavior stay the same regardless of which CLI executes
159
+ a given step. A fake runner adapter also exists for zero-token testing of the
160
+ control plane itself. See [docs/runner-comparison.md](docs/runner-comparison.md)
161
+ for capability differences between runners.
162
+
78
163
  ## Getting Started
79
164
 
80
165
  Wake is distributed as the `@atolis-hq/wake` npm package. You can run the CLI
@@ -143,3 +228,23 @@ Recommended local practices:
143
228
 
144
229
  Local setup, commands, sandbox operation, auth setup, UI notes, and GitHub
145
230
  polling details are documented in [docs/development.md](docs/development.md).
231
+
232
+ ## Documentation
233
+
234
+ - [docs/vision.md](docs/vision.md) — the rationale and long-term direction for Wake.
235
+ - [docs/architecture.md](docs/architecture.md) — module boundaries and the event-sourced core.
236
+ - [docs/implementation.md](docs/implementation.md) — the accepted implementation plan.
237
+ - [docs/workflows.md](docs/workflows.md) — how stages, prompts, and runner routes are configured.
238
+ - [docs/prompts.md](docs/prompts.md) — how prompt templates map to workflow stages.
239
+ - [docs/configuration.md](docs/configuration.md) — `config.json` options and the operator correlation escape hatch.
240
+ - [docs/development.md](docs/development.md) — local setup and sandbox development workflow.
241
+ - [docs/runner-comparison.md](docs/runner-comparison.md) — capability differences between supported runners.
242
+
243
+ ## Issues & Feature Requests
244
+
245
+ Found a bug or have an idea for Wake? [Open an issue](https://github.com/atolis-hq/wake/issues/new) —
246
+ bug reports and feature requests are both welcome.
247
+
248
+ ## License
249
+
250
+ Wake is licensed under the [Apache License 2.0](LICENSE).
@@ -35,6 +35,9 @@ export const indexHtml = `<!DOCTYPE html>
35
35
  .topbar .version { color: rgba(255, 255, 255, 0.7); font-size: 0.78rem; }
36
36
  .statusbar { display: flex; align-items: center; gap: 1rem; padding: 0.45rem 1rem; background: var(--brand-dark); border-top: 1px solid rgba(0, 0, 0, 0.18); flex-wrap: wrap; font-size: 0.8rem; }
37
37
  .statusbar .meta { color: rgba(255, 255, 255, 0.72); }
38
+ .statusbar button { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18); color: #fff; border-radius: 6px; padding: 0.22rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
39
+ .statusbar button:hover:not(:disabled) { border-color: var(--accent-light); background: rgba(45, 212, 191, 0.16); }
40
+ .statusbar button:disabled { cursor: wait; opacity: 0.62; }
38
41
  .pill { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
39
42
  .pill-idle { background: #1f3d2c; color: #7fe3a3; }
40
43
  .pill-polling { background: #1f3350; color: #7fb3ff; }
@@ -80,6 +83,7 @@ export const indexHtml = `<!DOCTYPE html>
80
83
  </header>
81
84
  <div class="statusbar">
82
85
  <span id="loop-pill" class="pill">…</span>
86
+ <button id="force-tick" type="button">Tick now</button>
83
87
  <span id="status-summary" class="meta"></span>
84
88
  </div>
85
89
  <nav>
@@ -102,6 +106,12 @@ async function getJson(path) {
102
106
  return res.json();
103
107
  }
104
108
 
109
+ async function postJson(path) {
110
+ const res = await fetch(API + path, { method: 'POST' });
111
+ if (!res.ok) throw new Error(path + ' -> ' + res.status);
112
+ return res.json();
113
+ }
114
+
105
115
  function fmtMs(ms) {
106
116
  if (ms === undefined || ms === null) return '—';
107
117
  const s = Math.floor(ms / 1000);
@@ -142,6 +152,24 @@ async function renderStatusBar() {
142
152
  }
143
153
  }
144
154
 
155
+ async function forceTickNow() {
156
+ const button = document.getElementById('force-tick');
157
+ button.disabled = true;
158
+ const original = button.textContent;
159
+ button.textContent = 'Requested';
160
+ try {
161
+ await postJson('/tick');
162
+ await renderStatusBar();
163
+ } catch (err) {
164
+ document.getElementById('status-summary').textContent = 'tick request failed: ' + err.message;
165
+ } finally {
166
+ setTimeout(() => {
167
+ button.disabled = false;
168
+ button.textContent = original;
169
+ }, 900);
170
+ }
171
+ }
172
+
145
173
  async function renderBoard() {
146
174
  const board = await getJson('/board');
147
175
  const main = document.getElementById('main');
@@ -320,6 +348,7 @@ for (const btn of document.querySelectorAll('nav button')) {
320
348
  document.getElementById('drawer-close').addEventListener('click', () => {
321
349
  document.getElementById('drawer').classList.remove('open');
322
350
  });
351
+ document.getElementById('force-tick').addEventListener('click', forceTickNow);
323
352
 
324
353
  renderStatusBar();
325
354
  switchView('board');
@@ -1,5 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { configuredTicketSource } from '../../domain/sources.js';
4
+ import { writeJsonFile } from '../../lib/json-file.js';
3
5
  import { indexHtml } from './ui-assets.js';
4
6
  import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
5
7
  function sendJson(res, status, body) {
@@ -77,12 +79,6 @@ async function handleRequest(req, res, options, now) {
77
79
  res.writeHead(404).end('not found');
78
80
  return;
79
81
  }
80
- if (req.method !== 'GET') {
81
- sendJson(res, 405, {
82
- error: 'this build only serves read endpoints; mutations are not implemented',
83
- });
84
- return;
85
- }
86
82
  const { stateStore, resourceIndex, config } = options;
87
83
  const segments = url.pathname
88
84
  .slice('/api/v1/'.length)
@@ -90,6 +86,20 @@ async function handleRequest(req, res, options, now) {
90
86
  .filter((part) => part.length > 0)
91
87
  .map((s) => decodeURIComponent(s));
92
88
  const resource = segments[0];
89
+ if (req.method === 'POST' && resource === 'tick' && segments.length === 1) {
90
+ const request = {
91
+ requestId: randomUUID(),
92
+ requestedAt: now().toISOString(),
93
+ requestedBy: 'ui',
94
+ };
95
+ await writeJsonFile(stateStore.paths.tickRequestFile, request);
96
+ sendJson(res, 202, request);
97
+ return;
98
+ }
99
+ if (req.method !== 'GET') {
100
+ sendJson(res, 405, { error: `method not allowed for ${url.pathname}` });
101
+ return;
102
+ }
93
103
  if (resource === 'status' && segments.length === 1) {
94
104
  sendJson(res, 200, await buildStatus({ stateStore, config, now: now() }));
95
105
  return;
@@ -6,11 +6,43 @@ export function createControlPlane(deps) {
6
6
  let consecutiveIdleTicks = 0;
7
7
  let consecutiveIntakeIdleTicks = 0;
8
8
  let consecutiveRunnerIdleTicks = 0;
9
+ let lastSingleTickRequest;
10
+ let lastIntakeTickRequest;
11
+ let lastRunnerTickRequest;
9
12
  const maxIntervalMs = deps.maxIntervalMs ?? deps.intervalMs * 16;
10
13
  function nextSleepMs(consecutiveTicks) {
11
14
  const backoffMs = deps.intervalMs * 2 ** Math.min(consecutiveTicks, 20);
12
15
  return Math.min(backoffMs, maxIntervalMs);
13
16
  }
17
+ async function tickRequestPending(input) {
18
+ const request = (await deps.readTickRequest?.()) ?? null;
19
+ if (request === null || request === input.getLastRequest()) {
20
+ return false;
21
+ }
22
+ input.setLastRequest(request);
23
+ return true;
24
+ }
25
+ async function sleepUntilNextTick(ms, requestState) {
26
+ if (deps.readTickRequest === undefined || ms <= 0) {
27
+ await deps.sleep(ms);
28
+ return { forced: false };
29
+ }
30
+ if (await tickRequestPending(requestState)) {
31
+ return { forced: true };
32
+ }
33
+ const deadline = Date.now() + ms;
34
+ while (running) {
35
+ const remainingMs = deadline - Date.now();
36
+ if (remainingMs <= 0) {
37
+ return { forced: false };
38
+ }
39
+ await deps.sleep(Math.min(remainingMs, 250));
40
+ if (await tickRequestPending(requestState)) {
41
+ return { forced: true };
42
+ }
43
+ }
44
+ return { forced: false };
45
+ }
14
46
  async function isPaused() {
15
47
  return Boolean(await deps.isPaused());
16
48
  }
@@ -58,8 +90,13 @@ export function createControlPlane(deps) {
58
90
  input.setIdleTicks(0);
59
91
  }
60
92
  else {
61
- await deps.sleep(nextSleepMs(input.getIdleTicks()));
62
- input.setIdleTicks(input.getIdleTicks() + 1);
93
+ const wait = await sleepUntilNextTick(nextSleepMs(input.getIdleTicks()), {
94
+ getLastRequest: input.getLastRequest,
95
+ setLastRequest: input.setLastRequest,
96
+ });
97
+ if (!wait.forced) {
98
+ input.setIdleTicks(input.getIdleTicks() + 1);
99
+ }
63
100
  }
64
101
  }
65
102
  }
@@ -94,6 +131,10 @@ export function createControlPlane(deps) {
94
131
  setIdleTicks: (value) => {
95
132
  consecutiveIntakeIdleTicks = value;
96
133
  },
134
+ getLastRequest: () => lastIntakeTickRequest,
135
+ setLastRequest: (value) => {
136
+ lastIntakeTickRequest = value;
137
+ },
97
138
  }),
98
139
  startLoop({
99
140
  run: deps.tickRunner.runRunnerTick,
@@ -102,6 +143,10 @@ export function createControlPlane(deps) {
102
143
  setIdleTicks: (value) => {
103
144
  consecutiveRunnerIdleTicks = value;
104
145
  },
146
+ getLastRequest: () => lastRunnerTickRequest,
147
+ setLastRequest: (value) => {
148
+ lastRunnerTickRequest = value;
149
+ },
105
150
  }),
106
151
  ]);
107
152
  return;
@@ -122,8 +167,15 @@ export function createControlPlane(deps) {
122
167
  consecutiveIdleTicks = 0;
123
168
  }
124
169
  else {
125
- await deps.sleep(nextSleepMs(consecutiveIdleTicks));
126
- consecutiveIdleTicks += 1;
170
+ const wait = await sleepUntilNextTick(nextSleepMs(consecutiveIdleTicks), {
171
+ getLastRequest: () => lastSingleTickRequest,
172
+ setLastRequest: (value) => {
173
+ lastSingleTickRequest = value;
174
+ },
175
+ });
176
+ if (!wait.forced) {
177
+ consecutiveIdleTicks += 1;
178
+ }
127
179
  }
128
180
  }
129
181
  },
@@ -12,6 +12,7 @@ export function createWakePaths(wakeRoot) {
12
12
  configFile: join(wakeRoot, 'config.json'),
13
13
  ledgerFile: join(wakeRoot, 'ledger.json'),
14
14
  pauseFile: join(wakeRoot, 'PAUSE'),
15
+ tickRequestFile: join(wakeRoot, 'control', 'tick-request.json'),
15
16
  tickLockFile: join(wakeRoot, 'locks', 'tick.lock'),
16
17
  runnerLockFile: join(wakeRoot, 'locks', 'runner.lock'),
17
18
  issueFixtureFile: join(wakeRoot, 'fixtures', 'issues.json'),
package/dist/src/main.js CHANGED
@@ -26,6 +26,7 @@ import { createControlPlane } from './core/control-plane.js';
26
26
  import { createOutboundSinkRouter, createWorkSourceFanIn } from './core/sink-router.js';
27
27
  import { createTickRunner } from './core/tick-runner.js';
28
28
  import { systemClock } from './lib/clock.js';
29
+ import { readJsonFile } from './lib/json-file.js';
29
30
  import { configuredTicketSource } from './domain/sources.js';
30
31
  import { wakeVersion } from './version.js';
31
32
  function commandArgsBeforeTerminator(args) {
@@ -135,6 +136,17 @@ async function runCommandCapture(command, args) {
135
136
  });
136
137
  });
137
138
  }
139
+ async function readTickRequestId(path) {
140
+ try {
141
+ const request = await readJsonFile(path);
142
+ return typeof request.requestId === 'string' && request.requestId.length > 0
143
+ ? request.requestId
144
+ : null;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
138
150
  async function inspectDockerImage(image) {
139
151
  return await new Promise((resolveInspect, reject) => {
140
152
  const child = spawn('docker', ['image', 'inspect', image], {
@@ -318,6 +330,9 @@ async function runStart(args) {
318
330
  setTimeout(resolveSleep, ms);
319
331
  });
320
332
  },
333
+ readTickRequest() {
334
+ return readTickRequestId(runtime.stateStore.paths.tickRequestFile);
335
+ },
321
336
  });
322
337
  const stop = () => controlPlane.stop();
323
338
  process.on('SIGINT', stop);
@@ -1 +1 @@
1
- export const wakeVersion = "0.1.21+g5f671ca";
1
+ export const wakeVersion = "0.1.23+ge21a0d4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {