ever-works-node 0.0.0-stage → 0.2.1
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/LICENSE +661 -0
- package/README.md +385 -2
- package/cli.js +21859 -0
- package/package.json +47 -4
- package/packaging/README.md +276 -0
- package/packaging/systemd/ever-works-node@.service +64 -0
- package/packaging/systemd/install.sh +68 -0
- package/packaging/windows/install-service.logic.ps1 +1082 -0
- package/packaging/windows/install-service.ps1 +943 -0
- package/packaging/windows/test-install-service-logic.ps1 +722 -0
- package/packaging/windows/uninstall-service.ps1 +67 -0
package/README.md
CHANGED
|
@@ -1,3 +1,386 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Ever Works Node (`apps/node`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless execution node: a CLI and long-running service that registers **this machine** with an
|
|
4
|
+
Ever Works platform, keeps it visible in the Fleet settings page with a regular heartbeat, and —
|
|
5
|
+
with `--work` — **leases and executes platform work** on it.
|
|
6
|
+
|
|
7
|
+
This app also **owns the shared node core** (`src/core/`) — enrollment, heartbeat, capability
|
|
8
|
+
detection and config persistence are written once here and imported by `apps/desktop-node`, so the
|
|
9
|
+
headless and desktop shells can never drift apart (PRD §3.3).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
The node ships on npm as the public package **`ever-works-node`** — one command per machine, no
|
|
14
|
+
monorepo checkout:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g ever-works-node # Node.js >= 22
|
|
18
|
+
ever-works-node --version
|
|
19
|
+
ever-works-node capabilities # what this machine would advertise, before enrolling
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The published package is a single bundled `cli.js` (see `build.js`); the only runtime dependency is
|
|
23
|
+
the optional `@napi-rs/keyring` native addon for the OS keychain — where its prebuilt binary is
|
|
24
|
+
unavailable the install still works, with the credential in the owner-locked config file instead.
|
|
25
|
+
|
|
26
|
+
The unattended-run scripts ship inside the package. After a global install the Windows installer is
|
|
27
|
+
at:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
& "$(npm root -g)\ever-works-node\packaging\windows\install-service.ps1" -Work
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(Windows: [NSSM](https://nssm.cc) is optional — with it the script registers a real Windows service,
|
|
34
|
+
without it a boot-time Scheduled Task. Either way it registers `node.exe` running the package's
|
|
35
|
+
`cli.js` directly — never npm's `.ps1`/`.cmd` shims, which service managers cannot launch — and
|
|
36
|
+
re-running it re-applies the current flags. From a source checkout pass
|
|
37
|
+
`-CliPath <checkout>\apps\node\dist\cli.js`. The systemd unit for Linux is under `packaging/systemd/`.)
|
|
38
|
+
|
|
39
|
+
**From source** instead — in the monorepo, `pnpm build:node` builds the node and its workspace
|
|
40
|
+
dependencies into `apps/node/dist/`, and `cd apps/node && npm link` puts `ever-works-node` on `PATH`
|
|
41
|
+
pointing at that checkout.
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ever-works-node enroll --api-url <url> --token <one-time-token> [--name <label>] [-i <seconds>] [--min-free-disk <mib>] [--workspace-max-age <days>]
|
|
47
|
+
ever-works-node start [-i <seconds>] [--work] [--concurrency <count>] [--claude-path <file>] [--codex-path <file>] [--workspace-root <dir>] [--min-free-disk <mib> | --no-disk-floor] [--workspace-max-age <days>] [--workspace-max-count <n>]
|
|
48
|
+
ever-works-node pause [--local-only]
|
|
49
|
+
ever-works-node resume [--local-only]
|
|
50
|
+
ever-works-node unenroll [--local-only]
|
|
51
|
+
ever-works-node status
|
|
52
|
+
ever-works-node capabilities
|
|
53
|
+
ever-works-node doctor [--workspace-root <dir>] [--max-age <days>] [--max-count <n>] [--offline] [--json]
|
|
54
|
+
ever-works-node gc [--workspace-root <dir>] [--max-age <days>] [--max-count <n>] [--dry-run] [--offline]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **`enroll`** consumes a one-time token from the platform's Fleet page (`POST /api/fleet/enroll`)
|
|
58
|
+
and writes `{apiUrl, nodeId, capabilities, …}` to the OS config directory, with the credential
|
|
59
|
+
itself going to the OS keychain where one exists.
|
|
60
|
+
- **`start`** runs the heartbeat loop (`POST /api/fleet/heartbeat`, default every 60s) with
|
|
61
|
+
exponential backoff on failure, refreshing capability tags on every beat, until SIGINT/SIGTERM.
|
|
62
|
+
With **`--work`** it also runs the worker host: lease → execute → report against
|
|
63
|
+
`POST /api/fleet/jobs/*`. This is opt-in on purpose — enrolling a machine and letting it run the
|
|
64
|
+
owner's commands are two different consents. `--claude-path` / `--codex-path` pin the model CLIs
|
|
65
|
+
for this process and `--workspace-root <dir>` sets the **absolute** directory the per-Task
|
|
66
|
+
worktrees of agent tasks live under (default `EVER_WORKS_NODE_WORKSPACE_ROOT`, then
|
|
67
|
+
`~/.ever-works/fleet-workspaces`); a relative path is a usage error.
|
|
68
|
+
- **`pause` / `resume`** drain and undrain this machine. Pausing stops leasing **immediately** and
|
|
69
|
+
lets in-flight jobs finish and report — it is not a kill. It tells the platform
|
|
70
|
+
(`POST /api/fleet/pause`, so the scheduler stops offering work) _and_ records the intent locally,
|
|
71
|
+
so a service restart comes back paused. A paused node keeps heartbeating: a drained machine that
|
|
72
|
+
vanishes from Fleet is indistinguishable from a dead one.
|
|
73
|
+
- **`unenroll`** retires the machine — `POST /api/fleet/unenroll` deletes the registration, then the
|
|
74
|
+
local credential is erased. The local erase happens **even if the API call fails**: a
|
|
75
|
+
decommissioned laptop holding a live fleet secret is the worse outcome.
|
|
76
|
+
- **`status`** prints the local enrollment with the credential reported but never shown, including
|
|
77
|
+
where it is stored and whether the node is paused.
|
|
78
|
+
- **`capabilities`** prints the tags this machine would report, without enrolling.
|
|
79
|
+
- **`doctor`** is read-only: free space on the workspace volume against the disk floor, the workspace
|
|
80
|
+
root, how many Task worktrees and repository pools it holds, and — per worktree — what `gc` would do
|
|
81
|
+
and why. `--json` emits one object for scripts. Works whether or not the machine is enrolled.
|
|
82
|
+
Both `doctor` and `gc` inspect the root the node's last `start --workspace-root` recorded in the
|
|
83
|
+
config, and the header says where the root came from — `--workspace-root`, the config, or the
|
|
84
|
+
process default. That matters when the service runs as its own account: the default resolves under
|
|
85
|
+
the account running the CLI, so a bare `doctor` there would answer about an empty directory that is
|
|
86
|
+
not the node's tree at all. Pass `--workspace-root` when the header says the root is only a default.
|
|
87
|
+
- **`gc`** runs the workspace reaper (see "Disk floor and workspace GC" below). `--dry-run` prints the
|
|
88
|
+
plan and removes nothing; `--max-age` / `--max-count` override the stored policy for that run only.
|
|
89
|
+
Exit `1` when a planned removal failed — nothing is ever force-deleted.
|
|
90
|
+
|
|
91
|
+
`--local-only` skips the API call, for a machine being drained or decommissioned offline.
|
|
92
|
+
|
|
93
|
+
Exit codes: `0` ok, `1` failure, `3` not enrolled (so provisioning scripts can branch on it).
|
|
94
|
+
|
|
95
|
+
## Config file
|
|
96
|
+
|
|
97
|
+
| Platform | Location |
|
|
98
|
+
| -------- | -------------------------------------------------------------------- |
|
|
99
|
+
| Windows | `%APPDATA%\ever-works-node\node-config.json` |
|
|
100
|
+
| macOS | `~/Library/Application Support/ever-works-node/node-config.json` |
|
|
101
|
+
| Linux | `$XDG_CONFIG_HOME/ever-works-node/node-config.json` (or `~/.config`) |
|
|
102
|
+
|
|
103
|
+
`EVER_WORKS_NODE_CONFIG` overrides the path entirely.
|
|
104
|
+
|
|
105
|
+
`EVER_WORKS_NODE_API_URL` overrides the **API origin** the node talks to, for every call after
|
|
106
|
+
enrollment (heartbeat, lease, job heartbeat, complete, pause, unenroll). It exists so a bad build on
|
|
107
|
+
the origin a fleet was enrolled against cannot orphan the machines: pin them at a stable origin,
|
|
108
|
+
restart, and they keep working while the fix ships.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
EVER_WORKS_NODE_API_URL=https://apistage.ever.works # stage
|
|
112
|
+
EVER_WORKS_NODE_API_URL=https://api.ever.works # prod
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Four rules, all deliberate:
|
|
116
|
+
|
|
117
|
+
- **It does not affect `enroll`.** Enrollment mints a credential against the origin given with
|
|
118
|
+
`--api-url`, and that is the origin stored in the config. A pin that silently redirected it would
|
|
119
|
+
store a secret as belonging to a platform that never issued it.
|
|
120
|
+
- **It is never written back to this file.** Unsetting the variable is a complete undo.
|
|
121
|
+
- **Empty means unset.** `EVER_WORKS_NODE_API_URL=` in a unit file turns the override off; it never
|
|
122
|
+
means "no API".
|
|
123
|
+
- **A malformed value stops the node at startup** with a URL error, rather than becoming a
|
|
124
|
+
mystifying 403/404 at the first request.
|
|
125
|
+
|
|
126
|
+
A pin pointing at a platform this node did **not** enrol against will be refused with 401 on every
|
|
127
|
+
call — the node has no credential there. `status` and `doctor` both print the effective origin, name
|
|
128
|
+
where it came from, and warn explicitly about that mismatch. The full procedure is
|
|
129
|
+
`docs/runbooks/FLEET_BREAK_GLASS.md`.
|
|
130
|
+
|
|
131
|
+
**The credential does not live in this file when it does not have to.** With an OS keychain
|
|
132
|
+
available (macOS Keychain, Windows Credential Manager, Linux Secret Service — all reached through
|
|
133
|
+
the `@napi-rs/keyring` SDK) the secret is stored there and the file records only
|
|
134
|
+
`"secretStorage": "keychain"`. Where no keychain exists — headless servers, containers — it falls
|
|
135
|
+
back into the file and **says so loudly**, on every load and every save.
|
|
136
|
+
|
|
137
|
+
Either way the file is locked to its owner: mode **0600** at creation and re-chmod'ed after write on
|
|
138
|
+
POSIX, and on Windows an inheritance-stripped owner-only ACL applied with `icacls`. Windows is no
|
|
139
|
+
longer skipped.
|
|
140
|
+
|
|
141
|
+
`EVER_WORKS_NODE_DISABLE_KEYCHAIN=1` forces the file fallback (the container image sets it, because
|
|
142
|
+
a container never has a keychain and a surprise warning is worse than a declared choice).
|
|
143
|
+
|
|
144
|
+
## Capability tags
|
|
145
|
+
|
|
146
|
+
Detected at enroll and re-detected on **every heartbeat**, so installing Docker or Git on a running
|
|
147
|
+
node shows up in Fleet without a restart:
|
|
148
|
+
|
|
149
|
+
`os:<platform>` · `arch:<arch>` · `node:<major>` · `terminal` · `workspace` · plus `docker`, `git`,
|
|
150
|
+
`display`, `browser`, `gpu` and `gpu:<vendor>` when present. Tags are normalized with the same rules
|
|
151
|
+
the server applies (trim → 32 chars → dedupe → max 16), so what the node reports is exactly what
|
|
152
|
+
Fleet stores.
|
|
153
|
+
|
|
154
|
+
Two rules govern what may appear:
|
|
155
|
+
|
|
156
|
+
1. **A tag is a promise the node can keep.** `browser` is emitted only when a browser executable was
|
|
157
|
+
actually resolved — the same path `browser-check` will spawn — and the `browser-check` executor is
|
|
158
|
+
registered by that same fact. A tag with nothing behind it routes real work to a machine that
|
|
159
|
+
cannot do it.
|
|
160
|
+
2. **Detection never fails the beat.** Every probe swallows its own errors: a missing tool is a
|
|
161
|
+
missing tag, not a missing heartbeat.
|
|
162
|
+
|
|
163
|
+
`EVER_WORKS_NODE_BROWSER` pins the browser executable explicitly. A pinned path that does not exist
|
|
164
|
+
disables the tag rather than falling through to some other browser — silently launching a different
|
|
165
|
+
engine than the one an operator chose is how a check passes for the wrong reason.
|
|
166
|
+
|
|
167
|
+
## Layout
|
|
168
|
+
|
|
169
|
+
- `src/core/` — the shared core, pure logic over injected IO:
|
|
170
|
+
- `fleet-client.ts` enroll/heartbeat HTTP over an injected `fetch`
|
|
171
|
+
- `heartbeat.ts` the loop + backoff state machine over an injected scheduler
|
|
172
|
+
- `capabilities.ts` detection over an injected command runner
|
|
173
|
+
- `config-store.ts` persistence over an injected filesystem
|
|
174
|
+
- `logger.ts` redacting logger — credentials are `protect()`ed once and scrubbed everywhere
|
|
175
|
+
- `job-client.ts` lease/heartbeat/complete HTTP over the same injected `fetch`
|
|
176
|
+
- `worker-loop.ts` the lease → execute → report loop (backoff, keep-alive, draining shutdown
|
|
177
|
+
**and draining pause**)
|
|
178
|
+
- `browser-probe.ts` / `gpu-probe.ts` the `browser` and `gpu` capability probes
|
|
179
|
+
- `secret-store.ts` OS keychain over an injected SDK loader, with a loud file fallback
|
|
180
|
+
- `executors/acceptance-checks.ts` the v1 job kind, with its own scrubbed subprocess env
|
|
181
|
+
- `executors/browser-check.ts` the v2 job kind — a real browser in a throwaway profile
|
|
182
|
+
- `runtime.ts` composition root (`enrollNode`, `createNodeRuntime`, shutdown handlers)
|
|
183
|
+
- `types.ts` wire types + the server's limits, mirrored (the job protocol instead comes from
|
|
184
|
+
`@ever-works/contracts`, which is where drift would actually hurt: it carries executable work)
|
|
185
|
+
- `src/node-io.ts` — the one place that binds those interfaces to Node built-ins
|
|
186
|
+
- `src/cli/program.ts` — argument parsing and command bodies (commander), fully injectable
|
|
187
|
+
- `src/cli.ts` — the `bin` entry point; IO binding only
|
|
188
|
+
|
|
189
|
+
## Security notes
|
|
190
|
+
|
|
191
|
+
- The heartbeat secret is **never logged**: the enrollment token and the minted secret are
|
|
192
|
+
registered with the redacting logger the moment they exist, and every sink is scrubbed.
|
|
193
|
+
- Server bodies are never echoed into client errors — the API answers every invalid credential path
|
|
194
|
+
with one undifferentiated 401, and this client keeps that posture.
|
|
195
|
+
- Transport is **outbound-only**; the node never listens on a port. That includes job leasing —
|
|
196
|
+
the node polls for work, nothing ever connects in.
|
|
197
|
+
- Check subprocesses get an env built **from scratch**, never the inherited one. A check command is
|
|
198
|
+
user-authored input running on somebody's real machine, so the allowlist covers toolchain
|
|
199
|
+
discovery and locale only, secret-shaped names are dropped, and the node's OWN credential
|
|
200
|
+
namespace (`FLEET_*`) can never be granted even by an explicit `envPassthrough`.
|
|
201
|
+
- A lease is a **deadline, not a lock**: if this process dies mid-job, the platform reclaims the
|
|
202
|
+
claim and re-offers the work. Shutdown drains in-flight jobs so their verdicts are reported
|
|
203
|
+
rather than abandoned.
|
|
204
|
+
- A real `User-Agent` is always sent — the production API edge answers default/absent agents with 403.
|
|
205
|
+
|
|
206
|
+
## Commands (development)
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pnpm --filter ever-works-node build # tsc type-check + CommonJS emit to dist/
|
|
210
|
+
pnpm --filter ever-works-node test # vitest unit tests (no network, no disk, no real timers)
|
|
211
|
+
pnpm --filter ever-works-node build:bundle # stage the publishable npm package under dist-bundle/
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`build:bundle` (`build.js`) inlines the `workspace:*` packages with esbuild into one `cli.js`,
|
|
215
|
+
writes the public manifest beside it and copies `packaging/`. The workspace package itself stays
|
|
216
|
+
`private`; `.github/workflows/publish-node.yml` publishes `dist-bundle/` on a `node-v<version>` tag, with an
|
|
217
|
+
npm provenance attestation. Release flow: bump `package.json` and `src/version.ts` together in a PR
|
|
218
|
+
(`src/version.spec.ts` pins them to each other), merge, then tag the merge commit `node-v<version>` —
|
|
219
|
+
the tag must equal the package version, so every published version maps to one commit. Running the
|
|
220
|
+
workflow by hand is a dry run only (build, smoke test, `npm publish --dry-run`).
|
|
221
|
+
|
|
222
|
+
## Notes
|
|
223
|
+
|
|
224
|
+
- **Excluded from the default root build**: like `apps/docs` and `apps/desktop`, this app is
|
|
225
|
+
filtered out of root `pnpm build` / `pnpm build:apps`; build it explicitly with
|
|
226
|
+
`pnpm build:node` (root `pnpm build:all` includes it).
|
|
227
|
+
- **Running it unattended** — systemd unit, Windows service / scheduled task, container image — is
|
|
228
|
+
documented in [`packaging/README.md`](packaging/README.md).
|
|
229
|
+
|
|
230
|
+
## What a node can and cannot run
|
|
231
|
+
|
|
232
|
+
The worker host resolves an executor by `job.kind`:
|
|
233
|
+
|
|
234
|
+
| Kind | Status |
|
|
235
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
236
|
+
| `acceptance-checks` | **Working end to end.** Runs a Task's dispatch-frozen acceptance checks in a workspace directory on this machine and reports each exit code, with verdict rules identical to the platform's `TaskGateRunnerService`. |
|
|
237
|
+
| `agent-task` | **Working end to end.** The general kind. In the platform's `command` mode it runs the operator's command template; in `model-cli` mode it provisions an isolated worktree of the Task's repository, runs a **local Claude Code / Codex** on the instructions the platform assembled, grades the acceptance checks, then commits and pushes the task branch. See "Agent execution v2" below. |
|
|
238
|
+
| `browser-check` | **Working end to end, on a node that resolved a browser.** Drives the machine's real browser against a URL in a throwaway profile and reports what it rendered (DOM bytes, `<title>`, an optional `expectText`). Registered only when the `browser` tag is advertised. |
|
|
239
|
+
|
|
240
|
+
## Agent execution v2 — model CLIs on this machine
|
|
241
|
+
|
|
242
|
+
A node advertises `claude-code` and/or `codex` when it resolved the executable at startup:
|
|
243
|
+
|
|
244
|
+
1. `EVER_WORKS_NODE_CLAUDE_PATH` / `EVER_WORKS_NODE_CODEX_PATH` (or `start --claude-path` /
|
|
245
|
+
`--codex-path`) pin an executable. A pin that does not resolve **disables** that CLI rather than
|
|
246
|
+
falling back to PATH — a run must never succeed on a binary the operator did not choose.
|
|
247
|
+
2. Otherwise the first `claude` / `codex` on PATH (on Windows, the `.cmd` / `.exe` form).
|
|
248
|
+
|
|
249
|
+
A `model-cli` job is only offered to nodes advertising the CLI it needs. On such a job the node:
|
|
250
|
+
|
|
251
|
+
- provisions the Task worktree (bare cache + `git worktree`, per-Task binding, root-confined);
|
|
252
|
+
- writes the platform's instructions to a scratch file and runs the CLI **through the same command
|
|
253
|
+
runner every check uses** — env scrub, timeout, cancellation, whole-tree kill — with the
|
|
254
|
+
instructions on stdin (`claude -p --output-format json …` / `codex exec --json … -`) and the CLI's
|
|
255
|
+
structured output captured to a scratch file. Nothing free-form ever reaches argv;
|
|
256
|
+
- runs the acceptance checks in the worktree;
|
|
257
|
+
- `git add -A && git commit && git push HEAD:refs/heads/<task-branch>` via the node's own Git
|
|
258
|
+
credential helper (token-free, like the fetch);
|
|
259
|
+
- reports one `FleetAgentTaskResult`: model summary / cost / turns / session id, check verdicts,
|
|
260
|
+
gate status, branch + head SHA + changed-file count, and a one-sentence `failureReason` when any
|
|
261
|
+
required part did not pass.
|
|
262
|
+
|
|
263
|
+
The CLI runs with the machine's own login (`~/.claude`, `~/.codex`) or the credential names the
|
|
264
|
+
platform granted (`CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `CODEX_ACCESS_TOKEN`,
|
|
265
|
+
`OPENAI_API_KEY` — one per family, subscription-backed first). Scratch files live under the OS temp
|
|
266
|
+
dir (`ever-works-node/agent-tasks/<job id>`) and are removed after every run.
|
|
267
|
+
|
|
268
|
+
`browser-check` runs headless by default (`--headless=new --dump-dom`), which is what makes its
|
|
269
|
+
verdict a real observation rather than "the process did not crash". `headed: true` opens a visible
|
|
270
|
+
window on a node that also advertises `display`; because Chrome exposes no DOM outside headless, a
|
|
271
|
+
headed job that asks for `expectText` is **refused** rather than quietly downgraded.
|
|
272
|
+
|
|
273
|
+
A leased job of any other kind is completed as a **failure naming the kind** — never silently
|
|
274
|
+
dropped, which would leave it to expire and retry forever on the same incapable node.
|
|
275
|
+
|
|
276
|
+
### Multi-repo Task workspaces
|
|
277
|
+
|
|
278
|
+
A job's workspace spec may carry `mounts`: additional repositories checked out on the same Task branch.
|
|
279
|
+
Each mount is its own binding under the workspace root (same pool, reuse and ownership proof as the
|
|
280
|
+
primary) and is linked into the primary worktree at `.mounts/<dir>` — a directory junction on Windows,
|
|
281
|
+
a symlink elsewhere — with `/.mounts/` written to the repository's shared `info/exclude`, so the
|
|
282
|
+
primary's Git never sees it. After the model step the node commits and pushes every writable mount
|
|
283
|
+
(one verdict each, reported as `mountGit`) and then the primary. A mount that cannot be provisioned
|
|
284
|
+
fails the job naming it; a mount whose push fails is reported on its own entry while the others still
|
|
285
|
+
complete.
|
|
286
|
+
|
|
287
|
+
### Disk floor and workspace GC
|
|
288
|
+
|
|
289
|
+
Two node-side guards keep a machine from filling its disk with Task worktrees (self-build program note
|
|
290
|
+
§6, findings OPS-12 and R8).
|
|
291
|
+
|
|
292
|
+
**Disk floor.** A node refuses to lease work while the volume that holds its workspace root has fewer
|
|
293
|
+
free bytes than the floor — **2 GiB by default**, set with `--min-free-disk <mib>` in **mebibytes** (persisted by
|
|
294
|
+
`enroll`, process-only on `start`, like `--max-cpu`), switched off with `--no-disk-floor`. The floor is
|
|
295
|
+
measured on the **workspace root's volume** (the nearest existing ancestor of it), never on the system
|
|
296
|
+
drive, and it is checked twice: at the lease, and again by the provisioner right before it writes
|
|
297
|
+
anything — once before the primary worktree and once before every mount, because disk can drop between
|
|
298
|
+
the two. A refused lease shows the node as `throttled` with the reason in the status window and one
|
|
299
|
+
warning in the log (one more line when it clears); a provision refused for disk (`disk-low`) is not a
|
|
300
|
+
verdict about the work — the node hands the job back **unsettled**, its claim lapses and the platform
|
|
301
|
+
re-offers it to a node with room, and the very next poll throttles this one.
|
|
302
|
+
|
|
303
|
+
Both checks treat an **unreadable** volume the same way: they refuse. The floor fails closed, because
|
|
304
|
+
the provision-time check is the last one before a clone, a fetch and a model's whole budget land on a
|
|
305
|
+
volume nobody can size — and the lease check has to be at least as strict, or the node takes every job
|
|
306
|
+
it is offered and then defers it here, spending one of the job's attempts per lapsed lease until the
|
|
307
|
+
platform fails it with a message that never mentions disk. The node therefore goes `throttled` with a
|
|
308
|
+
reason instead, in the log, in the drawer and in `doctor`; `--no-disk-floor` is the explicit way to
|
|
309
|
+
switch the control off on a host whose `statfs` cannot answer. The heartbeat's `diskFreeBytes` is taken
|
|
310
|
+
with the same measurement as the gates (the nearest existing ancestor of the root), so a node that has
|
|
311
|
+
never provisioned still reports a figure to compare the floor against.
|
|
312
|
+
|
|
313
|
+
**What the platform sees (EW-803).** The heartbeat also reports the floor in force
|
|
314
|
+
(`minFreeDiskBytes`; `null` when `--no-disk-floor` is set), what the last sweep retained
|
|
315
|
+
(`workspaceCount`, `workspaceBytes`) and when it ran and what it took (`lastReclaimAt`,
|
|
316
|
+
`lastReclaimFreedBytes`). These travel **upward only** — the platform never sets them and never routes
|
|
317
|
+
on them; the limit stays enforced here. The CPU and memory ceilings are not reported at all: there is no
|
|
318
|
+
CPU or memory reading beside them to make a ceiling meaningful. A node without `--work` enforces no
|
|
319
|
+
floor and runs no reaper, so it reports none of this and Fleet shows "not reported" rather than zeros.
|
|
320
|
+
|
|
321
|
+
**Workspace reaper.** With `--work`, the node runs a reaper over its workspace root a minute after start
|
|
322
|
+
and then every six hours (skipped, and retried in half an hour, while a job is running); `ever-works-node
|
|
323
|
+
gc` runs the same reaper by hand. It removes a Task worktree only when it can **prove** all of this, and
|
|
324
|
+
`doctor` prints the first rule each worktree fails:
|
|
325
|
+
|
|
326
|
+
1. it is the node's own — the provider's binding stamp _and_ an exact `git worktree list` registration;
|
|
327
|
+
2. no provisioning intent is pending for it;
|
|
328
|
+
3. no live process holds its lease (see below), and this process is not using it;
|
|
329
|
+
4. `git status --porcelain --untracked-files=all` is empty and no `index.lock` / `HEAD.lock` exists;
|
|
330
|
+
plus the two things that command structurally cannot see, because the node excludes them itself:
|
|
331
|
+
`.mounts` is a plain directory (not a junction someone put there), and `.ever-works/` — the
|
|
332
|
+
owner-question channel — is empty or absent;
|
|
333
|
+
5. no commit on `HEAD` is missing from every remote-tracking ref;
|
|
334
|
+
6. the remote was reachable, **and** the branch is gone from it or merged into its default branch — a
|
|
335
|
+
branch still open on the remote keeps the worktree, however old;
|
|
336
|
+
7. it was last provisioned longer ago than `--workspace-max-age` (default **14 days**, persisted by
|
|
337
|
+
`enroll` and by `start`, which differs from the process-only ceilings on purpose:
|
|
338
|
+
`install-service.ps1` re-applies `start` flags on every re-install). An optional
|
|
339
|
+
`--workspace-max-count <n>` additionally trims the least recently used worktrees that already pass
|
|
340
|
+
rules 1–6.
|
|
341
|
+
|
|
342
|
+
Unknown always means keep, so an `--offline` scan can inform but never removes. Rules 5 and 6 need the
|
|
343
|
+
remote because the node **pushes to a URL**, which never updates `refs/remotes/origin/<branch>`: the
|
|
344
|
+
scan does one `ls-remote` and one batched `fetch` per repository pool through the node's own Git
|
|
345
|
+
credential helper (token-free, `GIT_TERMINAL_PROMPT=0`, 30 s bound per call) before judging, and a
|
|
346
|
+
branch the remote no longer has gets its stale tracking ref deleted. A merged branch that still exists
|
|
347
|
+
on the remote is judged by `merge-base --is-ancestor` against the refreshed default branch; when the
|
|
348
|
+
merge predates the pool's shallow boundary that answers "no" and the worktree is kept.
|
|
349
|
+
|
|
350
|
+
Removal never calls a recursive filesystem delete on a checkout. Every `.mounts/*` link is unlinked
|
|
351
|
+
first — a junction there points at **another** Task's worktree — then the provider's own `teardown`
|
|
352
|
+
re-proves ownership and runs `git worktree remove --force` (which also deletes ignored files such as
|
|
353
|
+
`node_modules`; that is intended, the worktree was proven clean, pushed and closed) and `git worktree
|
|
354
|
+
prune`. A bare repository pool is deleted only once Git lists no worktree for it, no intent is pending,
|
|
355
|
+
its remote was refreshed by this scan, **and** nothing it can still reach — branches, tags, the stash,
|
|
356
|
+
the **reflog** — carries a commit missing from every remote-tracking ref (`rev-list --count --all
|
|
357
|
+
--reflog --not --remotes`). `git worktree remove` leaves the branch behind and the provider's `worktree
|
|
358
|
+
add -B` force-resets one into its reflog, so an emptied pool can still hold the only copy of unpushed
|
|
359
|
+
work, and that keeps it. A
|
|
360
|
+
pool is dated by its `worktrees/` and intents directories, never by `FETCH_HEAD`, which the scan itself
|
|
361
|
+
rewrites. Anything the reaper does not recognise under the root is reported and left alone.
|
|
362
|
+
|
|
363
|
+
Two files in each worktree's **private gitdir** (`<pool>/worktrees/<id>/`, never in the working tree,
|
|
364
|
+
gone with the worktree) carry the evidence: `ew-workspace-lease.json` (`{purpose: job|gc, pid, taskId}`,
|
|
365
|
+
created atomically and exclusively; a lease held by a dead pid is reclaimable; one this build cannot
|
|
366
|
+
PARSE — a future `version`, an unknown `purpose`, a torn write — is treated as HELD, never reclaimed;
|
|
367
|
+
one held by a live foreign job is a
|
|
368
|
+
`path-collision` that preserves the worktree and fails the job naming the holder; one held by the reaper
|
|
369
|
+
mid-removal is a transient `workspace-busy` that hands the job back unsettled, and the retry lands on a
|
|
370
|
+
fresh checkout) and
|
|
371
|
+
`ew-workspace-usage.json` (`lastUsedAt`, refreshed on every provision and release). A worktree from
|
|
372
|
+
before this build has neither; it is dated by its Git mtimes, and while a `.worker-session` marker
|
|
373
|
+
exists for the config — a worker that may predate leases could be using it — `gc` keeps it until its
|
|
374
|
+
first run under this build stamps it.
|
|
375
|
+
|
|
376
|
+
## Follow-ups
|
|
377
|
+
|
|
378
|
+
- Further job kinds behind the same executor seam: full agent execution, `pty-local`
|
|
379
|
+
(`terminal-stream`) sessions and `local-workspace` provisioning. Each is one `register(kind, …)`
|
|
380
|
+
call — no protocol change, no new endpoint, no new credential.
|
|
381
|
+
- Workspace **provisioning** on the node: today `acceptance-checks` requires the workspace to
|
|
382
|
+
already exist on this machine (it refuses a path it cannot resolve), so the end-to-end cloud
|
|
383
|
+
path still wants a checkout step.
|
|
384
|
+
- **Auto-update.** The npm package carries an npm provenance attestation, but a node does not
|
|
385
|
+
update itself — `npm install -g ever-works-node@latest` plus a service restart
|
|
386
|
+
is the upgrade path.
|