@stablekernel/pi-background-run 0.4.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/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@stablekernel/pi-background-run",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Run long shell commands detached in the background for pi; get woken on completion. Output lands in a file; context stays clean.",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
6
9
  "license": "MIT",
7
10
  "publishConfig": {
8
11
  "access": "public"
@@ -15,15 +18,18 @@
15
18
  "./extension/index.ts"
16
19
  ],
17
20
  "skills": [
18
- "./skill/run-bg"
21
+ "./skill/run-bg",
22
+ "./skill/digest-config"
19
23
  ]
20
24
  },
21
25
  "scripts": {
22
26
  "test": "bun test extension/index.test.ts",
23
- "lint": "tsc --noEmit"
27
+ "lint": "tsc --noEmit",
28
+ "lint:pr-title": "bun scripts/check-conventional-commit.ts"
24
29
  },
25
30
  "files": [
26
31
  "extension/",
32
+ "!extension/index.test.ts",
27
33
  "skill/",
28
34
  "README.md",
29
35
  "LICENSE"
@@ -56,6 +62,7 @@
56
62
  "devDependencies": {
57
63
  "@earendil-works/pi-coding-agent": "*",
58
64
  "@earendil-works/pi-tui": "*",
65
+ "@types/node": "^24.0.0",
59
66
  "typebox": "^1.3.0",
60
67
  "typescript": "^5.7.0"
61
68
  }
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: digest-config
3
+ description: Set up the pi-bgrun digest scorecard for this project. Use when the user asks to configure a digest, enable digest heuristics, or when a pi-bgrun nudge points at this skill. Samples the project's real job logs, picks a shipped preset (go-test, jest, pytest, junit-xml) or drafts a custom digest command, validates it against green AND red logs, then writes the digest section into .pi/pi-bgrun.json.
4
+ ---
5
+
6
+ # Configure a project digest scorecard
7
+
8
+ Goal: a `digest` section in `<project>/.pi/pi-bgrun.json` whose command turns a
9
+ job log into a short pass/fail scorecard, appended to every `bgrun` wake as
10
+ `digest (<label>): ...`. The scorecard must be reliable on both green and red
11
+ logs — a wrong scorecard is worse than none. Most projects run more than one
12
+ kind of job (unit tests, a build, e2e); configure one entry per job type rather
13
+ than one command that guesses.
14
+
15
+ Config shapes — a single object (legacy) or an ordered **list** of entries; if
16
+ both `preset` and `command` are set within one entry, the preset wins:
17
+
18
+ ```json
19
+ { "digest": { "preset": "go-test" } }
20
+ ```
21
+
22
+ ```json
23
+ { "digest": { "command": "grep -E 'FAIL|ok ' \"$1\" | head -5" } }
24
+ ```
25
+
26
+ ```json
27
+ {
28
+ "digest": [
29
+ { "type": "test", "preset": "go-test" },
30
+ { "type": "build", "label": "build",
31
+ "command": "grep -E '^error' \"$1\" | head -5" },
32
+ { "match": { "command": "*cargo*" }, "label": "cargo", "preset": "go-test" },
33
+ { "preset": "go-test" }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ Prefer a `type` on each entry: it is matched exactly (case-insensitive) against
39
+ the `type:` the agent passes to `bgrun`, so it does not depend on job names or
40
+ command lines staying stable. When you configure a `type`, tell the agent to
41
+ pass it: `bgrun(command: …, name: …, type: "test")`. If a job's `type`/name
42
+ selects no entry, pi-bgrun logs a one-line diagnostic naming the job and the
43
+ configured types — check it when a scorecard is expected but absent.
44
+
45
+ Selection (exactly one entry, or none):
46
+
47
+ 1. Entries with a `type` are checked **first**, in config order, and match only
48
+ a job declaring that exact type that also satisfies the entry's `match` if
49
+ it has one. First type match wins.
50
+ 2. Otherwise the entries **without** a `type` are scanned in config order:
51
+ `match.name` / `match.command` globs and no-`match` defaults.
52
+ 3. No match → no digest.
53
+
54
+ - `type` and `match` compose (AND): with both present, only a job of that type
55
+ that also satisfies the glob matches.
56
+ - `match.name` / `match.command` are **globs** tested against the job's
57
+ `name` and command line; both present → both must match. They are
58
+ **case-insensitive and whole-string** (`*` any run, `?` one character;
59
+ write `*text*` for a substring; `\` escapes the next character).
60
+ - An entry with no `match` (or an empty `match`) matches every job — put it
61
+ **last** as the default. Include one so jobs you did not anticipate still
62
+ get a scorecard.
63
+ - `label` sets the wake tag; without it a type entry uses its `type` string, a
64
+ glob entry uses the matched `match.name`, else the preset id (or `command`).
65
+ - An invalid `match` (non-string), an invalid `type`, or an entry with no valid
66
+ `preset`/`command` is dropped silently; an empty/all-invalid list counts as
67
+ unconfigured.
68
+
69
+ Shipped presets: `go-test` (package ok/FAIL counts + failing test names),
70
+ `jest` (Tests/Test Suites summary + failed test names), `pytest` (final
71
+ passed/failed/error summary line + FAILED test ids), `junit-xml`
72
+ (`<failure>`/`<error>` counts + failing testcase names). Each preset also
73
+ carries a `suggestedType` (all `test`) — use it as the `type` when scaffolding
74
+ an entry, e.g. `{ "type": "test", "preset": "go-test" }`. The suggestion is
75
+ advisory; a preset entry with no `type` still applies to every job.
76
+
77
+ ## Procedure
78
+
79
+ 1. **Find done-job logs.** Locate the project's jobsDir from the config
80
+ layering: in a project root it defaults to `<project>/.pi-bgrun/jobs`;
81
+ otherwise to the machine-global `~/.pi-bgrun/jobs`. An explicit `jobsDir`
82
+ (any config layer) or `PI_BGRUN_DIR` overrides it; `PI_BGRUN_GLOBAL_DIR`
83
+ retargets the machine-global base. The resolved path is printed as
84
+ `log: <path>` by `bgrun` and by `bgstatus <id>` — read it back there if
85
+ unsure. List the `*.log` files of finished jobs.
86
+ 2. **Sample the formats across job types.** Group the logs by job type using
87
+ each job's `name` and command line (from `bgstatus`); most projects have at
88
+ least a test job and a build job. Pick 2-3 logs per type — at least one
89
+ green and one red run each — and inspect them with `ctx_execute_file`
90
+ (context-mode sandbox, so only your printed summary enters context).
91
+ Identify the runner / output format for each type, and name the type with a
92
+ short token (`test`, `build`, `lint`, `e2e`).
93
+ 3. **Try a preset first, per type.** Run each shipped preset's command against
94
+ a sample log (`sh -c '<preset command>' sh <logpath>`). Preset commands are
95
+ data in the package's `extension/digestPresets.ts`. Clean scorecard on green
96
+ AND red samples → use that preset for that type, seeding the entry's
97
+ `type` from the preset's `suggestedType` (all shipped presets suggest
98
+ `test`). Repeat for each type.
99
+ 4. **Draft custom commands** for types no preset fits. Use awk/sed/grep/jq; the
100
+ command receives the log path as `$1` and MUST end in `head -N` so output is
101
+ bounded. Keep it to a count line plus failed-item names.
102
+ 5. **Validate each entry on green AND red.** Run every drafted command against
103
+ every sample for its type. Each must produce a correct scorecard on both: no
104
+ phantom failures on green logs, no missing failures on red ones. If a type
105
+ has no reliable command, omit that entry (or leave the digest unconfigured)
106
+ rather than shipping a wrong scorecard — say why.
107
+ 6. **Write the config, one `type` entry per job type.** Merge the entries into
108
+ `<project>/.pi/pi-bgrun.json`, preserving any existing keys, giving each
109
+ entry the `type` you identified in step 2, and putting the no-`match`
110
+ default entry **last**. Use `match.name` / `match.command` globs only for
111
+ jobs that will not pass a `type`. Create the file if absent. Tell the user
112
+ (or the agent driving `bgrun`) which `type:` value to pass for each job.
113
+ 7. **Smoke-test each entry.** Start a real `bgrun` job of each configured type
114
+ (e.g. the test command AND the build command), passing the matching
115
+ `type:`, and check that its wake carries a correct `digest (<label>):` block
116
+ for the right entry. If a block is empty, wrong, or comes from the wrong
117
+ entry, fix the command / type / ordering and repeat step 7.
@@ -28,15 +28,18 @@ no polling.
28
28
 
29
29
  | Action | Tool |
30
30
  |---|---|
31
- | Start | `bgrun(command: "make test-short", name: "unit-tests")` → `started: <job-id>` (name is an optional short label; use it so jobs are recognizable in `bgstatus`, the status widget, and wake messages) |
31
+ | Start | `bgrun(command: "make test-short", name: "unit-tests", type: "test")` → `started: <job-id>` (name is an optional short label; use it so jobs are recognizable in `bgstatus`, the status widget, and wake messages) |
32
32
  | Status | `bgstatus(<job-id>)` for one job, or `bgstatus()` for this session's running jobs — finished jobs are hidden by default; pass `includeDone: true` to list them |
33
- | Tail | `bgtail(<job-id>, 40)` |
33
+ | Tail | `bgtail(<job-id>, 40)` — first read: last-40 tail; later reads: only lines appended since (delta tailing) |
34
+ | Grep | `bggrep(<job-id>, "pattern", context?)` — line-numbered matches, capped and condensed; default pattern = generic failure signatures (override when you know the format) |
34
35
  | Clean | `bgclean()` for this session's old logs; `bgclean all` to sweep every session's (default 7-day retention) |
35
36
 
36
37
  ## Workflow
37
38
 
38
39
  1. **Start:** call `bgrun` with the command (and a short `name`, e.g. `name: "unit-tests"`).
39
- Note the returned job-id. Continue other
40
+ When the project's digest config defines `type` entries, also pass the
41
+ matching `type` (e.g. `type: "test"`) — like `name`, it helps the wake
42
+ select the right digest scorecard. Note the returned job-id. Continue other
40
43
  work; you will be woken automatically when the job finishes.
41
44
  2. **On wake:** check the exit status in the wake message first.
42
45
  - `exit: 0` → success. `bgtail` to confirm.
@@ -47,28 +50,76 @@ no polling.
47
50
  - `done exit=<non-zero>` → failure; analyze the log.
48
51
  - `running` but the job should have finished long ago → likely crashed (the
49
52
  process died without writing the exit marker). Analyze the log with
50
- `ctx_execute_file`.
53
+ `bggrep` (any jobs dir, last 2 MB) or `ctx_execute_file` on the absolute path
54
+ (whole file — needed for logs bigger than 2 MB).
51
55
 
52
56
  ### Reading results without flooding context
53
57
 
54
- - **Quick peek (≤40 lines):** call `bgtail` with the job id and `lines: 40` — strips the `__BGRUN_EXIT__` marker.
55
- - **Whole-log failure analysis:** `ctx_execute_file` on the log path:
56
-
57
- ```
58
+ If the wake message carries a `digest (<label>):` block (the label is the
59
+ entry's `label`, its type, a matched `match.name`, or the preset id /
60
+ `command`), read that
61
+ first — it is a short pass/fail scorecard configured for this project and
62
+ usually answers "what failed" without any follow-up read. `bgtail` stays the
63
+ positional-peek tool for everything else.
64
+
65
+ - **Quick peek (≤40 lines):** call `bgtail` with the job id and `lines: 40` — strips the `__BGRUN_EXIT__` marker. The first read returns the last-40 tail; repeat reads return only lines appended since your last read (delta tailing) — polling a running job is nearly free.
66
+ - **Failure extraction:** `bggrep(<job-id>, "pattern")` — line-numbered matches with optional context lines, capped and condensed. Resolves the job id to the configured jobs dir itself — no path to reconstruct. (`ctx_execute_file` can read the same file given its absolute path.) Searches the last 2 MiB by default; `bytes: 67108864` widens it to the whole capped log — more scanning costs latency and memory, **not context**, since the returned matches stay capped. Pass your own pattern whenever you know the tool's output format; the default only catches common failure signatures.
67
+ - **Whole-log failure analysis:** `ctx_execute_file` on the log's **absolute
68
+ path**. Unlike `bgtail`/`bggrep` (bounded to the last 2 MB), this reads the
69
+ whole file — the only way to cover a log bigger than 2 MB, e.g. one that hit
70
+ the size ceiling. Copy the `log:` path from `bgrun`'s `started:` line and
71
+ expand `~` yourself (it is not expanded for you; the tool takes an absolute
72
+ path or one relative to the project root). Otherwise it is an ordinary tool
73
+ call: your normal Read-deny rules still apply.
74
+
75
+ ```javascript
58
76
  ctx_execute_file(
59
- path: "~/.pi-bgrun/jobs/<JOB>.log",
77
+ path: "/Users/me/project/.pi-bgrun/jobs/<JOB>.log",
60
78
  language: "javascript",
61
79
  code: "const L=FILE_CONTENT.split('\\n'); \
62
80
  const fails=L.filter(l=>/(--- FAIL|FAIL|panic:|Error:)/.test(l)); \
63
81
  console.log(`lines: ${L.length}, failures: ${fails.length}`); \
64
82
  console.log(fails.slice(0,40).join('\\n'));"
65
- )
83
+ })
66
84
  ```
67
85
 
68
86
  A 10 000-line `make test` log collapses to a ~30-line summary in context.
69
87
 
70
- **Never `cat`, `Read`, or `bash cat` a full bgrun log.** Always `bgtail` or
71
- `ctx_execute_file`.
88
+ **Why `bggrep` instead of `bash grep` on the log?**
89
+
90
+ - `bash grep` output is uncapped — a retry-storm log can dump thousands of
91
+ matching lines (megabytes) straight into context, and staying safe depends
92
+ on remembering `| head` on every single call. `bggrep` is bounded by design
93
+ (last 2 MB of the log, per-line 10 000-char pre-truncation, ~50 matches,
94
+ ~8KB, plus a wall-clock match budget so a runaway regex errors instead of
95
+ hanging).
96
+ - It takes the job id — no log-path reconstruction, no shell-quoting of the
97
+ regex, and no reliance on the agent getting `~` expansion right.
98
+ - Output is self-describing: match count, line numbers, `…[N skipped]…` gap
99
+ markers, `— none` for no-match.
100
+
101
+ Plain `grep` via bash is fine only for a one-off search you know is tiny.
102
+
103
+ **Never `cat`, `Read`, `bash cat`, or `bash grep` a full bgrun log.** Always
104
+ `bgtail`, `bggrep`, or (for project-local logs) `ctx_execute_file`.
105
+
106
+ **Order of preference, cheapest first: `bgtail` → `bggrep` → `ctx_execute_file`.**
107
+ Reach for the sandbox only when you need something a regex over lines cannot
108
+ express — totals, dedup, grouping, joining the log against another file.
109
+
110
+ `ctx_execute_file` is not itself a context dump: the file's bytes never enter
111
+ context, only your script's **stdout** does ("raw content never leaves"). So the
112
+ cost is exactly what you print — which makes `console.log(FILE_CONTENT)` (or
113
+ `print(open(path).read())`, or a big unbounded slice) the one way a whole-log
114
+ analysis turns into a context dump, and a capped-by-default 64 MiB log makes
115
+ that expensive rather than merely rude. Aggregate, then cap what you print:
116
+
117
+ - print counts / grouped summaries / the first N matches — not the content;
118
+ - keep a `.slice(0, 40)` / `[:40]` on anything you echo;
119
+ - for many different questions about one big log, index it once (`ctx_index`)
120
+ and `ctx_search` it, instead of re-scanning the file per call;
121
+ - `bgtail` with a larger `lines`, or a tighter `bggrep` pattern, is usually the
122
+ cheaper answer to "I need to see more".
72
123
 
73
124
  ## After a pi restart or session switch
74
125
 
@@ -77,20 +128,45 @@ no polling.
77
128
  - After a restart/switch, run `bgstatus(<job-id>)` — the id still resolves via the
78
129
  log's `__BGRUN_EXIT__=N` marker. To browse everything on disk, use
79
130
  `bgstatus(includeDone: true)`.
80
- - Each session only tracks its own jobs by default. Jobs from other sessions
81
- appear only when `adoptForeignJobs` is enabled in `~/.pi/agent/pi-bgrun.json`
82
- (or `PI_BGRUN_FOREIGN_JOBS=1`).
131
+ - Each session only tracks its own jobs by default. Running jobs from other
132
+ sessions appear only when `adoptForeignJobs` is enabled in
133
+ `~/.pi/agent/pi-bgrun.json` (or `PI_BGRUN_FOREIGN_JOBS=1`); finished foreign
134
+ logs appear with `bgstatus(includeDone: true)` regardless.
83
135
 
84
136
  ## Rules
85
137
 
86
138
  - Call the tools; never hand-roll `nohup … &` inline.
87
139
  - One job = one id. Multiple concurrent jobs are fine — each has its own log.
88
- - Logs live in `~/.pi-bgrun/jobs` (override with `PI_BGRUN_DIR`).
140
+ - Job logs are capped by default (`maxLogBytes` / `PI_BGRUN_MAX_LOG_BYTES`,
141
+ 64 MiB; `0` = unlimited) and the cap keeps the **first** bytes. A log that
142
+ ends with `__BGRUN_TRUNC__ output truncated: kept the first <N> bytes` (or
143
+ `__BGRUN_NOCAP__ log ceiling unavailable`, when the ceiling could not be
144
+ installed and the job ran uncapped) hit that ceiling: output past it was dropped, not lost to a failure — the job
145
+ still ran to completion with its real exit code, and readers (`bgtail`,
146
+ `bggrep`, the wake's line count/last line) filter the notice out. The wake's
147
+ Stats line, `bgtail` and `bggrep` all say when a log was capped (and report
148
+ `truncatedAtBytes` in their details), and a configured digest scorecard is
149
+ skipped rather than scored against an incomplete log — so on a capped job,
150
+ read a missing digest as "unknown", **not** as "no failures", and do not
151
+ re-run the command to see the missing tail; raise the ceiling if you need the
152
+ whole log. The flag lives in the exit marker (`__BGRUN_EXIT__=0
153
+ truncated=<N>`, or `nocap=1`), never in printable text — the `__BGRUN_*__`
154
+ lines are reserved, so a notice-looking line printed by the command itself is
155
+ content, not a signal. A search window is also limited to its last
156
+ 500 000 lines: when that bites, `bgtail`/`bggrep` say so — a "none" from a
157
+ trimmed window means the head was not searched.
158
+ - Logs default to `<project>/.pi-bgrun/jobs` in a repo — project-scoped is the
159
+ model (`~/.pi-bgrun/jobs` is a deprecated fallback for a cwd with no project
160
+ root; an absolute `PI_BGRUN_DIR`/`jobsDir` still works but is legacy). Project-local dirs are
161
+ auto-ignored via `.git/info/exclude`, which keeps `git status` clean; the
162
+ logs stay reachable for project-sandboxed analysis tools like
163
+ `ctx_execute_file` because they live inside the project.
89
164
  - Cleanup: `bgclean` removes only THIS session's old logs; `bgclean all`
90
165
  sweeps every session's. Auto-sweeps at session start/shutdown are
91
- session-scoped plus a global orphan pass (default on — removes finished
92
- week-old logs from crashed/abandoned sessions; disable with
93
- `globalAutoClean: false` / `PI_BGRUN_GLOBAL_AUTO_CLEAN=0`). Retention is
94
- `cleanupDays` (default 7, configurable).
95
- - To stop a running job, use `bash` with `kill <pid>` (the pid is in the `bgstatus`
96
- output). There is no `bgkill` tool.
166
+ session-scoped plus an orphan pass (default on — removes finished week-old
167
+ logs from crashed/abandoned sessions; disable with `globalAutoClean: false`
168
+ / `PI_BGRUN_GLOBAL_AUTO_CLEAN=0`). Under the project-local default the orphan
169
+ pass covers the current project's jobs dir AND the machine-global
170
+ `~/.pi-bgrun/jobs`; an explicit absolute `jobsDir` is swept alone. Retention
171
+ is `cleanupDays` (default 7, configurable).
172
+ - To stop a running job, use `bash` with `kill -- -<pid>` (process group — required because the child is spawned detached). The pid is the last `--`-separated segment of the job id; it is not shown as a separate field in `bgstatus` output. There is no `bgkill` tool.