prdigest 0.1.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8788f06c317608d4ae85c2784408764af17e36148becd307a9332f853f7bb7eb
4
- data.tar.gz: e975b567596a673fabf283ae5b81e1c85bc901f3f54c71b5275bd24c29f1c3f8
3
+ metadata.gz: 9ff60040fe7091056de1781ae7154e758145cf7216ec1b664adc661cd479c96f
4
+ data.tar.gz: aa87baccb18ac37b7398c83e5d312187e3d697fa8fa9e161c6f3b930cef73ae4
5
5
  SHA512:
6
- metadata.gz: e20edb4683ef19cb47b498258536f1f5ef6b51331a69e41aa1730b5c65d7d72dc6418f260ea58bccffa47aaf564150dad24da53358f04bf85500cc19982f11c0
7
- data.tar.gz: 4ff9015ad80f6624b7ba726bd3c616dff0890c9eec6ffd0315a3d840dee07c7123295d5c914f99d3cac420254c2167c7a93325dbbe85b843c42d454f3f34657d
6
+ metadata.gz: 2e7499399445ffa51a7acb8bd0f7066bf3f930108d56f070cafdfdc49e4a8b6727b6288b3a5e95cae3ed36f6e01c19fa90726522c6aeb731a45edf1c6a88718a
7
+ data.tar.gz: 7bc1c60e8518b9abebeb1beb6fdb08b12aafb39beb4056db7f96fc225b2344e9643f5d27108138a4eecb06d100db16f55c18dd075ffdc35152087b7f5ad58dc3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-07-27
4
+
5
+ - Remove `prdigest run`, deterministic Telegram HTML, schedule/cursor state,
6
+ catch-up processing, the `prdigest-result` envelope, and the `serve` stub.
7
+ - Keep `prdigest facts` for agents and `prdigest prose` for stdout or
8
+ checkpointed plain-text Telegram delivery.
9
+ - Make `prdigest prose --deliver` the systemd and container default.
10
+
11
+ ## 0.2.0 - 2026-07-26
12
+
13
+ - Add a deterministic `prdigest facts` JSON contract over the canonical
14
+ collection engine without scheduling, Telegram, or provider side effects.
15
+ - Add optional OpenAI-compatible prose for stdout or explicit checkpointed
16
+ Telegram delivery, with provider failures kept visible.
17
+ - Restrict plaintext provider URLs to strict loopback hosts and reject generated
18
+ terminal control characters before output or delivery.
19
+ - Add a repository-owned, ClawHub-ready OpenClaw skill that writes prose only
20
+ from validated PRDigest facts.
21
+ - Prove the facts command from an isolated installed gem without provider or
22
+ network access beyond the stubbed GitHub boundary.
23
+
3
24
  ## 0.1.1 - 2026-07-25
4
25
 
5
26
  - Accept Octokit `Time` objects as well as ISO-8601 strings for pull-request
data/README.md CHANGED
@@ -1,155 +1,319 @@
1
- # PRDigest
1
+ <div align="center">
2
2
 
3
- PRDigest is a deterministic Ruby oneshot that sends a daily digest of merged
4
- GitHub pull requests to one allowlisted Telegram chat. It is designed for a
5
- single operator on a small VPS: repositories are explicit, secrets stay in the
6
- environment, and a hardened systemd timer owns scheduling.
3
+ <h1>PRDigest</h1>
7
4
 
8
- ## Requirements and installation
5
+ <p><strong>One merged-PR facts contract. Prose for people and JSON for agents.</strong></p>
9
6
 
10
- - Ruby 3.2–3.4 and `tzdata`
11
- - a fine-grained GitHub token with read-only access to the listed repositories
12
- - a dedicated Telegram bot and one destination chat ID
7
+ <p>
8
+ <a href="https://github.com/ivankuznetsov/prdigest/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/ivankuznetsov/prdigest/actions/workflows/ci.yml/badge.svg"></a>
9
+ <a href="https://rubygems.org/gems/prdigest"><img alt="Gem version" src="https://img.shields.io/gem/v/prdigest.svg"></a>
10
+ <a href="https://www.ruby-lang.org/"><img alt="Ruby 3.2+" src="https://img.shields.io/badge/Ruby-3.2%2B-CC342D?logo=ruby&amp;logoColor=white"></a>
11
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
12
+ </p>
13
13
 
14
- For development:
14
+ <p>
15
+ Give OpenClaw and other agents stable merged-PR facts, or generate concise<br>
16
+ prose through any OpenAI-compatible Chat Completions endpoint.
17
+ </p>
18
+
19
+ <p>
20
+ <a href="#quick-start">Quick start</a> ·
21
+ <a href="#choose-a-mode">Choose a mode</a> ·
22
+ <a href="#configuration">Configuration</a> ·
23
+ <a href="#deployment">Deploy</a> ·
24
+ <a href="#openclaw">OpenClaw</a>
25
+ </p>
26
+
27
+ </div>
28
+
29
+ ---
30
+
31
+ ## Why PRDigest?
32
+
33
+ PRDigest keeps collection separate from presentation. Every mode starts from
34
+ the same ordered, immutable facts, so adding an agent or prose model never
35
+ changes what was fetched from GitHub.
36
+
37
+ ```mermaid
38
+ flowchart LR
39
+ GH[GitHub repositories] --> C[Canonical collector]
40
+ C --> FACTS["prdigest facts<br/>versioned JSON"]
41
+ C --> PROSE["prdigest prose<br/>provider-written text"]
42
+ FACTS --> OC[OpenClaw or another client]
43
+ PROSE --> OUT[stdout or explicit Telegram delivery]
44
+ ```
45
+
46
+ - **Stable facts for agents** — repository order, pull-request order, and JSON
47
+ shape are deterministic.
48
+ - **AI stays explicit** — `facts` never configures or contacts a prose provider.
49
+ - **Safe to resume** — delivery checkpoints prevent already accepted chunks
50
+ from being sent twice.
51
+ - **Secrets stay out of config** — YAML names environment variables; it never
52
+ contains token values.
53
+
54
+ ## Choose a mode
55
+
56
+ | What you need | Command | Result | Side effects |
57
+ |---|---|---|---|
58
+ | Facts for OpenClaw or another agent | `prdigest facts` | `prdigest-facts` JSON on stdout | No Telegram, provider, or delivery state |
59
+ | Provider-written text | `prdigest prose` | Plain text on stdout | Fresh run calls GitHub, then the provider; no Telegram or checkpoint |
60
+ | Scheduled or one-off Telegram prose | `prdigest prose --deliver` | Checkpointed plain-text Telegram delivery | Fresh run calls GitHub and provider, persists, then sends; resume reuses the checkpoint |
61
+
62
+ All modes accept `--date YYYY-MM-DD` and repeatable
63
+ `--repo OWNER/NAME` overrides. Repository order is always preserved.
64
+
65
+ ## Quick start
66
+
67
+ ### 1. Install from source
68
+
69
+ Ruby 3.2 or newer and `tzdata` are required. CI covers Ruby 3.2–3.4.
15
70
 
16
71
  ```sh
72
+ git clone https://github.com/ivankuznetsov/prdigest.git
73
+ cd prdigest
17
74
  bundle install
18
- bundle exec prdigest version
19
- GITHUB_TOKEN=... bundle exec prdigest run --config configs/config.example.yml --dry-run
75
+ cp configs/config.example.yml prdigest.yml
20
76
  ```
21
77
 
22
- To build and install the prepared gem without publishing it:
78
+ Before running a delivery command from the source checkout, change the copied
79
+ config to state paths writable by your user:
80
+
81
+ ```yaml
82
+ state:
83
+ delivery_path: tmp/prdigest/deliveries
84
+ ```
85
+
86
+ Keep the example's `/var/lib/prdigest` paths for the packaged systemd
87
+ deployment, which creates that directory for the `prdigest` service user.
88
+
89
+ Edit `prdigest.yml`, then export the narrowly scoped credentials needed by the
90
+ mode you plan to run:
91
+
92
+ ```sh
93
+ export GITHUB_TOKEN=github_pat_...
94
+ export TELEGRAM_BOT_TOKEN=... # prose --deliver only
95
+ export OPENROUTER_API_KEY=... # prose only; use your configured env name
96
+ ```
97
+
98
+ Use a fine-grained GitHub token with read-only access to the configured
99
+ repositories. Delivery modes should use a dedicated Telegram bot and one
100
+ allowlisted destination chat.
101
+
102
+ ### 2. Try the read-only paths
103
+
104
+ ```sh
105
+ # Stable JSON: no Telegram, delivery state, or prose provider
106
+ bundle exec prdigest facts --config prdigest.yml
107
+
108
+ # Provider-written text on stdout: no Telegram or delivery checkpoint
109
+ bundle exec prdigest prose --config prdigest.yml
110
+ ```
111
+
112
+ ### 3. Deliver intentionally
113
+
114
+ ```sh
115
+ # Provider-written delivery, checkpointed before the first Telegram request
116
+ bundle exec prdigest prose --config prdigest.yml --deliver
117
+ ```
118
+
119
+ To build and install the current checkout as a gem without publishing it:
23
120
 
24
121
  ```sh
25
122
  gem build prdigest.gemspec
26
- gem install prdigest-0.1.1.gem
123
+ gem install prdigest-0.3.0.gem
27
124
  prdigest version
28
125
  ```
29
126
 
30
- ## Configuration and secrets
127
+ The [published RubyGems version](https://rubygems.org/gems/prdigest) may trail
128
+ the current `main` branch.
129
+
130
+ ### Publishing for maintainers
131
+
132
+ RubyGems publication uses GitHub OIDC rather than a stored API key. Configure
133
+ the `prdigest` gem's RubyGems trusted publisher with repository owner
134
+ `ivankuznetsov`, repository `prdigest`, workflow `release.yml`, and environment
135
+ `release`.
136
+
137
+ Pushing a new exact `vX.Y.Z` tag publishes it automatically. To publish an
138
+ existing prepared tag such as `v0.1.1`, run the **Release gem** workflow
139
+ manually and supply that tag. The workflow checks that the immutable tag and
140
+ package version agree, runs the unit and clean-install smoke suites, builds and
141
+ verifies the exact gem, then exchanges GitHub's OIDC identity for a short-lived
142
+ RubyGems credential immediately before `gem push`.
143
+
144
+ ## Configuration
31
145
 
32
- Configuration lookup is strict: an explicit `--config PATH` wins, then
33
- `PRDIGEST_CONFIG`, then an existing `/etc/prdigest/config.yml`. PRDigest refuses
34
- to run when none exists. See `configs/config.example.yml` for every setting.
146
+ Lookup is strict: `--config PATH`, then `PRDIGEST_CONFIG`, then an existing
147
+ `/etc/prdigest/config.yml`. PRDigest refuses to run when no configuration is
148
+ found.
35
149
 
36
150
  ```yaml
37
151
  timezone: Europe/London
38
- schedule:
39
- max_catchup_days: 7
152
+
40
153
  github:
41
154
  token_env: GITHUB_TOKEN
42
155
  repos:
43
- - owner/repo-one
44
- - owner/repo-two
156
+ - owner/api
157
+ - owner/web
158
+
159
+ digest:
160
+ line_stats: true
161
+
45
162
  telegram:
46
163
  token_env: TELEGRAM_BOT_TOKEN
47
164
  chat_id_allowlist: [-1001234567890]
48
165
  chat_id: -1001234567890
49
- digest:
50
- line_stats: true
51
- send_empty: true
52
- empty_message: "Merged PR digest — {date}\nTotal: 0 PRs"
166
+
53
167
  state:
54
- path: /var/lib/prdigest/state.json
55
168
  delivery_path: /var/lib/prdigest/deliveries
169
+
170
+ prose:
171
+ provider: openai_compatible
172
+ base_url: https://openrouter.ai/api/v1
173
+ model: provider/model
174
+ api_key_env: OPENROUTER_API_KEY
56
175
  ```
57
176
 
58
- Repository order controls digest order. `max_catchup_days` must be `1..30`.
59
- `chat_id` must appear in the non-empty allowlist; extra allowlisted IDs are
60
- accepted for schema compatibility but v0.1.x sends only to `chat_id`. Token
61
- values belong in environment variables, never YAML.
177
+ See [`configs/config.example.yml`](configs/config.example.yml) for the complete
178
+ annotated configuration.
62
179
 
63
- ## Commands and results
180
+ <details>
181
+ <summary><strong>Configuration rules</strong></summary>
64
182
 
65
- ```sh
66
- prdigest run [--config PATH] [--date YYYY-MM-DD] [--dry-run] [--json] [--repo OWNER/NAME ...]
67
- prdigest serve
183
+ - Repository order controls digest order.
184
+ - `chat_id` must appear in the non-empty allowlist. Extra IDs are accepted for
185
+ schema compatibility, but delivery sends only to `chat_id`.
186
+ - Token values belong in environment variables, never YAML.
187
+ - The prose block is validated only for `prdigest prose`; `facts` ignores it.
188
+ - Remote provider URLs require HTTPS. Plaintext HTTP is accepted only for exact
189
+ loopback hosts.
190
+
191
+ </details>
192
+
193
+ ## Command reference
194
+
195
+ ```text
196
+ prdigest facts [--config PATH] [--date YYYY-MM-DD] [--repo OWNER/NAME ...]
197
+ prdigest prose [--config PATH] [--date YYYY-MM-DD] [--repo OWNER/NAME ...] [--deliver]
68
198
  prdigest version
69
199
  ```
70
200
 
71
- An ordinary `run` reads state, processes owed dates oldest-first, and writes
72
- state after each settled day. `--date` replays exactly that local date and never
73
- reads or writes state. `--dry-run` previews the explicit date or yesterday,
74
- requires GitHub access, and constructs neither state nor Telegram delivery.
75
- `serve` remains a compatibility stub; use the supplied timer.
76
- Repeatable `--repo` values replace the configured repository list for that run.
77
- They use the same strict `owner/name` validation and deterministic order as the
78
- configuration, which lets callers such as Hive supply their registered projects
79
- without owning a second digest implementation.
80
-
81
- `--json` emits a versioned `prdigest-result` document with `status`, `mode`, `requested_days`,
82
- `settled_days`, `skipped_days`, `failed_date`, `remaining_days`, nullable
83
- `error`, dry-run `chunks`, and nullable structured `delivery` progress.
84
- `delivery` reports `accepted_chunks`, `total_chunks`, `status`, and, on failure,
85
- `failed_chunk`. Modes are `scheduled` and
86
- `explicit_date_replay`; statuses are `success`, `dry_run`, `failure`, and
87
- `partial_failure`.
88
-
89
- | Exit | Meaning |
90
- |---:|---|
91
- | 0 | completed or dry-run |
92
- | 1 | unexpected or render failure |
93
- | 2 | CLI/configuration refusal |
94
- | 3 | GitHub failure |
95
- | 4 | Telegram failure |
96
- | 5 | state failure |
97
- | 6 | failure after earlier durable progress |
98
-
99
- ## Delivery and state semantics
201
+ ### `facts`
100
202
 
101
- Each local day is converted to independent UTC midnight boundaries, including
102
- DST gaps and repeats. GitHub results are completely fetched and rendered before
103
- the first Telegram message. A day is settled only after every chunk succeeds,
104
- an enabled empty message succeeds, or an empty message is intentionally
105
- suppressed.
106
-
107
- Before sending, PRDigest stores the complete rendered chunk list in
108
- `state.delivery_path`. It marks one chunk in flight before each request and
109
- advances `next_chunk` only after Telegram definitely accepts it. A definite
110
- 429/5xx rejection receives at most three attempts; a later invocation resumes
111
- the original rendered payload at the next unsent chunk. If the date cursor write
112
- fails after complete delivery, the retry observes the completed delivery
113
- checkpoint and sends nothing before repairing the cursor.
114
-
115
- Telegram 400/invalid-response failures are permanent. Transport failures are
116
- ambiguous because Telegram might have accepted the request before the
117
- connection failed. Both cases remain parked and never replay automatically.
118
- Inspect the checkpoint and Telegram chat, reconcile the uncertain chunk, then
119
- move the checkpoint aside only when an intentional operator replay is safe.
120
- Changing the chat or repository scope for an existing date also fails closed.
121
- Explicit `--date` uses this same safety ledger, so a completed date is a no-op
122
- unless its checkpoint is deliberately archived first.
123
-
124
- State is secret-free JSON version 1:
203
+ Fetches one explicit date or yesterday and prints exactly one JSON document. It
204
+ never reads or writes scheduling state, constructs Telegram delivery, or
205
+ contacts the prose provider.
206
+
207
+ A complete successful empty digest looks like:
125
208
 
126
209
  ```json
127
- {"version":1,"timezone":"Europe/London","last_digested_date":"2026-07-15"}
210
+ {
211
+ "schema": "prdigest-facts",
212
+ "schema_version": 1,
213
+ "status": "success",
214
+ "error": null,
215
+ "digest": {
216
+ "date": "2026-07-25",
217
+ "timezone": "Europe/London",
218
+ "line_stats": true,
219
+ "repository_order": ["owner/api", "owner/web"],
220
+ "repositories": [
221
+ {"name": "owner/api", "pull_requests": []},
222
+ {"name": "owner/web", "pull_requests": []}
223
+ ],
224
+ "totals": {
225
+ "pull_requests": 0,
226
+ "additions": 0,
227
+ "deletions": 0,
228
+ "commits": 0
229
+ }
230
+ }
231
+ }
128
232
  ```
129
233
 
130
- It may include a `last_skip` audit with `start_date`, `end_date`, and
131
- `notice_pending`. Writes use an atomic mode-`0600` replacement and directory
132
- fsync. Delivery directories are mode `0700`; delivery files and locks are mode
133
- `0600`, and a per-date lock prevents concurrent sends for the same checkpoint.
134
- Missing state means first run and requests yesterday only. Malformed,
135
- future, unsupported, unreadable, or timezone-mismatched state fails closed with
136
- exit 5.
234
+ The full digest includes ordered repositories and pull requests, authors, URLs,
235
+ UTC merge times, nullable line and commit statistics, and aggregate totals.
236
+ Disabled statistics remain `null`, not zero. There is no generation timestamp,
237
+ so identical accepted GitHub input produces identical JSON.
238
+
239
+ ### `prose`
240
+
241
+ Sends the same facts document as untrusted data to
242
+ `<prose.base_url>/chat/completions`. The provider is instructed to present those
243
+ facts without adding to or changing them.
244
+
245
+ Without `--deliver`, prose is printed to stdout. With `--deliver`, the final
246
+ plain-text chunks are stored under `state.delivery_path/prose` before the first
247
+ Telegram request. Provider output containing terminal control
248
+ characters is rejected before it can reach stdout, a checkpoint, or Telegram.
249
+
250
+ There is no silent fallback to another provider.
251
+
252
+ ## Delivery guarantees
253
+
254
+ PRDigest treats sending as a durable protocol, not a best-effort loop:
255
+
256
+ 1. Fetch facts and generate the complete prose digest.
257
+ 2. Persist the exact final chunk list.
258
+ 3. Mark a chunk in flight before sending.
259
+ 4. Advance only after Telegram definitely accepts it.
260
+ 5. Resume at the next unsent chunk after a definite failure.
261
+
262
+ A definite Telegram 429/5xx response receives at most three attempts. Transport
263
+ failures are ambiguous because Telegram may have accepted the request before
264
+ the connection failed; PRDigest parks them instead of risking a duplicate.
265
+
266
+ Once a prose payload exists, retry
267
+ loads those exact chunks before checking GitHub or provider credentials, so a
268
+ resume never regenerates different prose. A failure before the payload becomes
269
+ durable can incur another provider request on retry.
270
+
271
+ <details>
272
+ <summary><strong>Date and checkpoint details</strong></summary>
273
+
274
+ Each local day is converted to independent UTC midnight boundaries, including
275
+ DST gaps and repeats. Both commands use yesterday in the configured timezone
276
+ unless `--date YYYY-MM-DD` is supplied.
137
277
 
138
- When backlog exceeds the cap, PRDigest durably skips the oldest prefix and
139
- processes only the newest window. That loss is intentional and reported. For a
140
- timezone migration, stop the timer, preserve the old state for audit, move it
141
- aside, change the configured timezone, use explicit replay for any required
142
- dates, then restart the timer. Never silently edit a corrupt checkpoint; inspect
143
- and repair ownership/JSON or restore a known-good copy first.
278
+ Checkpoint directories are mode `0700`; files and locks are mode `0600`.
279
+ A per-date lock prevents concurrent sends for the same repository scope and
280
+ chat. While a completed checkpoint exists, repeating `prose --deliver` for that
281
+ date is a no-op. Moving that checkpoint aside intentionally allows complete
282
+ regeneration and redelivery.
144
283
 
145
- GitHub does not publish a search-index freshness guarantee. Keep the host
146
- timezone equal to the configured digest timezone; the supplied 09:05 timer then
147
- leaves about nine hours after local midnight. If a later audit finds a delayed
148
- merge, use `--date YYYY-MM-DD` to replay it.
284
+ The supplied systemd timer invokes `prose --deliver` once each day. PRDigest
285
+ does not maintain a catch-up cursor: explicitly run missed dates with `--date`.
149
286
 
150
- ## systemd deployment
287
+ </details>
151
288
 
152
- On Ubuntu, install Ruby, `tzdata`, the gem, and then:
289
+ ## OpenClaw
290
+
291
+ The repository includes a ClawHub-ready skill at
292
+ [`openclaw/skills/prdigest/SKILL.md`](openclaw/skills/prdigest/SKILL.md). It:
293
+
294
+ - invokes only `prdigest facts`;
295
+ - validates schema version 1 and the success envelope;
296
+ - treats pull-request fields as untrusted data, never instructions;
297
+ - makes no second GitHub request; and
298
+ - never invokes delivery or configures a prose provider.
299
+
300
+ The skill is source-ready, **not claimed as published**. After a separately
301
+ authorized ClawHub release, the expected command is:
302
+
303
+ ```sh
304
+ clawhub install @ivankuznetsov/prdigest
305
+ ```
306
+
307
+ ClawHub installation and Ruby CLI installation are separate trust boundaries.
308
+ See [`openclaw/README.md`](openclaw/README.md) for local use and the
309
+ release-gated publication checklist.
310
+
311
+ ## Deployment
312
+
313
+ <details open>
314
+ <summary><strong>systemd on Ubuntu</strong></summary>
315
+
316
+ Install Ruby, `tzdata`, and the gem, then:
153
317
 
154
318
  ```sh
155
319
  sudo useradd --system --home /nonexistent --shell /usr/sbin/nologin prdigest
@@ -158,69 +322,87 @@ sudo install -o root -g prdigest -m 0640 configs/config.example.yml /etc/prdiges
158
322
  sudo install -o root -g root -m 0600 .env.example /etc/prdigest/.env
159
323
  sudo install -o root -g root -m 0644 scripts/systemd/prdigest.service /etc/systemd/system/
160
324
  sudo install -o root -g root -m 0644 scripts/systemd/prdigest.timer /etc/systemd/system/
325
+ sudoedit /etc/prdigest/config.yml
326
+ sudoedit /etc/prdigest/.env
161
327
  sudo systemctl daemon-reload
162
328
  sudo systemctl start prdigest.service
163
329
  sudo systemctl enable --now prdigest.timer
164
330
  ```
165
331
 
166
- Edit the installed config and environment file before starting. systemd creates
167
- `/var/lib/prdigest` as `prdigest:prdigest` mode `0700`; state files are `0600`.
168
- Inspect with `systemctl status prdigest.service`, `systemctl list-timers
169
- prdigest.timer`, and `journalctl -u prdigest.service`. Test a missed-day catch-up
170
- by stopping the timer for a day, then starting the service. Journal output can
171
- contain repository/date context, so restrict journal group membership and
172
- retention.
332
+ systemd creates `/var/lib/prdigest` as `prdigest:prdigest` mode `0700`.
173
333
 
174
- Rollback by stopping the timer, installing the prior gem/image, restoring the
175
- matching config and a known-good state backup, and restarting. Replay omitted
176
- dates explicitly; do not move a checkpoint forward by hand.
334
+ ```sh
335
+ systemctl status prdigest.service
336
+ systemctl list-timers prdigest.timer
337
+ journalctl -u prdigest.service
338
+ ```
339
+
340
+ Journal output can contain repository and date context, so restrict journal
341
+ group membership and retention.
342
+
343
+ </details>
177
344
 
178
- ## Container
345
+ <details>
346
+ <summary><strong>Container</strong></summary>
179
347
 
180
- The Alpine image includes `tzdata` and runs as the unprivileged `prdigest` user.
181
- Initialize mounted ownership with the image before normal use:
348
+ The Alpine image includes `tzdata` and runs as the unprivileged `prdigest`
349
+ user. Initialize mounted ownership once before normal use:
182
350
 
183
351
  ```sh
184
- docker build -t prdigest:0.1.1 .
352
+ docker build -t prdigest:local .
185
353
  docker volume create prdigest-state
186
- docker run --rm --user root --entrypoint sh -v prdigest-state:/var/lib/prdigest prdigest:0.1.1 \
354
+ docker run --rm --user root --entrypoint sh \
355
+ -v prdigest-state:/var/lib/prdigest \
356
+ prdigest:local \
187
357
  -c 'chown -R prdigest:prdigest /var/lib/prdigest && chmod 0700 /var/lib/prdigest'
358
+
188
359
  docker run --rm --env-file /etc/prdigest/.env \
189
360
  -v /etc/prdigest/config.yml:/etc/prdigest/config.yml:ro \
190
- -v prdigest-state:/var/lib/prdigest prdigest:0.1.1
361
+ -v prdigest-state:/var/lib/prdigest \
362
+ prdigest:local
191
363
  ```
192
364
 
193
- ## Verification and troubleshooting
365
+ </details>
366
+
367
+ ### Rollback
194
368
 
195
- `bundle exec rake test` is fully offline. Release preparation also supplies
196
- `test/smoke/gem_install.sh`, `test/smoke/docker.sh`, and
197
- `test/smoke/systemd.sh`. Live GitHub boundary and Telegram allowlist smokes are
198
- manual gates: retain timestamps and pass/fail only, never credentials, response
199
- bodies, message text, or private titles.
369
+ Stop the timer, install the prior gem or image, restore its matching config and
370
+ checkpoint backup, then restart. Never move a checkpoint forward by hand.
200
371
 
201
- - Exit 2: check config discovery, YAML, timezone, cap, chat allowlist, and env.
202
- - Exit 3: check repository access, rate/search limits, and replay later.
203
- - Exit 4: inspect `error.kind` and `delivery`. Retry ordinary `telegram` failures;
204
- reconcile `telegram_ambiguous`, `telegram_permanent`, and
205
- `delivery_checkpoint_permanent` before moving any checkpoint.
206
- - Exit 5: check `/var/lib/prdigest`, mode/owner, JSON version, date, and timezone.
207
- - Exit 6: earlier dates or a skipped prefix are durable; inspect the result before retry.
372
+ ## Exit codes and troubleshooting
208
373
 
209
- Concurrent scheduled runs remain unsupported because date-cursor scheduling is
210
- single-owner. Delivery itself takes a nonblocking per-date lock, so a competing
211
- sender fails instead of duplicating the same payload. The systemd oneshot is the
212
- normal run coordination mechanism.
374
+ | Exit | Meaning | First check |
375
+ |---:|---|---|
376
+ | `0` | Completed | |
377
+ | `1` | Unexpected or render failure | Logs and input shape |
378
+ | `2` | CLI/configuration refusal | Config path, YAML, timezone, allowlist, env |
379
+ | `3` | GitHub failure | Token scope, repository access, rate/search limits |
380
+ | `4` | Telegram failure | `error.kind` and delivery checkpoint |
381
+ | `5` | Checkpoint state failure | Path, owner, mode, checkpoint JSON |
382
+ | `7` | Provider failure or ambiguous outcome | Endpoint, model, key env, retry cost |
213
383
 
214
- See [SECURITY.md](SECURITY.md) for token scope, rotation, and private-data flow.
384
+ Reconcile `telegram_ambiguous`, `telegram_permanent`, and
385
+ `delivery_checkpoint_permanent` before moving a checkpoint. Overlapping delivery
386
+ for the same date is refused by the checkpoint lock; the systemd oneshot is the
387
+ normal scheduler.
215
388
 
216
- ## Non-goals
389
+ See [`SECURITY.md`](SECURITY.md) for token scope, rotation, and private-data
390
+ flow.
391
+
392
+ ## Development
393
+
394
+ ```sh
395
+ bundle install
396
+ bundle exec rake test
397
+ test/smoke/gem_install.sh
398
+ test/smoke/docker.sh
399
+ test/smoke/systemd.sh
400
+ ```
217
401
 
218
- v0.1.x has no built-in Hive configuration discovery, LLM content, web UI,
219
- interactive bot commands,
220
- multi-chat routing, organization discovery, non-GitHub forge support, or
221
- long-running scheduler. The build and test process never tags or publishes
222
- automatically.
402
+ The test suite is fully offline. Live GitHub and Telegram smokes are manual
403
+ release gates; retain only timestamps and pass/fail status, never credentials,
404
+ response bodies, generated prose, or private titles.
223
405
 
224
406
  ## License
225
407
 
226
- MIT
408
+ [MIT](LICENSE) © 2026 Ivan Kuznetsov
data/SECURITY.md CHANGED
@@ -12,10 +12,42 @@ Telegram bot whose `chat_id` is the only delivery target in the allowlist. Rotat
12
12
  either token immediately if it may have appeared in output, then inspect and
13
13
  restrict journal retention.
14
14
 
15
+ Standalone prose uses the environment variable named by
16
+ `prose.api_key_env`. Never place the provider key itself in YAML, shell history,
17
+ arguments, checkpoints, fixtures, or logs. Remote provider URLs must use HTTPS;
18
+ plaintext HTTP is accepted only for exact `localhost`, IPv4 `127.0.0.0/8`, or
19
+ IPv6 `::1` loopback hosts. PRDigest rejects provider URLs with embedded
20
+ credentials, query strings, or fragments and does not include provider response
21
+ bodies in errors. Generated prose containing C0/C1 terminal controls is rejected
22
+ before stdout, checkpoints, or Telegram; tabs and newlines remain allowed.
23
+
15
24
  Private pull-request titles and author names cross from GitHub into the configured
16
25
  Telegram chat. Treat that chat and its members as having access to repository
17
26
  metadata. PRDigest refuses non-allowlisted chat IDs before opening a connection.
18
27
 
28
+ `prdigest facts` writes repository names, titles, authors, URLs, merge times, and
29
+ optional statistics to stdout. The caller owns that output after the single JSON
30
+ document is emitted; avoid terminal capture, shell tracing, and logs that are
31
+ broader than the repository's audience.
32
+
33
+ OpenClaw mode sends that facts document into the configured OpenClaw execution
34
+ and model boundary so OpenClaw can write prose. Standalone `prdigest prose`
35
+ sends the same complete document plus the configured model name to the
36
+ OpenAI-compatible endpoint. Private repositories therefore require an OpenClaw
37
+ deployment or provider whose data handling, retention, and access controls are
38
+ acceptable to the operator. Provider access is never ambient: `prdigest facts`
39
+ does not contact the standalone prose provider.
40
+
41
+ Pull-request fields are untrusted input. The built-in provider prompt and the
42
+ OpenClaw skill explicitly classify the facts JSON as data, never instructions,
43
+ but operators should still restrict tools and authority available to any model
44
+ processing private repository content. The OpenClaw skill must not make a second
45
+ GitHub query, deliver messages, install software silently, or print credentials.
46
+
47
+ Prose Telegram checkpoints contain generated text derived from repository facts.
48
+ They use secret-free mode-`0600` files inside mode-`0700` directories. Protect,
49
+ retain, and delete them as private repository metadata.
50
+
19
51
  ## Reporting
20
52
 
21
53
  Report vulnerabilities privately to the maintainer address in the gem metadata.
@@ -25,4 +57,6 @@ state files, or unredacted journal output.
25
57
  ## Supported release
26
58
 
27
59
  Security fixes target the latest published release. The build and test process
28
- does not tag or publish releases automatically.
60
+ does not tag or publish releases automatically. The exact-tag release workflow
61
+ uses a RubyGems trusted publisher and a short-lived GitHub OIDC credential; no
62
+ long-lived RubyGems API key is stored in the repository or Actions secrets.