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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +374 -142
- data/SECURITY.md +36 -1
- data/configs/config.example.yml +9 -0
- data/lib/prdigest/cli.rb +169 -5
- data/lib/prdigest/collector.rb +38 -0
- data/lib/prdigest/config.rb +97 -5
- data/lib/prdigest/delivery_checkpoint_store.rb +17 -4
- data/lib/prdigest/facts.rb +76 -0
- data/lib/prdigest/facts_runner.rb +32 -0
- data/lib/prdigest/openai_compatible.rb +172 -0
- data/lib/prdigest/prose_renderer.rb +36 -0
- data/lib/prdigest/prose_runner.rb +102 -0
- data/lib/prdigest/result.rb +2 -1
- data/lib/prdigest/runner.rb +7 -12
- data/lib/prdigest/telegram.rb +3 -2
- data/lib/prdigest/version.rb +1 -1
- data/lib/prdigest.rb +14 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 409ae44cab5ad005c5dcb413632295fdd3aae2e8d6383852b71863f8fc484d15
|
|
4
|
+
data.tar.gz: 7ae70339af9e94123d83795362bfde4c07ee57d94bf447123fe4ce305f9117b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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&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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
66
|
+
All modes accept `--date YYYY-MM-DD` and repeatable
|
|
67
|
+
`--repo OWNER/NAME` overrides. Repository order is always preserved.
|
|
9
68
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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.
|
|
134
|
+
gem install prdigest-0.2.0.gem
|
|
27
135
|
prdigest version
|
|
28
136
|
```
|
|
29
137
|
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
max_catchup_days: 7
|
|
163
|
+
|
|
40
164
|
github:
|
|
41
165
|
token_env: GITHUB_TOKEN
|
|
42
166
|
repos:
|
|
43
|
-
- owner/
|
|
44
|
-
- owner/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
197
|
+
<details>
|
|
198
|
+
<summary><strong>Configuration rules</strong></summary>
|
|
64
199
|
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
103
|
-
|
|
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
|
-
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Missing state means first run and requests yesterday only. Malformed,
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
+
<details open>
|
|
363
|
+
<summary><strong>systemd on Ubuntu</strong></summary>
|
|
151
364
|
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
392
|
+
</details>
|
|
179
393
|
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
401
|
+
docker build -t prdigest:local .
|
|
185
402
|
docker volume create prdigest-state
|
|
186
|
-
docker run --rm --user root --entrypoint sh
|
|
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
|
|
410
|
+
-v prdigest-state:/var/lib/prdigest \
|
|
411
|
+
prdigest:local
|
|
191
412
|
```
|
|
192
413
|
|
|
193
|
-
|
|
414
|
+
</details>
|
|
415
|
+
|
|
416
|
+
### Rollback
|
|
194
417
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|