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/README.md CHANGED
@@ -1,3 +1,386 @@
1
- # Temporary Holding Version
1
+ # Ever Works Node (`apps/node`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.