@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 +107 -2
- package/dist/src/adapters/http/ui-assets.js +29 -0
- package/dist/src/adapters/http/ui-server.js +16 -6
- package/dist/src/core/control-plane.js +56 -4
- package/dist/src/lib/paths.js +1 -0
- package/dist/src/main.js +15 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
126
|
-
|
|
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
|
},
|
package/dist/src/lib/paths.js
CHANGED
|
@@ -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);
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const wakeVersion = "0.1.
|
|
1
|
+
export const wakeVersion = "0.1.23+ge21a0d4";
|