prdigest 0.1.1 → 0.2.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: 409ae44cab5ad005c5dcb413632295fdd3aae2e8d6383852b71863f8fc484d15
4
+ data.tar.gz: 7ae70339af9e94123d83795362bfde4c07ee57d94bf447123fe4ce305f9117b4
5
5
  SHA512:
6
- metadata.gz: e20edb4683ef19cb47b498258536f1f5ef6b51331a69e41aa1730b5c65d7d72dc6418f260ea58bccffa47aaf564150dad24da53358f04bf85500cc19982f11c0
7
- data.tar.gz: 4ff9015ad80f6624b7ba726bd3c616dff0890c9eec6ffd0315a3d840dee07c7123295d5c914f99d3cac420254c2167c7a93325dbbe85b843c42d454f3f34657d
6
+ metadata.gz: f5c7c0e265e13c87e2a625c3ee8fd10cde644b99297277b0dd5fd1f2f4a980ea0d7d81f67a63021b9741af8811b8903af47595c9713b507dd36fb749118e4489
7
+ data.tar.gz: 4c787a605900e7f93722a72929708cf5032e35d1aff08a2134d41430d6e17b58f578ef20d965141391c87943eefbe3e99781a99cfcf72f08e0b35257c90d1d50
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-07-26
4
+
5
+ - Add a deterministic `prdigest facts` JSON contract over the canonical
6
+ collection engine without scheduling, Telegram, or provider side effects.
7
+ - Add optional OpenAI-compatible prose for stdout or explicit checkpointed
8
+ Telegram delivery, with provider failures kept visible.
9
+ - Restrict plaintext provider URLs to strict loopback hosts and reject generated
10
+ terminal control characters before output or delivery.
11
+ - Add a repository-owned, ClawHub-ready OpenClaw skill that writes prose only
12
+ from validated PRDigest facts.
13
+ - Prove the facts command from an isolated installed gem without provider or
14
+ network access beyond the stubbed GitHub boundary.
15
+
3
16
  ## 0.1.1 - 2026-07-25
4
17
 
5
18
  - Accept Octokit `Time` objects as well as ISO-8601 strings for pull-request
data/README.md CHANGED
@@ -1,125 +1,311 @@
1
- # PRDigest
1
+ <div align="center">
2
+
3
+ <h1>PRDigest</h1>
4
+
5
+ <p><strong>One source of truth for merged pull requests. Three ways to present it.</strong></p>
6
+
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
+
14
+ <p>
15
+ Collect merged PRs once, then deliver a deterministic Telegram digest, emit<br>
16
+ stable JSON for an agent, or generate optional prose through an
17
+ OpenAI-compatible Chat Completions endpoint.
18
+ </p>
19
+
20
+ <p>
21
+ <a href="#quick-start">Quick start</a> ·
22
+ <a href="#choose-a-mode">Choose a mode</a> ·
23
+ <a href="#configuration">Configuration</a> ·
24
+ <a href="#deployment">Deploy</a> ·
25
+ <a href="#openclaw">OpenClaw</a>
26
+ </p>
27
+
28
+ </div>
29
+
30
+ ---
31
+
32
+ ## Why PRDigest?
33
+
34
+ PRDigest keeps collection separate from presentation. Every mode starts from
35
+ the same ordered, immutable facts, so adding an agent or prose model never
36
+ changes what was fetched from GitHub.
37
+
38
+ ```mermaid
39
+ flowchart LR
40
+ GH[GitHub repositories] --> C[Canonical collector]
41
+ C --> RUN["prdigest run<br/>deterministic Telegram"]
42
+ C --> FACTS["prdigest facts<br/>versioned JSON"]
43
+ C --> PROSE["prdigest prose<br/>provider-written text"]
44
+ FACTS --> OC[OpenClaw or another client]
45
+ PROSE --> OUT[stdout or explicit Telegram delivery]
46
+ ```
47
+
48
+ - **Deterministic by default** — repository order, pull-request order, and JSON
49
+ shape are stable.
50
+ - **AI stays optional** — neither `run` nor `facts` configures or contacts a
51
+ prose provider.
52
+ - **Safe to resume** — delivery checkpoints prevent already accepted chunks
53
+ from being sent twice.
54
+ - **Secrets stay out of config** — YAML names environment variables; it never
55
+ contains token values.
56
+
57
+ ## Choose a mode
2
58
 
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.
59
+ | What you need | Command | Result | Side effects |
60
+ |---|---|---|---|
61
+ | Reliable scheduled digest | `prdigest run` | Deterministic Telegram HTML | Reads/writes schedule and delivery state |
62
+ | Facts for OpenClaw or another client | `prdigest facts` | `prdigest-facts` JSON on stdout | No schedule state, Telegram, or AI provider |
63
+ | Provider-written text | `prdigest prose` | Plain text on stdout | Fresh run calls GitHub, then the provider; no Telegram or checkpoint |
64
+ | Provider-written Telegram digest | `prdigest prose --deliver` | Checkpointed Telegram delivery | Fresh run calls GitHub and provider, persists, then sends; resume uses the checkpoint without GitHub/provider generation |
7
65
 
8
- ## Requirements and installation
66
+ All modes accept `--date YYYY-MM-DD` and repeatable
67
+ `--repo OWNER/NAME` overrides. Repository order is always preserved.
9
68
 
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
69
+ ## Quick start
13
70
 
14
- For development:
71
+ ### 1. Install from source
72
+
73
+ Ruby 3.2 or newer and `tzdata` are required. CI covers Ruby 3.2–3.4.
15
74
 
16
75
  ```sh
76
+ git clone https://github.com/ivankuznetsov/prdigest.git
77
+ cd prdigest
17
78
  bundle install
18
- bundle exec prdigest version
19
- GITHUB_TOKEN=... bundle exec prdigest run --config configs/config.example.yml --dry-run
79
+ cp configs/config.example.yml prdigest.yml
80
+ ```
81
+
82
+ Before running a delivery command from the source checkout, change the copied
83
+ config to state paths writable by your user:
84
+
85
+ ```yaml
86
+ state:
87
+ path: tmp/prdigest/state.json
88
+ delivery_path: tmp/prdigest/deliveries
89
+ ```
90
+
91
+ Keep the example's `/var/lib/prdigest` paths for the packaged systemd
92
+ deployment, which creates that directory for the `prdigest` service user.
93
+
94
+ Edit `prdigest.yml`, then export the narrowly scoped credentials needed by the
95
+ mode you plan to run:
96
+
97
+ ```sh
98
+ export GITHUB_TOKEN=github_pat_...
99
+ export TELEGRAM_BOT_TOKEN=... # run or prose --deliver only
100
+ export OPENROUTER_API_KEY=... # prose only; use your configured env name
101
+ ```
102
+
103
+ Use a fine-grained GitHub token with read-only access to the configured
104
+ repositories. Delivery modes should use a dedicated Telegram bot and one
105
+ allowlisted destination chat.
106
+
107
+ ### 2. Try the read-only paths
108
+
109
+ ```sh
110
+ # Stable JSON: no scheduler state, Telegram, or prose provider
111
+ bundle exec prdigest facts --config prdigest.yml
112
+
113
+ # Deterministic Telegram preview: fetches GitHub but does not send or save state
114
+ bundle exec prdigest run --config prdigest.yml --dry-run
115
+
116
+ # Provider-written text on stdout: no Telegram or delivery checkpoint
117
+ bundle exec prdigest prose --config prdigest.yml
20
118
  ```
21
119
 
22
- To build and install the prepared gem without publishing it:
120
+ ### 3. Deliver intentionally
121
+
122
+ ```sh
123
+ # Scheduled deterministic delivery
124
+ bundle exec prdigest run --config prdigest.yml
125
+
126
+ # Provider-written delivery, checkpointed before the first Telegram request
127
+ bundle exec prdigest prose --config prdigest.yml --deliver
128
+ ```
129
+
130
+ To build and install the current checkout as a gem without publishing it:
23
131
 
24
132
  ```sh
25
133
  gem build prdigest.gemspec
26
- gem install prdigest-0.1.1.gem
134
+ gem install prdigest-0.2.0.gem
27
135
  prdigest version
28
136
  ```
29
137
 
30
- ## Configuration and secrets
138
+ The [published RubyGems version](https://rubygems.org/gems/prdigest) may trail
139
+ the current `main` branch.
140
+
141
+ ### Publishing for maintainers
142
+
143
+ RubyGems publication uses GitHub OIDC rather than a stored API key. Configure
144
+ the `prdigest` gem's RubyGems trusted publisher with repository owner
145
+ `ivankuznetsov`, repository `prdigest`, workflow `release.yml`, and environment
146
+ `release`.
147
+
148
+ Pushing a new exact `vX.Y.Z` tag publishes it automatically. To publish an
149
+ existing prepared tag such as `v0.1.1`, run the **Release gem** workflow
150
+ manually and supply that tag. The workflow checks that the immutable tag and
151
+ package version agree, runs the unit and clean-install smoke suites, builds and
152
+ verifies the exact gem, then exchanges GitHub's OIDC identity for a short-lived
153
+ RubyGems credential immediately before `gem push`.
154
+
155
+ ## Configuration
31
156
 
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.
157
+ Lookup is strict: `--config PATH`, then `PRDIGEST_CONFIG`, then an existing
158
+ `/etc/prdigest/config.yml`. PRDigest refuses to run when no configuration is
159
+ found.
35
160
 
36
161
  ```yaml
37
162
  timezone: Europe/London
38
- schedule:
39
- max_catchup_days: 7
163
+
40
164
  github:
41
165
  token_env: GITHUB_TOKEN
42
166
  repos:
43
- - owner/repo-one
44
- - owner/repo-two
45
- telegram:
46
- token_env: TELEGRAM_BOT_TOKEN
47
- chat_id_allowlist: [-1001234567890]
48
- chat_id: -1001234567890
167
+ - owner/api
168
+ - owner/web
169
+
170
+ schedule:
171
+ max_catchup_days: 7
172
+
49
173
  digest:
50
174
  line_stats: true
51
175
  send_empty: true
52
176
  empty_message: "Merged PR digest — {date}\nTotal: 0 PRs"
177
+
178
+ telegram:
179
+ token_env: TELEGRAM_BOT_TOKEN
180
+ chat_id_allowlist: [-1001234567890]
181
+ chat_id: -1001234567890
182
+
53
183
  state:
54
184
  path: /var/lib/prdigest/state.json
55
185
  delivery_path: /var/lib/prdigest/deliveries
186
+
187
+ prose:
188
+ provider: openai_compatible
189
+ base_url: https://openrouter.ai/api/v1
190
+ model: provider/model
191
+ api_key_env: OPENROUTER_API_KEY
56
192
  ```
57
193
 
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.
194
+ See [`configs/config.example.yml`](configs/config.example.yml) for the complete
195
+ annotated configuration.
62
196
 
63
- ## Commands and results
197
+ <details>
198
+ <summary><strong>Configuration rules</strong></summary>
64
199
 
65
- ```sh
200
+ - Repository order controls digest order.
201
+ - `max_catchup_days` must be between `1` and `30`.
202
+ - `chat_id` must appear in the non-empty allowlist. Extra IDs are accepted for
203
+ schema compatibility, but delivery sends only to `chat_id`.
204
+ - Token values belong in environment variables, never YAML.
205
+ - The prose block is validated only for `prdigest prose`; it does not ambiently
206
+ enable provider access for deterministic commands.
207
+ - Remote provider URLs require HTTPS. Plaintext HTTP is accepted only for exact
208
+ loopback hosts.
209
+
210
+ </details>
211
+
212
+ ## Command reference
213
+
214
+ ```text
66
215
  prdigest run [--config PATH] [--date YYYY-MM-DD] [--dry-run] [--json] [--repo OWNER/NAME ...]
216
+ prdigest facts [--config PATH] [--date YYYY-MM-DD] [--repo OWNER/NAME ...]
217
+ prdigest prose [--config PATH] [--date YYYY-MM-DD] [--repo OWNER/NAME ...] [--deliver]
67
218
  prdigest serve
68
219
  prdigest version
69
220
  ```
70
221
 
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
222
+ ### `run`
223
+
224
+ An ordinary run processes owed dates oldest-first and advances state after each
225
+ settled day. `--date` replays exactly one local date without reading or writing
226
+ the schedule cursor. `--dry-run` previews an explicit date or yesterday and
227
+ constructs neither state nor Telegram delivery.
228
+
229
+ `--json` emits a versioned `prdigest-result` document with requested, settled,
230
+ skipped, failed, and remaining dates plus delivery progress. `serve` is a
231
+ compatibility stub; use the supplied systemd timer for scheduling.
232
+
233
+ ### `facts`
234
+
235
+ Fetches one explicit date or yesterday and prints exactly one JSON document. It
236
+ never reads or writes scheduling state, constructs Telegram delivery, or
237
+ contacts the prose provider.
238
+
239
+ A complete successful empty digest looks like:
240
+
241
+ ```json
242
+ {
243
+ "schema": "prdigest-facts",
244
+ "schema_version": 1,
245
+ "status": "success",
246
+ "error": null,
247
+ "digest": {
248
+ "date": "2026-07-25",
249
+ "timezone": "Europe/London",
250
+ "line_stats": true,
251
+ "repository_order": ["owner/api", "owner/web"],
252
+ "repositories": [
253
+ {"name": "owner/api", "pull_requests": []},
254
+ {"name": "owner/web", "pull_requests": []}
255
+ ],
256
+ "totals": {
257
+ "pull_requests": 0,
258
+ "additions": 0,
259
+ "deletions": 0,
260
+ "commits": 0
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ The full digest includes ordered repositories and pull requests, authors, URLs,
267
+ UTC merge times, nullable line and commit statistics, and aggregate totals.
268
+ Disabled statistics remain `null`, not zero. There is no generation timestamp,
269
+ so identical accepted GitHub input produces identical JSON.
270
+
271
+ ### `prose`
272
+
273
+ Sends the same facts document as untrusted data to
274
+ `<prose.base_url>/chat/completions`. The provider is instructed to present those
275
+ facts without adding to or changing them.
276
+
277
+ Without `--deliver`, prose is printed to stdout. With `--deliver`, the final
278
+ escaped and chunked payload is stored under `state.delivery_path/prose` before
279
+ the first Telegram request. Provider output containing terminal control
280
+ characters is rejected before it can reach stdout, a checkpoint, or Telegram.
281
+
282
+ There is no silent fallback to deterministic prose or another provider.
283
+
284
+ ## Delivery guarantees
285
+
286
+ PRDigest treats sending as a durable protocol, not a best-effort loop:
287
+
288
+ 1. Fetch and render the complete day.
289
+ 2. Persist the exact final chunk list.
290
+ 3. Mark a chunk in flight before sending.
291
+ 4. Advance only after Telegram definitely accepts it.
292
+ 5. Resume at the next unsent chunk after a definite failure.
293
+
294
+ A definite Telegram 429/5xx response receives at most three attempts. Transport
295
+ failures are ambiguous because Telegram may have accepted the request before
296
+ the connection failed; PRDigest parks them instead of risking a duplicate.
297
+
298
+ Prose delivery uses its own checkpoint namespace. Once a payload exists, retry
299
+ loads those exact chunks before checking GitHub or provider credentials, so a
300
+ resume never regenerates different prose. A failure before the payload becomes
301
+ durable can incur another provider request on retry.
302
+
303
+ <details>
304
+ <summary><strong>Scheduling, replay, and state details</strong></summary>
100
305
 
101
306
  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.
307
+ DST gaps and repeats. A day settles only after every chunk succeeds, an enabled
308
+ empty message succeeds, or an empty message is intentionally suppressed.
123
309
 
124
310
  State is secret-free JSON version 1:
125
311
 
@@ -127,29 +313,56 @@ State is secret-free JSON version 1:
127
313
  {"version":1,"timezone":"Europe/London","last_digested_date":"2026-07-15"}
128
314
  ```
129
315
 
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.
316
+ Writes use atomic mode-`0600` replacement and directory fsync. Delivery
317
+ directories are mode `0700`; files and locks are mode `0600`. A per-date lock
318
+ prevents concurrent sends for the same checkpoint.
319
+
320
+ Missing state means first run and requests yesterday only. Malformed, future,
321
+ unsupported, unreadable, or timezone-mismatched state fails closed.
322
+
323
+ When backlog exceeds the configured cap, PRDigest durably skips the oldest
324
+ prefix and processes only the newest window. For a timezone migration, stop the
325
+ timer, preserve and move aside the old state, change timezone, explicitly
326
+ replay any required dates, then restart the timer.
327
+
328
+ GitHub does not guarantee search-index freshness. Keep the host timezone aligned
329
+ with the digest timezone and use `--date YYYY-MM-DD` for a delayed merge found
330
+ by a later audit. Explicit replay uses the same delivery ledger: while a
331
+ completed checkpoint for that date exists, delivery is a no-op and newly
332
+ rendered content is ignored. Intentionally archiving the checkpoint makes the
333
+ next replay regenerate and send the complete date, not only the delayed pull
334
+ request.
335
+
336
+ </details>
337
+
338
+ ## OpenClaw
339
+
340
+ The repository includes a ClawHub-ready skill at
341
+ [`openclaw/skills/prdigest/SKILL.md`](openclaw/skills/prdigest/SKILL.md). It:
342
+
343
+ - invokes only `prdigest facts`;
344
+ - validates schema version 1 and the success envelope;
345
+ - treats pull-request fields as untrusted data, never instructions;
346
+ - makes no second GitHub request; and
347
+ - never invokes delivery or configures a prose provider.
348
+
349
+ The skill is source-ready, **not claimed as published**. After a separately
350
+ authorized ClawHub release, the expected command is:
351
+
352
+ ```sh
353
+ clawhub install @ivankuznetsov/prdigest
354
+ ```
137
355
 
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.
356
+ ClawHub installation and Ruby CLI installation are separate trust boundaries.
357
+ See [`openclaw/README.md`](openclaw/README.md) for local use and the
358
+ release-gated publication checklist.
144
359
 
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.
360
+ ## Deployment
149
361
 
150
- ## systemd deployment
362
+ <details open>
363
+ <summary><strong>systemd on Ubuntu</strong></summary>
151
364
 
152
- On Ubuntu, install Ruby, `tzdata`, the gem, and then:
365
+ Install Ruby, `tzdata`, and the gem, then:
153
366
 
154
367
  ```sh
155
368
  sudo useradd --system --home /nonexistent --shell /usr/sbin/nologin prdigest
@@ -158,69 +371,88 @@ sudo install -o root -g prdigest -m 0640 configs/config.example.yml /etc/prdiges
158
371
  sudo install -o root -g root -m 0600 .env.example /etc/prdigest/.env
159
372
  sudo install -o root -g root -m 0644 scripts/systemd/prdigest.service /etc/systemd/system/
160
373
  sudo install -o root -g root -m 0644 scripts/systemd/prdigest.timer /etc/systemd/system/
374
+ sudoedit /etc/prdigest/config.yml
375
+ sudoedit /etc/prdigest/.env
161
376
  sudo systemctl daemon-reload
162
377
  sudo systemctl start prdigest.service
163
378
  sudo systemctl enable --now prdigest.timer
164
379
  ```
165
380
 
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.
381
+ systemd creates `/var/lib/prdigest` as `prdigest:prdigest` mode `0700`.
173
382
 
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.
383
+ ```sh
384
+ systemctl status prdigest.service
385
+ systemctl list-timers prdigest.timer
386
+ journalctl -u prdigest.service
387
+ ```
388
+
389
+ Journal output can contain repository and date context, so restrict journal
390
+ group membership and retention.
177
391
 
178
- ## Container
392
+ </details>
179
393
 
180
- The Alpine image includes `tzdata` and runs as the unprivileged `prdigest` user.
181
- Initialize mounted ownership with the image before normal use:
394
+ <details>
395
+ <summary><strong>Container</strong></summary>
396
+
397
+ The Alpine image includes `tzdata` and runs as the unprivileged `prdigest`
398
+ user. Initialize mounted ownership once before normal use:
182
399
 
183
400
  ```sh
184
- docker build -t prdigest:0.1.1 .
401
+ docker build -t prdigest:local .
185
402
  docker volume create prdigest-state
186
- docker run --rm --user root --entrypoint sh -v prdigest-state:/var/lib/prdigest prdigest:0.1.1 \
403
+ docker run --rm --user root --entrypoint sh \
404
+ -v prdigest-state:/var/lib/prdigest \
405
+ prdigest:local \
187
406
  -c 'chown -R prdigest:prdigest /var/lib/prdigest && chmod 0700 /var/lib/prdigest'
407
+
188
408
  docker run --rm --env-file /etc/prdigest/.env \
189
409
  -v /etc/prdigest/config.yml:/etc/prdigest/config.yml:ro \
190
- -v prdigest-state:/var/lib/prdigest prdigest:0.1.1
410
+ -v prdigest-state:/var/lib/prdigest \
411
+ prdigest:local
191
412
  ```
192
413
 
193
- ## Verification and troubleshooting
414
+ </details>
415
+
416
+ ### Rollback
194
417
 
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.
418
+ Stop the timer, install the prior gem or image, restore its matching config and
419
+ a known-good state backup, then restart. Replay omitted dates explicitly; never
420
+ move a checkpoint forward by hand.
200
421
 
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.
422
+ ## Exit codes and troubleshooting
208
423
 
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.
424
+ | Exit | Meaning | First check |
425
+ |---:|---|---|
426
+ | `0` | Completed or dry-run | |
427
+ | `1` | Unexpected or render failure | Logs and input shape |
428
+ | `2` | CLI/configuration refusal | Config path, YAML, timezone, allowlist, env |
429
+ | `3` | GitHub failure | Token scope, repository access, rate/search limits |
430
+ | `4` | Telegram failure | `error.kind` and delivery checkpoint |
431
+ | `5` | State failure | Path, owner, mode, JSON version/date/timezone |
432
+ | `6` | Failure after durable progress | Settled/skipped dates before retry |
433
+ | `7` | Provider failure or ambiguous outcome | Endpoint, model, key env, retry cost |
213
434
 
214
- See [SECURITY.md](SECURITY.md) for token scope, rotation, and private-data flow.
435
+ Reconcile `telegram_ambiguous`, `telegram_permanent`, and
436
+ `delivery_checkpoint_permanent` before moving a checkpoint. Concurrent
437
+ scheduled runs are unsupported; the systemd oneshot is the normal coordinator.
215
438
 
216
- ## Non-goals
439
+ See [`SECURITY.md`](SECURITY.md) for token scope, rotation, and private-data
440
+ flow.
441
+
442
+ ## Development
443
+
444
+ ```sh
445
+ bundle install
446
+ bundle exec rake test
447
+ test/smoke/gem_install.sh
448
+ test/smoke/docker.sh
449
+ test/smoke/systemd.sh
450
+ ```
217
451
 
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.
452
+ The test suite is fully offline. Live GitHub and Telegram smokes are manual
453
+ release gates; retain only timestamps and pass/fail status, never credentials,
454
+ response bodies, generated prose, or private titles.
223
455
 
224
456
  ## License
225
457
 
226
- MIT
458
+ [MIT](LICENSE) © 2026 Ivan Kuznetsov