@stablekernel/pi-background-run 0.5.0 → 0.6.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.
- package/README.md +452 -59
- package/extension/digestPresets.ts +316 -0
- package/extension/index.ts +2315 -478
- package/package.json +10 -3
- package/skill/digest-config/SKILL.md +117 -0
- package/skill/run-bg/SKILL.md +81 -24
- package/extension/index.test.ts +0 -2569
package/README.md
CHANGED
|
@@ -8,10 +8,14 @@ pi-background-run **wakes the live agent session** so it proactively reads a
|
|
|
8
8
|
condensed digest of the results and continues — no polling, no human intervention.
|
|
9
9
|
|
|
10
10
|
Built as a [pi](https://github.com/earendil-works/pi-coding-agent) extension. No
|
|
11
|
-
shell runner
|
|
11
|
+
shell runner and no external daemon — the extension spawns the job in-process,
|
|
12
12
|
detects completion via the child `exit` event, and calls `pi.sendUserMessage` to wake
|
|
13
13
|
the agent. The log file is self-describing (full output + a trailing
|
|
14
|
-
`__BGRUN_EXIT__=N` marker), so exit codes survive pi restarting.
|
|
14
|
+
`__BGRUN_EXIT__=N` marker), so exit codes survive pi restarting. Two small pieces
|
|
15
|
+
exist beyond the spawn: a 30s timer that only re-checks jobs whose live child handle
|
|
16
|
+
is gone (reconstructed from a restart, or adopted from another session), and a
|
|
17
|
+
`.last-clean` marker that throttles the **global** orphan sweep (the
|
|
18
|
+
session-scoped sweep is unthrottled).
|
|
15
19
|
|
|
16
20
|
## Install
|
|
17
21
|
|
|
@@ -19,11 +23,9 @@ the agent. The log file is self-describing (full output + a trailing
|
|
|
19
23
|
pi install npm:pi-background-run
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pi install npm:@stablekernel/pi-background-run
|
|
26
|
-
```
|
|
26
|
+
The scoped alias `@stablekernel/pi-background-run` is the same package (permanent
|
|
27
|
+
namespace claim, published in lockstep). Prefer the unscoped name; the alias is
|
|
28
|
+
not deprecated, so both stay installable and receive every release.
|
|
27
29
|
|
|
28
30
|
Restart pi after install so the extension loads.
|
|
29
31
|
|
|
@@ -32,10 +34,10 @@ Restart pi after install so the extension loads.
|
|
|
32
34
|
| Tool | Purpose |
|
|
33
35
|
| ------ | --------- |
|
|
34
36
|
| `bgrun` | Launch a command detached in the background. Optional `name` gives the job a short human-readable label. Returns `started: <job-id>` immediately. Wakes the session automatically on completion. |
|
|
35
|
-
| `bgstatus` | Show job status. With an id: any job's state + exit code. Without: this session's running jobs (finished jobs hidden by default — pass `includeDone: true` or set `showCompletedJobs`).
|
|
36
|
-
| `bgtail` | Read the newest lines of a job's log (default 40), **condensed for context**: ANSI escapes stripped, repeated lines collapsed, long lines and total size capped. First read = full last-N tail; repeat reads return **only lines appended since your last read** (delta tailing) — polling a running job never re-pays for lines already seen. Pass `raw: true` for the unprocessed last-N window (still advances the bookmark). |
|
|
37
|
-
| `bggrep` | Regex search over a job's log: line-numbered matches, optional `context` lines, capped (~50 matches, ~
|
|
38
|
-
| `bgclean` | Remove old job logs. **Default scope: this session's jobs only** — other sessions' logs are untouched. Pass `all: true` to sweep
|
|
37
|
+
| `bgstatus` | Show job status. With an id: any job's state + exit code. Without: this session's running jobs (finished jobs hidden by default — pass `includeDone: true` or set `showCompletedJobs`). Other sessions' *running* jobs are listed only when `adoptForeignJobs` is enabled; finished foreign logs from the shared dir can also appear when finished jobs are included. |
|
|
38
|
+
| `bgtail` | Read the newest lines of a job's log (default 40; it reads the log's **last 2 MB** — widen with `bytes`, max 64 MiB), **condensed for context**: ANSI escapes stripped, repeated lines collapsed, long lines and total size capped. First read = full last-N tail; repeat reads return **only lines appended since your last read** (delta tailing) — polling a running job never re-pays for lines already seen. Pass `raw: true` for the unprocessed last-N window (still advances the bookmark). |
|
|
39
|
+
| `bggrep` | Regex search over the **last 2 MB** of a job's log (`bytes` widens the window, max 64 MiB): line-numbered matches, optional `context` lines, each line pre-truncated to 10 000 chars before matching, results capped (~50 matches, ~8KB) and condensed. Resolves the job id to the configured jobs dir itself — no log path to reconstruct. `ctx_execute_file` can read the same file (it takes an absolute path; only your Read-deny rules apply), but it needs that path. Matching runs under a wall-clock budget ([Bounded matching](#bounded-matching)). With no `pattern`, a generic failure-signature default is used (override it — convenience, not guarantee). |
|
|
40
|
+
| `bgclean` | Remove old job logs. **Default scope: this session's jobs only** — other sessions' logs are untouched — and it also drops stale per-project digest markers (`.bgrun-used-*`, `.digest-nudge-*`) in the session's jobs dir (markers are not session data). Pass `all: true` to sweep every shared jobs dir — under the project-local default that is the project's dir plus the machine-global one, while an explicit absolute `jobsDir` is swept alone — and do the same marker sweep across them. Retention: `cleanupDays` config (7 days); `days` must be a positive number (`days: 0` is rejected rather than purging everything). Never removes a running job's log. |
|
|
39
41
|
|
|
40
42
|
## Slash commands
|
|
41
43
|
|
|
@@ -55,16 +57,20 @@ wake messages) is the agent's workflow.
|
|
|
55
57
|
|
|
56
58
|
## Roadmap / not provided
|
|
57
59
|
|
|
58
|
-
-
|
|
60
|
+
- Deprecated: the machine-global jobs dir (`PI_BGRUN_GLOBAL_DIR`, `~/.pi-bgrun/jobs`) — see [deprecation](#deprecated-machine-global-jobs-dir). Supported until a future major.
|
|
61
|
+
- `bgkill` — not implemented; to stop a running job, use `kill -- -<pid>` (kill the process group — the child is spawned detached). The pid is the last `--`-separated segment of the job id (e.g. `unit-tests-1726680000-12345` → pid `12345`); it is not shown as a separate field in `bgstatus` output.
|
|
59
62
|
- `bgwait` — not implemented; the wake mechanism makes blocking on a job unnecessary in the normal flow.
|
|
60
63
|
|
|
61
64
|
## How it works
|
|
62
65
|
|
|
63
66
|
```text
|
|
64
67
|
agent calls bgrun(command: "make test-short", name: "unit-tests")
|
|
65
|
-
→ extension resolves log path: <jobsDir>/<slug>-<ts>-<pid>.log (default ~/.pi-bgrun/jobs/)
|
|
66
|
-
→ spawn('sh', ['-c',
|
|
68
|
+
→ extension resolves log path: <jobsDir>/<slug>-<ts>-<pid>.log (default <project>/.pi-bgrun/jobs/ in a repo, else ~/.pi-bgrun/jobs/)
|
|
69
|
+
→ spawn('sh', ['-c', <wrapper>, 'bgrun', '<cmd>'],
|
|
67
70
|
{ stdio: ['ignore', logFd, logFd], detached: true }).unref()
|
|
71
|
+
<wrapper> = the output-ceiling pipeline (see "Log size ceiling"), or the
|
|
72
|
+
uncapped one-liner 'sh -c "$1"; ec=$?; printf "\\n__BGRUN_EXIT__=%d\\n" "$ec"; exit "$ec"'
|
|
73
|
+
when the ceiling is disabled (maxLogBytes: 0)
|
|
68
74
|
→ records job in-memory + appends a bgrun-job entry to the session
|
|
69
75
|
→ returns "started: <job-id>"
|
|
70
76
|
|
|
@@ -83,8 +89,9 @@ exit code even after a restart.
|
|
|
83
89
|
|
|
84
90
|
## Reading results without flooding context
|
|
85
91
|
|
|
86
|
-
Two-tier read model — the log file stays
|
|
87
|
-
|
|
92
|
+
Two-tier read model — the log file itself stays on disk, capped (see
|
|
93
|
+
[log size ceiling](#log-size-ceiling)), for deep analysis; only bounded digests
|
|
94
|
+
ever enter the conversation:
|
|
88
95
|
|
|
89
96
|
- **Quick peek:** `bgtail <id>` — condensed newest lines (ANSI stripped, repeats
|
|
90
97
|
collapsed, ~2KB/line and ~8KB caps). The first read is the last-40-lines tail; each later
|
|
@@ -92,31 +99,108 @@ only bounded digests ever enter the conversation:
|
|
|
92
99
|
free. The wake message itself already carries the exit code and the log's
|
|
93
100
|
last line, so many turns need no follow-up read at all.
|
|
94
101
|
- **Pattern search:** `bggrep <id> [pattern] [context]` — line-numbered matches,
|
|
95
|
-
capped and condensed (~50 matches, ~2KB/line, ~8KB);
|
|
96
|
-
|
|
102
|
+
capped and condensed (~50 matches, ~2KB/line, ~8KB); takes the job id, so
|
|
103
|
+
there is no log path to reconstruct. Searches the **last 2 MB** by default —
|
|
104
|
+
pass `bytes` to widen (max 64 MiB), or use `ctx_execute_file` on the path for
|
|
105
|
+
whole-file code-based analysis. Pass your own pattern when you know the log's
|
|
106
|
+
format. A **wider window costs latency and memory, not context**: the returned
|
|
107
|
+
matches stay capped either way.
|
|
97
108
|
- **Whole-log analysis:** `ctx_execute_file` on the job's log path (reachable
|
|
98
109
|
when logs are project-local) to extract only failure lines. Never `cat` or
|
|
99
|
-
`Read` a full bgrun log.
|
|
110
|
+
`Read` a full bgrun log. The sandbox keeps the file's bytes out of context —
|
|
111
|
+
only your script's **stdout** enters it — so print aggregates and capped
|
|
112
|
+
slices (`fails.slice(0, 40)`), never the content. With a 64 MiB-ceiling log,
|
|
113
|
+
an unsliced `console.log(FILE_CONTENT)` is the one way this path becomes the
|
|
114
|
+
dump it exists to avoid; use `bgtail`/`bggrep` first, and this third.
|
|
100
115
|
|
|
101
116
|
**Why `bggrep` instead of `bash grep` on the log?** A bash grep's output is
|
|
102
117
|
uncapped — a retry-storm log can dump thousands of matching lines straight
|
|
103
118
|
into context, and safety depends on remembering `| head` on every call.
|
|
104
|
-
`bggrep` is bounded by design (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
`bggrep` is bounded by design (last 2 MB of the log by default — `bytes` widens
|
|
120
|
+
it, max 64 MiB — per-line 10 000-char pre-truncation before matching, ~50
|
|
121
|
+
matches, ~8KB), takes the job id instead of
|
|
122
|
+
a reconstructed log path (no shell-quoting of the regex), resolves the job id to the
|
|
123
|
+
configured jobs dir itself (no path to reconstruct), and reports match counts,
|
|
124
|
+
line numbers, and skip markers. Plain `grep` is fine only for a one-off search you know is tiny.
|
|
125
|
+
|
|
126
|
+
### Bounded matching
|
|
127
|
+
|
|
128
|
+
`bggrep` takes a **caller-supplied regex**, and a pathological one (for example
|
|
129
|
+
`(a+)+$`) can backtrack exponentially. V8 has no regex step limit and cannot
|
|
130
|
+
interrupt a regex running on the main thread, so the match loop runs in a
|
|
131
|
+
worker with a wall-clock budget (default `2000ms`, override with
|
|
132
|
+
`PI_BGRUN_GREP_TIMEOUT_MS`). If the budget is exceeded the worker is terminated
|
|
133
|
+
and `bggrep` returns an error — **a runaway pattern fails, it never hangs the
|
|
134
|
+
session.** Normal patterns and logs finish far inside the budget; worker
|
|
135
|
+
startup adds a few tens of milliseconds per call.
|
|
136
|
+
|
|
137
|
+
### Log size ceiling
|
|
138
|
+
|
|
139
|
+
stdout+stderr used to go straight to the log file with no write bound, so a
|
|
140
|
+
runaway job (`yes`, a spew loop, a pathological build) could fill the disk and
|
|
141
|
+
take the machine down. Job logs are now capped (`maxLogBytes` / `PI_BGRUN_MAX_LOG_BYTES`,
|
|
142
|
+
default **64 MiB**, `0` = unlimited):
|
|
143
|
+
|
|
144
|
+
- The cap keeps the **first** N bytes. There is no portable in-tree way to keep
|
|
145
|
+
the tail — a ring buffer needs a helper binary, and rewriting the file breaks
|
|
146
|
+
the readers that depend on the exit marker staying last. A job past 64 MiB is
|
|
147
|
+
almost always a runaway, so the head is the useful part.
|
|
148
|
+
- The ceiling lives **inside the detached process tree**, so it still holds
|
|
149
|
+
after pi exits or crashes — it is not a pi-side watchdog.
|
|
150
|
+
- The job is **not** killed, and its real exit code is preserved: bytes past the
|
|
151
|
+
cap are drained and discarded instead of SIGPIPE'ing the producer into `141`.
|
|
152
|
+
- It is **not silent**. The log carries
|
|
153
|
+
`__BGRUN_TRUNC__ output truncated: kept the first <N> bytes` on the line
|
|
154
|
+
before the exit marker, and the marker line itself carries the flag
|
|
155
|
+
(`__BGRUN_EXIT__=0 truncated=67108864`). Both are reserved `__BGRUN_*__` lines
|
|
156
|
+
that content readers filter exactly like the exit marker, and readers classify
|
|
157
|
+
the notice by that **marker flag**, never by matching text — a command that
|
|
158
|
+
echoes a notice-shaped line cannot make its own log look capped, and cannot
|
|
159
|
+
get its own output discounted as wrapper bookkeeping either. If the ceiling
|
|
160
|
+
could not be installed at all (`mkfifo` unavailable, so the job ran uncapped)
|
|
161
|
+
the log says that too — `__BGRUN_NOCAP__ log ceiling unavailable`, with
|
|
162
|
+
`nocap=1` in the marker — so "uncapped" is never indistinguishable from
|
|
163
|
+
"output was that small". Every surface the agent reads is labelled: the wake's
|
|
164
|
+
Stats line gains `log truncated at 64 MiB`,
|
|
165
|
+
`bgtail` and `bggrep` append a note and report `truncatedAtBytes` in their
|
|
166
|
+
details, and a configured digest scorecard is **skipped** rather than run
|
|
167
|
+
against a log that lost its end — summaries and failure lists live at the end,
|
|
168
|
+
so its numbers would be confidently wrong. Treat a skipped digest on a capped
|
|
169
|
+
job as "unknown", not "no failures".
|
|
170
|
+
- Reading a capped log stays readable-whole: the widest `bytes` window is the
|
|
171
|
+
ceiling **plus 4 KiB** (not the ceiling itself, which a capped log always
|
|
172
|
+
exceeds by its notices and marker), so `bytes: 67108864` still spans the whole
|
|
173
|
+
kept log. Windows are additionally limited to their last 500 000 lines —
|
|
174
|
+
materializing a 64 MiB window of one-character lines would cost gigabytes of
|
|
175
|
+
strings — and when that line bound trims a window, `bgtail`/`bggrep` say so in
|
|
176
|
+
the same labelled way instead of silently answering from a subset.
|
|
177
|
+
- Maintainer rationale — why a fifo, why the *first* bytes, which alternatives
|
|
178
|
+
were measured and rejected: [`docs/log-size-ceiling.md`](docs/log-size-ceiling.md).
|
|
179
|
+
- Cost: a capped job runs through one copier process (`perl` where available,
|
|
180
|
+
else `dd`/`head`) reading the job through a fifo, plus a bounded drain wait —
|
|
181
|
+
a few tens of milliseconds of job startup, no steady-state overhead. The
|
|
182
|
+
copier is also what drains the stream past the cap, so the producer is never
|
|
183
|
+
SIGPIPE'd.
|
|
184
|
+
- Configure `maxLogBytes: 0` for the previous uncapped behavior, e.g. when the
|
|
185
|
+
whole log must survive for `ctx_execute_file`.
|
|
109
186
|
|
|
110
187
|
## Configuration
|
|
111
188
|
|
|
112
|
-
The jobs dir
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
189
|
+
The jobs dir defaults to `<project>/.pi-bgrun/jobs` when the session cwd is
|
|
190
|
+
inside a recognizable project root (`.git` or `.pi`, found by walking up from
|
|
191
|
+
the cwd); otherwise it falls back to `~/.pi-bgrun/jobs`. **Warning:** a
|
|
192
|
+
`jobsDir` (or a `PI_BGRUN_GLOBAL_DIR` target) equal to your home directory is
|
|
193
|
+
dangerous — cleanup removes matching `*.log` files directly there. The home
|
|
194
|
+
directory itself is never treated as a project root — pi's global `~/.pi/agent`
|
|
195
|
+
dir would otherwise make every cwd under `$HOME` resolve to `$HOME` (a symlinked
|
|
196
|
+
is still recognized). Override via `jobsDir` / `PI_BGRUN_DIR`. Within a project,
|
|
197
|
+
the dir is shared by every pi session working in that checkout — that sharing
|
|
198
|
+
enables cross-session job lookup, session-restart reconstruction, and
|
|
199
|
+
per-project cleanup. By default each session only *tracks its own jobs*: the
|
|
200
|
+
widget and `bgstatus` listings show this session's running jobs, and finished
|
|
201
|
+
jobs are hidden (ask for them explicitly with `bgstatus includeDone: true`).
|
|
202
|
+
Jobs started by other sessions can still be inspected by id, but they don't
|
|
203
|
+
clutter your widget.
|
|
120
204
|
|
|
121
205
|
Configuration is layered (later wins): **defaults ← user config file ← project
|
|
122
206
|
config file (trusted projects only) ← environment variables**.
|
|
@@ -124,55 +208,309 @@ config file (trusted projects only) ← environment variables**.
|
|
|
124
208
|
- User: `~/.pi/agent/pi-bgrun.json`
|
|
125
209
|
- Project: `<project>/.pi/pi-bgrun.json`
|
|
126
210
|
|
|
211
|
+
The project file is per-contributor state, not shared policy: it is read only for
|
|
212
|
+
a trusted project, it changes what every `bgrun` job in that checkout does, and a
|
|
213
|
+
digest entry runs a shell command at wake time. This repo therefore gitignores
|
|
214
|
+
its own; [`docs/dogfooding.md`](docs/dogfooding.md) has the setup its maintainers
|
|
215
|
+
run locally (completed jobs visible, a scorecard on `bun test` runs).
|
|
216
|
+
|
|
127
217
|
```json
|
|
128
218
|
{
|
|
129
219
|
"adoptForeignJobs": false,
|
|
130
220
|
"showCompletedJobs": false,
|
|
131
221
|
"cleanupDays": 7,
|
|
222
|
+
"maxLogBytes": 67108864,
|
|
132
223
|
"globalAutoClean": true,
|
|
133
224
|
"jobsDir": "/some/other/dir"
|
|
134
225
|
}
|
|
135
226
|
```
|
|
136
227
|
|
|
137
|
-
### Project-local logs
|
|
228
|
+
### Project-local logs (default in repos)
|
|
138
229
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
config
|
|
230
|
+
Inside a recognizable project root, job logs land at `<project>/.pi-bgrun/jobs`
|
|
231
|
+
by default. The root is found by walking up from the session cwd, so a session
|
|
232
|
+
started in a subdirectory still resolves project-locally. An explicit
|
|
233
|
+
**relative** `jobsDir` (from any config layer, or `PI_BGRUN_DIR`) still
|
|
234
|
+
resolves against the project root — e.g. `"jobsDir": "var/bgrun-logs"` writes
|
|
235
|
+
to `<project>/var/bgrun-logs`.
|
|
143
236
|
|
|
144
|
-
|
|
237
|
+
Benefits:
|
|
145
238
|
|
|
146
239
|
- Logs sit inside the project sandbox, so project-confined analysis tools
|
|
147
240
|
(e.g. context-mode's `ctx_execute_file` / `ctx_index`) can process whole logs
|
|
148
241
|
without pulling raw bytes into the context window.
|
|
149
|
-
- Each checkout/worktree gets its own logs — no cross-project clutter in
|
|
150
|
-
|
|
242
|
+
- Each checkout/worktree gets its own logs — no cross-project clutter in a
|
|
243
|
+
machine-global dir.
|
|
151
244
|
- The dir is auto-added to the repo's `.git/info/exclude` (local-only — the
|
|
152
245
|
tracked `.gitignore` is never touched), so logs never pollute `git status`.
|
|
153
|
-
|
|
246
|
+
This happens on the first `bgrun`; at session start it also happens for
|
|
247
|
+
**trusted** projects only, so merely opening pi in an untrusted repo neither
|
|
248
|
+
edits `.git/info/exclude` nor creates the dir. Works in linked worktrees too
|
|
249
|
+
(writes to the common git dir, resolved via the worktree's `commondir` file).
|
|
250
|
+
|
|
251
|
+
**Upgrading from a pre-project-local version:** in a repo the default jobs dir
|
|
252
|
+
is now `<project>/.pi-bgrun/jobs`, not `~/.pi-bgrun/jobs`. Keep the old
|
|
253
|
+
behavior with an absolute `jobsDir`/`PI_BGRUN_DIR`. Old global logs aren't
|
|
254
|
+
moved, but under the project-local default the orphan sweep and `bgclean all`
|
|
255
|
+
still reach them — both cover your project's dir **and** the machine-global
|
|
256
|
+
one. An explicit absolute `jobsDir` is swept alone, exactly as before. Jobs are
|
|
257
|
+
no longer discoverable across projects through a single shared dir — unless you
|
|
258
|
+
opt into a shared absolute `jobsDir`.
|
|
154
259
|
|
|
155
260
|
Rules and migration notes:
|
|
156
261
|
|
|
157
|
-
- Absolute `jobsDir` values behave exactly as in older versions
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
262
|
+
- Absolute `jobsDir` values behave exactly as in older versions: used as-is,
|
|
263
|
+
never treated as project-local, and swept alone (the orphan sweep and
|
|
264
|
+
`bgclean all` do not also touch `~/.pi-bgrun/jobs`). Set
|
|
265
|
+
`"jobsDir": "~/.pi-bgrun/jobs"` (or any absolute path) to keep using the
|
|
266
|
+
machine-global dir inside a repo.
|
|
267
|
+
- If the cwd has no `.git`/`.pi` at or above it, the default falls back to
|
|
268
|
+
`~/.pi-bgrun/jobs`; a relative override also falls back to the global dir
|
|
269
|
+
rather than scattering logs across arbitrary directories.
|
|
162
270
|
- Tools resolve a job's log from the session's job record first, so jobs
|
|
163
271
|
started before a config change stay readable after it.
|
|
164
272
|
- Existing logs in the old global dir are not migrated (they're ephemeral,
|
|
165
|
-
`cleanupDays`-retained)
|
|
273
|
+
`cleanupDays`-retained). They are still reclaimed automatically: the orphan
|
|
274
|
+
sweep and `bgclean all` both cover `~/.pi-bgrun/jobs` in addition to the
|
|
275
|
+
current project's dir.
|
|
276
|
+
|
|
277
|
+
### Deprecated: machine-global jobs dir
|
|
278
|
+
|
|
279
|
+
**Project-scoped logs are the model.** A single shared `~/.pi-bgrun/jobs` was
|
|
280
|
+
the old default; it is now **deprecated** and is no longer what any of the docs
|
|
281
|
+
lead with. Retirement is staged — nothing breaks today:
|
|
282
|
+
|
|
283
|
+
- `PI_BGRUN_GLOBAL_DIR` and the `~/.pi-bgrun/jobs` **default are deprecated**;
|
|
284
|
+
they will be removed in a future major.
|
|
285
|
+
- **Supported for now:** an existing absolute `jobsDir` / `PI_BGRUN_DIR` keeps
|
|
286
|
+
working exactly as before, and a cwd with no project root still falls back to
|
|
287
|
+
`~/.pi-bgrun/jobs` (there is nowhere project-scoped to put it, and the
|
|
288
|
+
alternative — scattering logs into an arbitrary cwd — is worse).
|
|
289
|
+
|
|
290
|
+
Why project-scoped won:
|
|
291
|
+
|
|
292
|
+
- **Each checkout owns its logs** — no cross-project clutter, no ambiguous
|
|
293
|
+
`bgstatus` scope, and `bgclean` can't reach into another project's runs.
|
|
294
|
+
- **Reachable by project-sandboxed analysis** (`ctx_execute_file`,
|
|
295
|
+
`ctx_index`): logs live inside the workspace, so whole-log analysis no longer
|
|
296
|
+
needs a path outside it.
|
|
297
|
+
- **Disposable with the workspace** — delete the checkout, lose its logs.
|
|
298
|
+
|
|
299
|
+
What changes for you, if you set a global dir on purpose:
|
|
300
|
+
|
|
301
|
+
1. Drop the absolute `jobsDir` / `PI_BGRUN_DIR` from your config to get
|
|
302
|
+
`<project>/.pi-bgrun/jobs`.
|
|
303
|
+
2. Planned sharing across projects is what you lose: jobs started in one
|
|
304
|
+
checkout are no longer visible to a session in another, and
|
|
305
|
+
`adoptForeignJobs` only adopts within the same jobs dir. If you need that,
|
|
306
|
+
keep the absolute dir — it is supported, merely no longer the recommended
|
|
307
|
+
default — and say so upstream if it is load-bearing for you.
|
|
308
|
+
3. Old logs in `~/.pi-bgrun/jobs` keep being swept (the orphan sweep and
|
|
309
|
+
`bgclean all` cover both dirs under the project-local default). Delete the
|
|
310
|
+
dir by hand once its logs are past retention.
|
|
166
311
|
|
|
167
312
|
Environment variables (same knobs, handy for one-off overrides):
|
|
168
313
|
|
|
169
314
|
| Variable | Default | Description |
|
|
170
315
|
| --- | --- | --- |
|
|
171
|
-
| `PI_BGRUN_DIR` | `~/.pi-bgrun/jobs` | Override where job logs are stored. An absolute path is used as-is; a **relative** path resolves against the project root (see [project-local logs](#project-local-logs)), falling back to
|
|
316
|
+
| `PI_BGRUN_DIR` | `<project>/.pi-bgrun/jobs` in repos; else `~/.pi-bgrun/jobs` | Override where job logs are stored. An absolute path is used as-is; a **relative** path resolves against the project root (see [project-local logs](#project-local-logs-default-in-repos)), falling back to `~/.pi-bgrun/jobs` when there is no project root. |
|
|
317
|
+
| `PI_BGRUN_GLOBAL_DIR` | `~/.pi-bgrun/jobs` | **Deprecated.** Overrides the machine-global jobs base — the fallback used only when the cwd has no project root (see [deprecation](#deprecated-machine-global-jobs-dir)). A leading `~` or `~/` is expanded to the home dir; `~user` is not. |
|
|
172
318
|
| `PI_BGRUN_FOREIGN_JOBS` | `false` | Adopt other sessions' running jobs into this session's widget and job list. Adopted jobs are polled so they leave the widget when they finish. |
|
|
173
319
|
| `PI_BGRUN_SHOW_COMPLETED` | `false` | Include finished jobs in `bgstatus` listings by default. |
|
|
174
320
|
| `PI_BGRUN_CLEANUP_DAYS` | `7` | Log retention for cleanup sweeps and the `bgclean` default. |
|
|
175
|
-
| `
|
|
321
|
+
| `PI_BGRUN_MAX_LOG_BYTES` | `67108864` (64 MiB) | Byte ceiling for a job's log (stdout+stderr). `0` disables it (unlimited). See [Log size ceiling](#log-size-ceiling). |
|
|
322
|
+
| `PI_BGRUN_GLOBAL_AUTO_CLEAN` | `true` | Set `0`/`false` to disable the automatic orphan sweep (see below). |
|
|
323
|
+
| `PI_BGRUN_GREP_TIMEOUT_MS` | `2000` | Wall-clock budget for a `bggrep` match. A caller-supplied regex that exceeds it is aborted (its worker terminated) and reported as an error instead of hanging — see [Bounded matching](#bounded-matching). |
|
|
324
|
+
| `PI_BGRUN_USER_CONFIG` | `~/.pi/agent/pi-bgrun.json` | Override the user-level config file path (see [Configuration](#configuration)). |
|
|
325
|
+
|
|
326
|
+
### Digest scorecard (opt-in)
|
|
327
|
+
|
|
328
|
+
Wake messages always lead with universal facts — exit code, duration, and the
|
|
329
|
+
command's own log line count (the internal exit marker is excluded). A project
|
|
330
|
+
can additionally opt into a **digest scorecard**: a one-line pass/fail summary
|
|
331
|
+
extracted from the log and appended to the wake.
|
|
332
|
+
|
|
333
|
+
#### Job identity: name, type, command
|
|
334
|
+
|
|
335
|
+
Every `bgrun` job carries three identifiers, and the digest selector reads all
|
|
336
|
+
three:
|
|
337
|
+
|
|
338
|
+
| Field | Required | Normalized | Drives |
|
|
339
|
+
| --- | --- | --- | --- |
|
|
340
|
+
| `command` | yes | used as-is (`sh -c`) | what runs; the `match.command` target |
|
|
341
|
+
| `name` | no | trimmed, blank → none, ≤80 chars | display label + job-id/log slug; the `match.name` target |
|
|
342
|
+
| `type` | no | trimmed, lowercased, blank → none, ≤40 chars | digest routing only; the first-class selector |
|
|
343
|
+
|
|
344
|
+
`name` names the job (and its log file); `type` never affects the id or the
|
|
345
|
+
widget, but is echoed in `bgstatus <id>` and `bgrun`'s `started:` line — its
|
|
346
|
+
main job is selecting the scorecard. Selection tries `type`
|
|
347
|
+
entries first (exact, case-insensitive), then falls back to `match.name` /
|
|
348
|
+
`match.command` globs. The config `type` is capped to the same 40 characters
|
|
349
|
+
as the job `type`, so an over-long type still matches.
|
|
350
|
+
|
|
351
|
+
#### Setting it up
|
|
352
|
+
|
|
353
|
+
Three ways, easiest first — pick the first one you're comfortable with:
|
|
354
|
+
|
|
355
|
+
1. **Ask your agent (recommended).** Say: *"Set up the pi-bgrun digest for
|
|
356
|
+
this project."* The `digest-config` skill ships with this package and does
|
|
357
|
+
the whole job: it samples your project's real job logs, tries the shipped
|
|
358
|
+
presets against them, drafts a custom command if none fits, validates the
|
|
359
|
+
result on both a green and a red log, and writes the config. It sees your
|
|
360
|
+
actual output format, which is exactly what a good digest depends on —
|
|
361
|
+
and you never have to read a log yourself. The one-shot toast some
|
|
362
|
+
projects see on session start ("no digest configured") — once per project
|
|
363
|
+
that has run a bgrun job — is pointing at this same skill.
|
|
364
|
+
2. **One-line preset if you know your stack.** Create
|
|
365
|
+
`<project>/.pi/pi-bgrun.json` (or merge into an existing one):
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{ "digest": { "preset": "go-test" } }
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
| Preset | What it summarizes | Suggested `type` |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| `go-test` | Go test output: package ok/FAIL counts + failing test names | `test` |
|
|
374
|
+
| `jest` | Jest output: Tests/Test Suites summary + failed test names | `test` |
|
|
375
|
+
| `pytest` | pytest output: final passed/failed/error summary line + FAILED test ids | `test` |
|
|
376
|
+
| `junit-xml` | JUnit XML: `<failure>`/`<error>` counts + failing testcase names | `test` |
|
|
377
|
+
|
|
378
|
+
All shipped presets are test runners, so they all suggest the conventional
|
|
379
|
+
type `test`. The suggestion is documentation, not behavior: you still write
|
|
380
|
+
the `type` on the entry yourself, and a preset entry with no `type` applies
|
|
381
|
+
to every job as before.
|
|
382
|
+
|
|
383
|
+
3. **Custom command.** For formats the presets don't cover:
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
{ "digest": { "command": "grep -E 'FAIL|ok ' \"$1\" | head -5" } }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The command receives the job's log path as `$1` and its stdout is appended
|
|
390
|
+
to the wake. Worked example — a log containing:
|
|
391
|
+
|
|
392
|
+
```text
|
|
393
|
+
PASS src/auth.test.ts (2.1s)
|
|
394
|
+
FAIL src/api.test.ts
|
|
395
|
+
Tests: 12 passed, 1 failed, 13 total
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
plus the command `grep -E '^(PASS|FAIL|Tests:)' "$1" | head -5`, wakes with:
|
|
399
|
+
|
|
400
|
+
```text
|
|
401
|
+
digest (command): FAIL src/api.test.ts
|
|
402
|
+
Tests: 12 passed, 1 failed, 13 total
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Rules of thumb: quote `"$1"`, end the pipeline in `head -N` so output is
|
|
406
|
+
bounded, and — this is the important one — **check the command against a
|
|
407
|
+
green and a red log before committing to it**. A scorecard that says "all
|
|
408
|
+
passing" on a failing log is worse than no scorecard. The `digest-config`
|
|
409
|
+
skill does this validation for you; if you'd rather hand-tune a command
|
|
410
|
+
yourself, you can also ask your agent to validate a specific command
|
|
411
|
+
against specific job logs.
|
|
412
|
+
|
|
413
|
+
#### Multiple scorecards (one per job type)
|
|
414
|
+
|
|
415
|
+
`digest` can also be an ordered **list** of scorecards. Give each a `type` and
|
|
416
|
+
pass the matching `type:` when you start the job — the most reliable selector,
|
|
417
|
+
because it does not depend on the agent naming every job consistently:
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{
|
|
421
|
+
"digest": [
|
|
422
|
+
{ "type": "test", "preset": "go-test" },
|
|
423
|
+
{ "type": "build", "label": "build",
|
|
424
|
+
"command": "grep -E '^error' \"$1\" | head -5" },
|
|
425
|
+
{ "match": { "command": "*cargo*" }, "label": "cargo", "preset": "go-test" },
|
|
426
|
+
{ "preset": "go-test" }
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Start jobs with the matching type:
|
|
432
|
+
|
|
433
|
+
```text
|
|
434
|
+
bgrun(command: "go test ./...", name: "unit-tests", type: "test")
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`type` is an optional `bgrun` parameter. The vocabulary is defined by the
|
|
438
|
+
`type` fields of the project's digest config in `.pi/pi-bgrun.json`; when the
|
|
439
|
+
project's digest config defines types, prefer passing the matching one. If a
|
|
440
|
+
job's `type` (or name/command) selects no entry, pi-bgrun logs a one-line
|
|
441
|
+
diagnostic naming the job and the configured types — so a mismatched type is
|
|
442
|
+
visible instead of silently scorecard-less.
|
|
443
|
+
|
|
444
|
+
Selection order (exactly one entry, or none):
|
|
445
|
+
|
|
446
|
+
1. **Type entries first.** An entry declaring a `type` matches ONLY a job that
|
|
447
|
+
declared that same type — exact and case-insensitive (`"test"` matches
|
|
448
|
+
`"Test"`) — and must ALSO satisfy the entry's `match` if it has one. All type
|
|
449
|
+
entries are checked first, in config order, regardless of where they sit
|
|
450
|
+
relative to match entries. First type match wins.
|
|
451
|
+
2. **Match/default fallback.** If no type entry matched — including when the
|
|
452
|
+
job has no type — the entries *without* a `type` are scanned in config order:
|
|
453
|
+
`match.name` / `match.command` globs and no-`match` defaults, first match
|
|
454
|
+
wins. Put a no-`match` default **last** so jobs you didn't anticipate still get
|
|
455
|
+
a scorecard.
|
|
456
|
+
3. No match → no digest.
|
|
457
|
+
|
|
458
|
+
- `type` and `match` compose (AND): with both present the entry matches only a
|
|
459
|
+
job of that type that also satisfies the glob. Use `match` alone for jobs
|
|
460
|
+
that won't pass a `type`.
|
|
461
|
+
- `match.name` and `match.command` are **globs** tested against the job's
|
|
462
|
+
`name` and command line. Both present → both must match. Matching is
|
|
463
|
+
**case-insensitive and whole-string** — `*` matches any run, `?` exactly one
|
|
464
|
+
character, everything else is literal, and `\` escapes the next character
|
|
465
|
+
(`\*` is a literal star) — so `"*unit*"` matches `"unit-tests-run3"` while a
|
|
466
|
+
bare `"unit-tests"` matches only exactly that.
|
|
467
|
+
- `label` sets the wake tag: `digest (<label>): …`. Precedence: `label` →
|
|
468
|
+
(type entry) the type string → (matched glob entry) `match.name` → the
|
|
469
|
+
entry's preset id (else `command`). So a bare `{ "preset": "go-test" }`
|
|
470
|
+
wakes as `digest (go-test):`.
|
|
471
|
+
- First match wins; exactly one digest block is appended per wake.
|
|
472
|
+
|
|
473
|
+
The legacy single-object form still works unchanged — `{ "digest": { "preset":
|
|
474
|
+
"go-test" } }` is a one-entry list with no matchers.
|
|
475
|
+
|
|
476
|
+
Opt in per project via `<project>/.pi/pi-bgrun.json` (read only for trusted
|
|
477
|
+
projects). If both `preset` and `command` are set within one entry, the preset
|
|
478
|
+
wins. An empty list (or one where every entry is invalid) counts as *not
|
|
479
|
+
configured*.
|
|
480
|
+
|
|
481
|
+
#### Guarantees
|
|
482
|
+
|
|
483
|
+
- **Exit code always leads.** The digest is appended after the universal
|
|
484
|
+
stats, labeled `digest (<label>):` — `label` follows the precedence above
|
|
485
|
+
(entry `label` → type string → `match.name` → preset id / `command`). It
|
|
486
|
+
never overrides or reorders the exit code, duration, or line count.
|
|
487
|
+
- **Capped and timed.** Digest output is capped at ~500 chars, and buffering
|
|
488
|
+
stops once that cap is reached — a command that prints unbounded output
|
|
489
|
+
cannot balloon the wake. The digest command gets a 5s timeout plus a 250ms
|
|
490
|
+
SIGTERM→SIGKILL grace (≈5.25s worst case), during which the wake waits.
|
|
491
|
+
- **Silent-fail.** A digest command that errors, times out, or prints nothing
|
|
492
|
+
simply contributes nothing — it never breaks a wake.
|
|
493
|
+
- **No config, no behavior.** Absent or invalid config contributes nothing;
|
|
494
|
+
without a `digest` section the wake is unchanged.
|
|
495
|
+
|
|
496
|
+
A configured wake reads like this:
|
|
497
|
+
|
|
498
|
+
```text
|
|
499
|
+
✅ Background job "tests" `abc123` finished (exit 1).
|
|
500
|
+
Command: go test ./...
|
|
501
|
+
Stats: 42.3s, 1204 lines
|
|
502
|
+
Last output: FAIL example.com/api/handlers
|
|
503
|
+
digest (go-test): 7 ok / 1 FAIL: TestResolveNotFound
|
|
504
|
+
Review the result now: call `bgtail` ...
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Shell safety: the command comes from trust-gated config and runs with your
|
|
508
|
+
own privileges — the same trust boundary as the `jobsDir` setting.
|
|
509
|
+
|
|
510
|
+
A user-level default digest works too: set `digest` in
|
|
511
|
+
`~/.pi/agent/pi-bgrun.json` (path overridable via `PI_BGRUN_USER_CONFIG`), and
|
|
512
|
+
any project without its own digest inherits it. The project `digest` section
|
|
513
|
+
overrides the user-level one **wholesale** (no per-key merge).
|
|
176
514
|
|
|
177
515
|
### Log cleanup
|
|
178
516
|
|
|
@@ -182,20 +520,75 @@ not delete another session's artifacts.**
|
|
|
182
520
|
- **Session-scoped auto-sweep (default)** runs at `session_start` and
|
|
183
521
|
`session_shutdown` and removes only *this session's* finished logs older
|
|
184
522
|
than `cleanupDays`. Cheap and unthrottled.
|
|
185
|
-
- **
|
|
186
|
-
`PI_BGRUN_GLOBAL_AUTO_CLEAN=0`)** — also sweeps
|
|
187
|
-
session boundaries
|
|
188
|
-
|
|
523
|
+
- **Orphan sweep (default on; opt out with `globalAutoClean: false` /
|
|
524
|
+
`PI_BGRUN_GLOBAL_AUTO_CLEAN=0`)** — also sweeps every shared jobs dir at
|
|
525
|
+
session boundaries — the machine-global `~/.pi-bgrun/jobs` plus the current
|
|
526
|
+
project's dir — removing *finished* logs (exit marker, or dead pid) older
|
|
527
|
+
than `cleanupDays`. This is what keeps orphans from sessions that
|
|
189
528
|
crashed or will never be resumed from accumulating: a week-old finished log
|
|
190
529
|
is garbage under the same retention its owning session would apply itself.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
530
|
+
Under the project-local default it covers the current project's dir plus the
|
|
531
|
+
machine-global one; an explicit absolute `jobsDir` is swept alone. Throttled
|
|
532
|
+
to once per `cleanupDays` via a `.last-clean` marker so restart-heavy
|
|
533
|
+
workflows don't re-sweep on every launch. Running jobs are pid-protected, so
|
|
534
|
+
live sessions are never affected.
|
|
194
535
|
- **Manual**: `bgclean` cleans this session's old logs; `bgclean` with
|
|
195
|
-
`all: true` sweeps every session's logs
|
|
196
|
-
|
|
536
|
+
`all: true` sweeps every session's logs across the shared dirs immediately
|
|
537
|
+
(and refreshes the markers).
|
|
538
|
+
|
|
197
539
|
- Running jobs are never swept while their pid is alive.
|
|
198
540
|
|
|
541
|
+
**The jobs dir is only *auto*-swept when it is recognizably ours.** `bgrun`
|
|
542
|
+
writes a `.bgrun-jobs` ownership marker into the dir on first use; the
|
|
543
|
+
automatic global sweep refuses to delete `*.log` files in a dir without it, so
|
|
544
|
+
a stray `PI_BGRUN_DIR` (or a config pointing at an unrelated directory) can't
|
|
545
|
+
be quietly emptied a week later. Manual `bgclean all` is an explicit
|
|
546
|
+
instruction, so it bypasses the gate and always works.
|
|
547
|
+
|
|
548
|
+
The dir also carries small bookkeeping files. The digest markers
|
|
549
|
+
(`.bgrun-used-*`, `.digest-nudge-*`) and stale `.tmp-*.log` staging files are
|
|
550
|
+
swept at `cleanupDays`; `.bgrun-jobs` and `.last-clean` persist until removed
|
|
551
|
+
by hand:
|
|
552
|
+
|
|
553
|
+
| File | Purpose |
|
|
554
|
+
| --- | --- |
|
|
555
|
+
| `.bgrun-jobs` | Ownership marker — gates the *automatic* global sweep. |
|
|
556
|
+
| `.last-clean` | Throttles the global sweep to once per `cleanupDays`. |
|
|
557
|
+
| `.bgrun-used-<hash>` | Per-project evidence that bgrun has run here (digest nudge). |
|
|
558
|
+
| `.digest-nudge-<hash>` | Per-project: the one-shot digest nudge was already shown. |
|
|
559
|
+
|
|
560
|
+
## Releasing
|
|
561
|
+
|
|
562
|
+
Version numbers and the changelog are derived from commit messages via
|
|
563
|
+
[release-please](https://github.com/googleapis/release-please), so the prefix on a
|
|
564
|
+
squash-merged PR title is load-bearing:
|
|
565
|
+
|
|
566
|
+
| Prefix | Release |
|
|
567
|
+
| --- | --- |
|
|
568
|
+
| `fix:` / `feat:` / `deps:` | yes — patch / minor / patch |
|
|
569
|
+
| `feat!:` / `fix!:` / `BREAKING CHANGE:` | yes — minor (pre-1.0) |
|
|
570
|
+
| `refactor:` `docs:` `test:` `ci:` `build:` `chore:` `style:` | no |
|
|
571
|
+
| no prefix, e.g. `Address review findings (#11)` | no |
|
|
572
|
+
|
|
573
|
+
An unprefixed commit is ignored outright: no changelog entry, and it cannot trigger
|
|
574
|
+
a release on its own. `pr-title.yml` enforces the format on every PR
|
|
575
|
+
(`bun run lint:pr-title` locally).
|
|
576
|
+
|
|
577
|
+
**PRs are squash-merged, and that is structural rather than stylistic:** the squash
|
|
578
|
+
collapses the PR to a single commit whose subject is the *title*, which is the
|
|
579
|
+
message release-please parses. That is why the title — not the branch commits — is
|
|
580
|
+
what CI validates, and why work-in-progress commit messages never surface. Rebase
|
|
581
|
+
and merge-commit methods would put branch subjects on `main` and break that mapping,
|
|
582
|
+
so repository settings must disable both ([`docs/releasing.md`](docs/releasing.md)
|
|
583
|
+
lists the exact toggles).
|
|
584
|
+
|
|
585
|
+
Every merge to `main` updates a single open **Release PR** holding the `package.json`
|
|
586
|
+
bump and `CHANGELOG.md` entry. Nothing is published until that PR is merged —
|
|
587
|
+
ordinary merges only update it.
|
|
588
|
+
|
|
589
|
+
Process, the required repository settings, and the one secret:
|
|
590
|
+
[`docs/releasing.md`](docs/releasing.md).
|
|
591
|
+
|
|
199
592
|
## Status
|
|
200
593
|
|
|
201
|
-
Early / pre-release.
|
|
594
|
+
Early / pre-release.
|