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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +331 -149
- data/SECURITY.md +35 -1
- data/configs/config.example.yml +11 -11
- data/lib/prdigest/cli.rb +176 -62
- data/lib/prdigest/collector.rb +38 -0
- data/lib/prdigest/config.rb +96 -29
- 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 +33 -0
- data/lib/prdigest/prose_runner.rb +102 -0
- data/lib/prdigest/telegram.rb +3 -3
- data/lib/prdigest/version.rb +1 -1
- data/lib/prdigest.rb +14 -5
- data/scripts/systemd/prdigest.service +1 -1
- metadata +10 -9
- data/lib/prdigest/renderer.rb +0 -134
- data/lib/prdigest/result.rb +0 -82
- data/lib/prdigest/runner.rb +0 -173
- data/lib/prdigest/schedule.rb +0 -30
- data/lib/prdigest/state.rb +0 -147
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ff60040fe7091056de1781ae7154e758145cf7216ec1b664adc661cd479c96f
|
|
4
|
+
data.tar.gz: aa87baccb18ac37b7398c83e5d312187e3d697fa8fa9e161c6f3b930cef73ae4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
PRDigest
|
|
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
|
-
|
|
5
|
+
<p><strong>One merged-PR facts contract. Prose for people and JSON for agents.</strong></p>
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
13
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
123
|
+
gem install prdigest-0.3.0.gem
|
|
27
124
|
prdigest version
|
|
28
125
|
```
|
|
29
126
|
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
max_catchup_days: 7
|
|
152
|
+
|
|
40
153
|
github:
|
|
41
154
|
token_env: GITHUB_TOKEN
|
|
42
155
|
repos:
|
|
43
|
-
- owner/
|
|
44
|
-
- owner/
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
180
|
+
<details>
|
|
181
|
+
<summary><strong>Configuration rules</strong></summary>
|
|
64
182
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
the
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
287
|
+
</details>
|
|
151
288
|
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
345
|
+
<details>
|
|
346
|
+
<summary><strong>Container</strong></summary>
|
|
179
347
|
|
|
180
|
-
The Alpine image includes `tzdata` and runs as the unprivileged `prdigest`
|
|
181
|
-
Initialize mounted ownership
|
|
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:
|
|
352
|
+
docker build -t prdigest:local .
|
|
185
353
|
docker volume create prdigest-state
|
|
186
|
-
docker run --rm --user root --entrypoint sh
|
|
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
|
|
361
|
+
-v prdigest-state:/var/lib/prdigest \
|
|
362
|
+
prdigest:local
|
|
191
363
|
```
|
|
192
364
|
|
|
193
|
-
|
|
365
|
+
</details>
|
|
366
|
+
|
|
367
|
+
### Rollback
|
|
194
368
|
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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.
|