@theford/runly 0.0.0-stage → 0.20.0

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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/dist/cli/client.d.ts +80 -0
  4. package/dist/cli/client.js +294 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/daemon-unit.d.ts +58 -0
  7. package/dist/cli/daemon-unit.js +289 -0
  8. package/dist/cli/daemon-unit.js.map +1 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +1482 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/install.d.ts +9 -0
  13. package/dist/cli/install.js +53 -0
  14. package/dist/cli/install.js.map +1 -0
  15. package/dist/cli/logs.d.ts +43 -0
  16. package/dist/cli/logs.js +232 -0
  17. package/dist/cli/logs.js.map +1 -0
  18. package/dist/cli/tether.d.ts +3 -0
  19. package/dist/cli/tether.js +65 -0
  20. package/dist/cli/tether.js.map +1 -0
  21. package/dist/cli/watchdog.d.ts +1 -0
  22. package/dist/cli/watchdog.js +81 -0
  23. package/dist/cli/watchdog.js.map +1 -0
  24. package/dist/core/builds.d.ts +20 -0
  25. package/dist/core/builds.js +93 -0
  26. package/dist/core/builds.js.map +1 -0
  27. package/dist/core/caller-env.d.ts +17 -0
  28. package/dist/core/caller-env.js +77 -0
  29. package/dist/core/caller-env.js.map +1 -0
  30. package/dist/core/diagnostics.d.ts +68 -0
  31. package/dist/core/diagnostics.js +34 -0
  32. package/dist/core/diagnostics.js.map +1 -0
  33. package/dist/core/env-vars.d.ts +35 -0
  34. package/dist/core/env-vars.js +32 -0
  35. package/dist/core/env-vars.js.map +1 -0
  36. package/dist/core/events.d.ts +10 -0
  37. package/dist/core/events.js +42 -0
  38. package/dist/core/events.js.map +1 -0
  39. package/dist/core/exec.d.ts +22 -0
  40. package/dist/core/exec.js +84 -0
  41. package/dist/core/exec.js.map +1 -0
  42. package/dist/core/journal.d.ts +227 -0
  43. package/dist/core/journal.js +395 -0
  44. package/dist/core/journal.js.map +1 -0
  45. package/dist/core/logs.d.ts +56 -0
  46. package/dist/core/logs.js +259 -0
  47. package/dist/core/logs.js.map +1 -0
  48. package/dist/core/manifest.d.ts +292 -0
  49. package/dist/core/manifest.js +179 -0
  50. package/dist/core/manifest.js.map +1 -0
  51. package/dist/core/namespaces.d.ts +7 -0
  52. package/dist/core/namespaces.js +59 -0
  53. package/dist/core/namespaces.js.map +1 -0
  54. package/dist/core/paths.d.ts +31 -0
  55. package/dist/core/paths.js +65 -0
  56. package/dist/core/paths.js.map +1 -0
  57. package/dist/core/policy.d.ts +94 -0
  58. package/dist/core/policy.js +122 -0
  59. package/dist/core/policy.js.map +1 -0
  60. package/dist/core/ports.d.ts +52 -0
  61. package/dist/core/ports.js +94 -0
  62. package/dist/core/ports.js.map +1 -0
  63. package/dist/core/presets.d.ts +25 -0
  64. package/dist/core/presets.js +86 -0
  65. package/dist/core/presets.js.map +1 -0
  66. package/dist/core/procscan.d.ts +111 -0
  67. package/dist/core/procscan.js +342 -0
  68. package/dist/core/procscan.js.map +1 -0
  69. package/dist/core/retention.d.ts +97 -0
  70. package/dist/core/retention.js +322 -0
  71. package/dist/core/retention.js.map +1 -0
  72. package/dist/core/sleep.d.ts +51 -0
  73. package/dist/core/sleep.js +99 -0
  74. package/dist/core/sleep.js.map +1 -0
  75. package/dist/core/tree-ledger.d.ts +39 -0
  76. package/dist/core/tree-ledger.js +92 -0
  77. package/dist/core/tree-ledger.js.map +1 -0
  78. package/dist/core/types.d.ts +21 -0
  79. package/dist/core/types.js +6 -0
  80. package/dist/core/types.js.map +1 -0
  81. package/dist/core/units.d.ts +20 -0
  82. package/dist/core/units.js +69 -0
  83. package/dist/core/units.js.map +1 -0
  84. package/dist/core/upkeep.d.ts +90 -0
  85. package/dist/core/upkeep.js +176 -0
  86. package/dist/core/upkeep.js.map +1 -0
  87. package/dist/core/util.d.ts +46 -0
  88. package/dist/core/util.js +168 -0
  89. package/dist/core/util.js.map +1 -0
  90. package/dist/core/version.d.ts +39 -0
  91. package/dist/core/version.js +144 -0
  92. package/dist/core/version.js.map +1 -0
  93. package/dist/core/worktree.d.ts +51 -0
  94. package/dist/core/worktree.js +309 -0
  95. package/dist/core/worktree.js.map +1 -0
  96. package/dist/daemon/budget.d.ts +155 -0
  97. package/dist/daemon/budget.js +288 -0
  98. package/dist/daemon/budget.js.map +1 -0
  99. package/dist/daemon/election.d.ts +9 -0
  100. package/dist/daemon/election.js +140 -0
  101. package/dist/daemon/election.js.map +1 -0
  102. package/dist/daemon/engine.d.ts +1703 -0
  103. package/dist/daemon/engine.js +5305 -0
  104. package/dist/daemon/engine.js.map +1 -0
  105. package/dist/daemon/index.d.ts +2 -0
  106. package/dist/daemon/index.js +412 -0
  107. package/dist/daemon/index.js.map +1 -0
  108. package/dist/daemon/proxy.d.ts +108 -0
  109. package/dist/daemon/proxy.js +615 -0
  110. package/dist/daemon/proxy.js.map +1 -0
  111. package/dist/daemon/supervisor.d.ts +101 -0
  112. package/dist/daemon/supervisor.js +493 -0
  113. package/dist/daemon/supervisor.js.map +1 -0
  114. package/dist/drivers/appliances.d.ts +11 -0
  115. package/dist/drivers/appliances.js +174 -0
  116. package/dist/drivers/appliances.js.map +1 -0
  117. package/dist/drivers/datastores.d.ts +144 -0
  118. package/dist/drivers/datastores.js +775 -0
  119. package/dist/drivers/datastores.js.map +1 -0
  120. package/dist/drivers/preview.d.ts +69 -0
  121. package/dist/drivers/preview.js +605 -0
  122. package/dist/drivers/preview.js.map +1 -0
  123. package/dist/drivers/types.d.ts +70 -0
  124. package/dist/drivers/types.js +13 -0
  125. package/dist/drivers/types.js.map +1 -0
  126. package/package.json +63 -4
  127. package/schema/backlot.schema.json +549 -0
  128. package/schema/runly.schema.json +556 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Process identity and orphan discovery.
3
+ *
4
+ * Runly spawns each service detached (its own process group) and kills the
5
+ * group on teardown — but a daemon that dies ungracefully leaves those groups
6
+ * running with nothing tracking them. Recorded pids alone are not enough to
7
+ * clean up afterwards: by the time a new daemon reads them they may have been
8
+ * recycled by the OS onto an unrelated process, and signalling those is worse
9
+ * than leaking.
10
+ *
11
+ * So every supervised service carries a tag in its environment (see
12
+ * `serviceTag`), and every recorded pid carries the kernel's start-time for
13
+ * that pid. Together they answer the two questions cleanup needs:
14
+ *
15
+ * - "is pid N still the process I recorded?" -> `sameProcess`
16
+ * - "which runly processes has everyone lost?" -> `scanTagged`
17
+ *
18
+ * Identity works on every platform: /proc where it exists, `ps -o lstart=`
19
+ * otherwise, so a recycled pid is never mistaken for the process we recorded.
20
+ * Orphan SCANNING needs to read other processes' environments and so is
21
+ * Linux-only; elsewhere `pool gc` reports unsupported rather than guessing.
22
+ * That asymmetry is deliberate — skipping a sweep is safe, signalling a
23
+ * stranger's process is not.
24
+ */
25
+ import { readFileSync, readdirSync, readlinkSync } from 'node:fs';
26
+ import { execFileSync } from 'node:child_process';
27
+ export const ENV_TAG = 'BACKLOT_ENV_ID';
28
+ export const SERVICE_TAG = 'BACKLOT_SERVICE';
29
+ /** Scopes a tag to one state root so parallel installs never reap each other. */
30
+ export const ROOT_TAG = 'BACKLOT_STATE_ROOT';
31
+ /** The environment every supervised service is spawned with. */
32
+ /** The service name `runly exec`'s command is tagged with; never reaped (see scanTagged). */
33
+ export const EXEC_SERVICE = 'exec';
34
+ export function serviceTag(envId, service, stateRoot) {
35
+ return { [ENV_TAG]: envId, [SERVICE_TAG]: service, [ROOT_TAG]: stateRoot };
36
+ }
37
+ export const procScanSupported = () => process.platform === 'linux';
38
+ /**
39
+ * Field 22 of /proc/<pid>/stat: the process start time in clock ticks since
40
+ * boot. Unique per pid *life*, so (pid, startTime) survives pid reuse.
41
+ *
42
+ * comm (field 2) is an arbitrary string wrapped in parens and may itself
43
+ * contain spaces or parens, so parse after the LAST ')' rather than splitting
44
+ * the whole line.
45
+ */
46
+ export function startTime(pid) {
47
+ if (!procScanSupported())
48
+ return darwinStartTime(pid);
49
+ try {
50
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
51
+ const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
52
+ const ticks = Number(rest[19]); // field 22 == index 19 after pid and comm
53
+ return Number.isFinite(ticks) ? ticks : undefined;
54
+ }
55
+ catch {
56
+ return undefined; // gone, or not ours to read
57
+ }
58
+ }
59
+ /**
60
+ * A portable identity for platforms without /proc.
61
+ *
62
+ * Without this, non-Linux recovery fell back to a bare liveness check, which is
63
+ * exactly the pid-reuse hazard the start time exists to close — and macOS is
64
+ * the platform runly's primary users are on. `ps -o lstart=` reports a
65
+ * process's start wall-clock to the second, which is enough to distinguish a
66
+ * recycled pid from the process we recorded.
67
+ *
68
+ * Returns undefined if ps is unavailable or the pid is gone; callers treat an
69
+ * un-verifiable pid as un-signallable, so failing to read is safe.
70
+ */
71
+ function darwinStartTime(pid) {
72
+ try {
73
+ const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
74
+ encoding: 'utf8',
75
+ stdio: ['ignore', 'pipe', 'ignore'],
76
+ timeout: 2000,
77
+ }).trim();
78
+ if (!out)
79
+ return undefined;
80
+ const ms = Date.parse(out);
81
+ return Number.isFinite(ms) ? ms : undefined;
82
+ }
83
+ catch {
84
+ return undefined; // gone, not ours to read, or no ps
85
+ }
86
+ }
87
+ /**
88
+ * Field 5 of /proc/<pid>/stat: the process group id.
89
+ *
90
+ * `sh -c <cmd>` does NOT reliably exec: dash forks for anything non-trivial,
91
+ * so the pid runly records is often only the wrapper, and the real server is
92
+ * a sibling in the same group. Killing "the leader" and then checking the
93
+ * leader's liveness therefore reports success while the server keeps running —
94
+ * group membership is the only honest liveness question.
95
+ */
96
+ export function processGroup(pid) {
97
+ if (!procScanSupported())
98
+ return undefined;
99
+ try {
100
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
101
+ const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
102
+ const pgrp = Number(rest[2]); // field 5 == index 2 after pid and comm
103
+ return Number.isFinite(pgrp) ? pgrp : undefined;
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ /**
110
+ * Is ANY process still in this group?
111
+ *
112
+ * Without /proc, `kill(-pgid, 0)` is still a genuine GROUP query — ESRCH means
113
+ * the group is empty. Checking the leader instead would report "gone" the
114
+ * moment an `sh -c` wrapper exited, while the real server kept running, which
115
+ * is the whole failure this function exists to detect.
116
+ */
117
+ export function groupAlive(pgid) {
118
+ // `kill(-pgid, 0)` is a SINGLE syscall and an authoritative group query:
119
+ // ESRCH means no process remains in the group. An earlier version walked all
120
+ // of /proc here, which is O(processes) file reads per call — and
121
+ // killGroupVerified polls this every 50ms, so on a busy host a single reap
122
+ // issued hundreds of thousands of reads and stalled the daemon.
123
+ try {
124
+ process.kill(-pgid, 0);
125
+ return true;
126
+ }
127
+ catch (err) {
128
+ // EPERM: the group exists but belongs to another user — still alive.
129
+ return err.code === 'EPERM';
130
+ }
131
+ }
132
+ /** Is this pid alive at all? Says nothing about whose it is. */
133
+ export function isAlive(pid) {
134
+ try {
135
+ process.kill(pid, 0);
136
+ return true;
137
+ }
138
+ catch (err) {
139
+ // EPERM means alive but owned by another user — still alive.
140
+ return err.code === 'EPERM';
141
+ }
142
+ }
143
+ /**
144
+ * Is pid N still the same process that was recorded with `recordedStart`?
145
+ *
146
+ * With no recorded start time (a journal written by an older runly, or a
147
+ * non-Linux host) this can only fall back to liveness, which is exactly the
148
+ * pid-reuse hazard this exists to close — so callers should treat an
149
+ * un-verifiable pid as un-signallable where a mistake would be costly.
150
+ */
151
+ export function sameProcess(pid, recordedStart) {
152
+ if (!isAlive(pid))
153
+ return false;
154
+ if (recordedStart === undefined)
155
+ return true;
156
+ const current = startTime(pid);
157
+ if (current === undefined)
158
+ return false;
159
+ return current === recordedStart;
160
+ }
161
+ /** Read a process's environment as a map, or undefined if unreadable. */
162
+ function readEnviron(pid) {
163
+ try {
164
+ const raw = readFileSync(`/proc/${pid}/environ`, 'utf8');
165
+ const out = {};
166
+ for (const entry of raw.split('\0')) {
167
+ if (!entry)
168
+ continue;
169
+ const eq = entry.indexOf('=');
170
+ if (eq > 0)
171
+ out[entry.slice(0, eq)] = entry.slice(eq + 1);
172
+ }
173
+ return out;
174
+ }
175
+ catch {
176
+ return undefined; // exited mid-scan, or another user's process
177
+ }
178
+ }
179
+ /**
180
+ * Every live process tagged as belonging to this state root.
181
+ *
182
+ * Note this finds *descendants* too — an `ng serve` inherits the tag from the
183
+ * `sh -c` wrapper runly spawned, which is the whole point: after the wrapper
184
+ * dies the child is still identifiable.
185
+ */
186
+ export function scanTagged(stateRoot) {
187
+ if (!procScanSupported())
188
+ return [];
189
+ const found = [];
190
+ let entries;
191
+ try {
192
+ entries = readdirSync('/proc');
193
+ }
194
+ catch {
195
+ return [];
196
+ }
197
+ for (const entry of entries) {
198
+ const pid = Number(entry);
199
+ if (!Number.isInteger(pid) || pid <= 0)
200
+ continue;
201
+ const env = readEnviron(pid);
202
+ if (!env)
203
+ continue;
204
+ if (env[ROOT_TAG] !== stateRoot)
205
+ continue;
206
+ const envId = env[ENV_TAG];
207
+ if (!envId)
208
+ continue;
209
+ // `runly exec`'s command (decision 0040) carries the tag so it can be told
210
+ // apart, but it is the CALLER's process, run by the CLI: no daemon path
211
+ // (gc, recycle, shutdown) may reap it as an environment's orphan.
212
+ if (env[SERVICE_TAG] === EXEC_SERVICE)
213
+ continue;
214
+ const st = startTime(pid);
215
+ if (st === undefined)
216
+ continue; // exited between the two reads
217
+ found.push({ pid, envId, service: env[SERVICE_TAG] ?? '?', startTime: st });
218
+ }
219
+ return found;
220
+ }
221
+ /** Carried by `runly db with`'s command and its descendants: the copy it runs against (decision 0039). */
222
+ export const DB_COPY_TAG = 'BACKLOT_DB_COPY';
223
+ /** The environment `runly db with` runs its command with, so the daemon can find it again. */
224
+ export function dbCopyTag(copy, stateRoot) {
225
+ return { [DB_COPY_TAG]: copy, [ROOT_TAG]: stateRoot };
226
+ }
227
+ /**
228
+ * Every live process of this state root running against database copy
229
+ * `copy` — `runly db with`'s command and whatever it started (the tag is
230
+ * inherited). Linux only, like `scanTagged`; elsewhere the recorded pid is
231
+ * all there is.
232
+ */
233
+ export function scanDbCopy(copy, stateRoot) {
234
+ if (!procScanSupported())
235
+ return [];
236
+ const found = [];
237
+ let entries;
238
+ try {
239
+ entries = readdirSync('/proc');
240
+ }
241
+ catch {
242
+ return [];
243
+ }
244
+ for (const entry of entries) {
245
+ const pid = Number(entry);
246
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid)
247
+ continue;
248
+ const env = readEnviron(pid);
249
+ if (!env || env[ROOT_TAG] !== stateRoot || env[DB_COPY_TAG] !== copy)
250
+ continue;
251
+ const st = startTime(pid);
252
+ if (st !== undefined)
253
+ found.push({ pid, startTime: st });
254
+ }
255
+ return found;
256
+ }
257
+ /**
258
+ * Field 7 of /proc/<pid>/stat: the controlling terminal, 0 for none.
259
+ *
260
+ * Runly spawns every service with `stdio: ['ignore', 'pipe', 'pipe']` and
261
+ * detached, so it and all its descendants have NO controlling terminal. A human's
262
+ * interactive shell always has one. That difference is the only cheap evidence
263
+ * available for "is a person sitting in front of this process".
264
+ */
265
+ function hasControllingTty(pid) {
266
+ try {
267
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
268
+ const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
269
+ const ttyNr = Number(rest[4]); // field 7 == index 4 after pid and comm
270
+ return Number.isFinite(ttyNr) && ttyNr !== 0;
271
+ }
272
+ catch {
273
+ return false; // unreadable — the caller's other guards decide
274
+ }
275
+ }
276
+ /**
277
+ * Every live process whose working directory sits inside `prefix` AND which has
278
+ * no controlling terminal.
279
+ *
280
+ * The tag scan above misses a descendant that scrubbed its environment (a
281
+ * process launched through `env -i`, a re-exec that rebuilds environ, some
282
+ * language runtimes' worker pools). Those still have to live SOMEWHERE. Under
283
+ * the old projection that somewhere was the environment's own tree; since
284
+ * decision 0032 services run in the caller's worktree, where cwd proves
285
+ * nothing, so the engine only ever points this at an environment's PRIVATE
286
+ * directory (see Engine.reapEnvTree).
287
+ *
288
+ * Linux keeps the cwd link readable after the directory is unlinked, appending
289
+ * " (deleted)" to the target — which is precisely the shape of the leak this
290
+ * exists to catch: hundreds of service children still running out of an
291
+ * environment tree that was removed a day earlier. The suffix is stripped so a
292
+ * deleted tree still matches its prefix.
293
+ *
294
+ * The tty exclusion is not a nicety. cwd is NOT proof of ownership — a developer
295
+ * who runs `cd <env-tree>` to look around matches this scan exactly, and callers
296
+ * signal a matched process's whole GROUP, so without the exclusion a teardown
297
+ * would kill that person's shell and every job in it. That is the one outcome
298
+ * this module exists to prevent (see the header): skipping a sweep is safe,
299
+ * signalling a stranger's process is not. A runly service can never be
300
+ * excluded by it, because it never has a terminal to begin with.
301
+ *
302
+ * Even so, deliberately NOT used on the quiesce path, and never on a worktree:
303
+ * only teardown, which is about to delete the environment's private directory
304
+ * anyway, may reap by cwd — and only inside that directory.
305
+ */
306
+ export function scanByCwd(prefix) {
307
+ if (!procScanSupported())
308
+ return [];
309
+ const self = process.pid;
310
+ const found = [];
311
+ let entries;
312
+ try {
313
+ entries = readdirSync('/proc');
314
+ }
315
+ catch {
316
+ return [];
317
+ }
318
+ for (const entry of entries) {
319
+ const pid = Number(entry);
320
+ if (!Number.isInteger(pid) || pid <= 0 || pid === self)
321
+ continue;
322
+ let cwd;
323
+ try {
324
+ cwd = readlinkSync(`/proc/${pid}/cwd`);
325
+ }
326
+ catch {
327
+ continue; // exited, or another user's process — not ours to touch
328
+ }
329
+ const path = cwd.endsWith(' (deleted)') ? cwd.slice(0, -' (deleted)'.length) : cwd;
330
+ // Prefix match on a path BOUNDARY, so `…/env-1` never matches `…/env-10`.
331
+ if (path !== prefix && !path.startsWith(prefix.endsWith('/') ? prefix : prefix + '/'))
332
+ continue;
333
+ if (hasControllingTty(pid))
334
+ continue; // a person is using this one
335
+ const st = startTime(pid);
336
+ if (st === undefined)
337
+ continue; // exited between the two reads
338
+ found.push({ pid, startTime: st, cwd });
339
+ }
340
+ return found;
341
+ }
342
+ //# sourceMappingURL=procscan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procscan.js","sourceRoot":"","sources":["../../src/core/procscan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ import type { Journal } from './journal.js';
2
+ import type { Policy } from './policy.js';
3
+ /**
4
+ * Service logs past the cap are rotated once (decision 0038): `<name>.log`
5
+ * becomes `<name>.log.1`, replacing the previous rotation. The writer rotates
6
+ * as it writes; this is the backstop for a file it never reached (a writer
7
+ * from an older daemon, or a log that grew between restarts). Only `*.log`
8
+ * files are touched.
9
+ */
10
+ export declare function truncateLogs(p: Policy, root?: string): number;
11
+ /**
12
+ * Which stacks a template may still serve: anything a journal row names, or
13
+ * whose worktree is still on disk (decision 0037). A stack whose worktree is
14
+ * gone and that no row names keeps no "current" template — nobody can bind
15
+ * it again.
16
+ */
17
+ export interface TemplateRefs {
18
+ /** `<stack>/<file>` of every template an environment or copy was restored from. */
19
+ referenced: ReadonlySet<string>;
20
+ /** False when the stack can never be bound again. */
21
+ stackAlive: (stackId: string) => boolean;
22
+ /**
23
+ * Where a template's recorded drop command runs: it comes from the
24
+ * manifest and is written to run in the repo (`node scripts/db.cjs drop …`).
25
+ * The stack's own worktree while it exists, else a live worktree of the same
26
+ * project (same manifest name; from the journal or a sibling's `.root`),
27
+ * else the templates root.
28
+ */
29
+ dropCwd?: (stackId: string) => string;
30
+ }
31
+ export type TemplateVerdict = {
32
+ f: string;
33
+ keep: true;
34
+ why: 'current' | 'referenced' | 'grace';
35
+ } | {
36
+ f: string;
37
+ keep: false;
38
+ why: 'superseded' | 'dead' | 'private' | 'duplicate';
39
+ };
40
+ /**
41
+ * What retention (and `pool doctor`, without the grace) does with each
42
+ * template in one templates dir (decisions 0037, 0044):
43
+ *
44
+ * - per datastore and preset, the newest `keep` are current — in a stack dir
45
+ * that can be bound again, or in a `<name>@shared` dir while any worktree of
46
+ * that name can;
47
+ * - a template a row or a live worktree's record references stays;
48
+ * - a private (`--pristine`) template is never "current": it stays only while
49
+ * referenced;
50
+ * - a per-worktree template whose file name the shared dir also holds is a
51
+ * duplicate of it (the migration adopted it), never "current" either;
52
+ * - anything else goes once older than `grace`.
53
+ */
54
+ export declare function templateVerdicts(dir: string, files: Array<{
55
+ f: string;
56
+ mtime: number;
57
+ }>, refs: TemplateRefs, keepNewest: number, graceMs: number, sharedFiles?: ReadonlySet<string>): TemplateVerdict[];
58
+ /** The template files of one dir with their mtimes (dot-files — `.root` — excluded). */
59
+ export declare function templateFiles(dir: string): Array<{
60
+ f: string;
61
+ mtime: number;
62
+ }>;
63
+ /** The file names in the shared dir of `dir`'s project (empty for a shared dir itself). */
64
+ export declare function sharedFilesFor(dir: string, root?: string): Set<string>;
65
+ /**
66
+ * Templates are collected by reference (decisions 0037, 0044): see
67
+ * `templateVerdicts`. A marker for a server-side template is dropped WITH its
68
+ * database (its persisted drop command), unless another marker still names it.
69
+ */
70
+ export declare function pruneTemplates(p: Pick<Policy, 'templatesKeep'> & Partial<Pick<Policy, 'templateGraceMs'>>, root?: string, refs?: TemplateRefs): Promise<number>;
71
+ /**
72
+ * Per-worktree state (decision 0032 — the trigger-file hash cache and the upkeep
73
+ * ledger) for a worktree that no longer exists. It must outlive every
74
+ * environment, because `runly warm` writes it for a worktree with none; so it
75
+ * goes only when its recorded root is gone AND no environment still names the
76
+ * stack. A missing or unreadable record is left alone: it proves nothing.
77
+ */
78
+ export declare function pruneWorktreeState(journal: Journal, root?: string): number;
79
+ /**
80
+ * What the journal says about templates (decision 0037): the ones rows
81
+ * reference, and which stacks are alive. A stack no row names is alive while
82
+ * the worktree it was recorded for exists — recorded by its worktree state
83
+ * (`worktrees/<stack>/*.json`) or by the `.root` its first bake wrote next to
84
+ * its templates. A stack with no record at all is NOT alive (0.19): its
85
+ * worktree records were pruned because the worktree was gone, and treating it
86
+ * as alive kept the templates of every pruned worktree forever.
87
+ *
88
+ * Shared templates (decision 0044): `<name>@shared` is alive while any stack
89
+ * of that name is; a live worktree's `templates.json` (the template each of
90
+ * its datastores was last restored from) counts as a reference.
91
+ */
92
+ export declare function templateRefs(journal: Journal, root?: string, templates?: string): TemplateRefs;
93
+ export declare function retentionSweep(journal: Journal, p: Policy): Promise<{
94
+ logs: number;
95
+ templates: number;
96
+ worktrees: number;
97
+ }>;