Some OMO stuff and a release workflow
Check / check (push) Successful in 1m41s
Check / guardrails (push) Successful in 1m37s
Check / bundle (push) Successful in 1m15s
Image / image (push) Successful in 2m36s

This commit is contained in:
2026-09-01 21:23:13 -04:00
parent 25337bc433
commit ea02a73435
14 changed files with 726 additions and 162 deletions
+44 -23
View File
@@ -6,9 +6,9 @@
"work_id": "news-triage-ba2b546d",
"active_plan": "/home/connor/docs/projects/news/.omo/plans/news-triage.md",
"plan_name": "news-triage",
"status": "active",
"status": "completed",
"started_at": "2026-08-31T22:07:39.562Z",
"updated_at": "2026-08-31T23:08:47.954Z",
"updated_at": "2026-09-01T23:10:33.551Z",
"session_ids": [
"opencode:ses_fa68cd6a7ffey9dPiMrqx8nxlG"
],
@@ -22,6 +22,21 @@
"3": "ses_fa6148485ffeMfOcdTvXaX0NcD",
"4": "ses_fa6142db0ffeonLNswU6Pf5JZ4",
"5": "ses_fa60d8537ffeb0ixaMfK8Gbf4j",
"6": "ses_fa600712fffeT3812Kuh4GbY5X",
"7": "ses_fa5ffd7fdffeHDq2g7gCOeFmOX",
"8": "ses_fa2423a00ffeKtuEIs903E34fD",
"9": "ses_fa239d2f9ffetXGdnoQPJIslFe",
"10": "ses_fa2264a47ffeaUYaHGLcrFiSx7",
"11": "ses_fa214daefffebte5jP4JO25mWV",
"12": "ses_fa20911d8ffe0v6hC7RsQKnnDE",
"13": "ses_fa2003acfffeltYpWlKdEn7GDp",
"14": "ses_fa1ef2bf1ffeFKyIblMEgoTnxa",
"15": "ses_fa1e438ccffetxuuygC4Vpiaqa",
"16": "ses_fa1d8953effeCQ8VaistSCa4yC",
"17": "ses_fa1c4b569ffeY4FUvm3RgKtBAi",
"18": "ses_fa1a1d2c7ffekWd8w2psQ2e7pa",
"20": "ses_fa1353850ffefFoVW4C2ZnlDaI",
"21": "ses_fa126fcffffeYJpjQhdFoxlwVY",
"todo:1": {
"task_key": "todo:1",
"task_label": "1",
@@ -86,30 +101,17 @@
"ended_at": "2026-08-31T23:08:47.954Z",
"elapsed_ms": 1175298
},
"6": "ses_fa600712fffeT3812Kuh4GbY5X",
"7": "ses_fa5ffd7fdffeHDq2g7gCOeFmOX",
"8": "ses_fa2423a00ffeKtuEIs903E34fD",
"9": "ses_fa239d2f9ffetXGdnoQPJIslFe",
"10": "ses_fa2264a47ffeaUYaHGLcrFiSx7",
"11": "ses_fa214daefffebte5jP4JO25mWV",
"12": "ses_fa20911d8ffe0v6hC7RsQKnnDE",
"13": "ses_fa2003acfffeltYpWlKdEn7GDp",
"14": "ses_fa1ef2bf1ffeFKyIblMEgoTnxa",
"15": "ses_fa1e438ccffetxuuygC4Vpiaqa",
"16": "ses_fa1d8953effeCQ8VaistSCa4yC",
"17": "ses_fa1c4b569ffeY4FUvm3RgKtBAi",
"18": "ses_fa1a1d2c7ffekWd8w2psQ2e7pa",
"19a": "ses_fa1734076ffeTsS6Ice8fIrJZs",
"19b": "ses_fa15bdb2fffeH06NS8ddqULaf0",
"20": "ses_fa1353850ffefFoVW4C2ZnlDaI",
"21": "ses_fa126fcffffeYJpjQhdFoxlwVY"
}
"19b": "ses_fa15bdb2fffeH06NS8ddqULaf0"
},
"ended_at": "2026-09-01T23:10:33.551Z",
"elapsed_ms": 90173989
}
},
"active_plan": "/home/connor/docs/projects/news/.omo/plans/news-triage.md",
"started_at": "2026-08-31T22:07:39.562Z",
"status": "active",
"updated_at": "2026-08-31T23:08:47.954Z",
"status": "completed",
"updated_at": "2026-09-01T23:10:33.551Z",
"session_ids": [
"opencode:ses_fa68cd6a7ffey9dPiMrqx8nxlG"
],
@@ -123,6 +125,21 @@
"3": "ses_fa6148485ffeMfOcdTvXaX0NcD",
"4": "ses_fa6142db0ffeonLNswU6Pf5JZ4",
"5": "ses_fa60d8537ffeb0ixaMfK8Gbf4j",
"6": "ses_fa600712fffeT3812Kuh4GbY5X",
"7": "ses_fa5ffd7fdffeHDq2g7gCOeFmOX",
"8": "ses_fa2423a00ffeKtuEIs903E34fD",
"9": "ses_fa239d2f9ffetXGdnoQPJIslFe",
"10": "ses_fa2264a47ffeaUYaHGLcrFiSx7",
"11": "ses_fa214daefffebte5jP4JO25mWV",
"12": "ses_fa20911d8ffe0v6hC7RsQKnnDE",
"13": "ses_fa2003acfffeltYpWlKdEn7GDp",
"14": "ses_fa1ef2bf1ffeFKyIblMEgoTnxa",
"15": "ses_fa1e438ccffetxuuygC4Vpiaqa",
"16": "ses_fa1d8953effeCQ8VaistSCa4yC",
"17": "ses_fa1c4b569ffeY4FUvm3RgKtBAi",
"18": "ses_fa1a1d2c7ffekWd8w2psQ2e7pa",
"20": "ses_fa1353850ffefFoVW4C2ZnlDaI",
"21": "ses_fa126fcffffeYJpjQhdFoxlwVY",
"todo:1": {
"task_key": "todo:1",
"task_label": "1",
@@ -186,7 +203,11 @@
"status": "completed",
"ended_at": "2026-08-31T23:08:47.954Z",
"elapsed_ms": 1175298
}
},
"19a": "ses_fa1734076ffeTsS6Ice8fIrJZs",
"19b": "ses_fa15bdb2fffeH06NS8ddqULaf0"
},
"agent": "atlas"
"agent": "atlas",
"ended_at": "2026-09-01T23:10:33.551Z",
"elapsed_ms": 90173989
}
+27 -15
View File
@@ -1,22 +1,34 @@
F2 final-verification code-quality review: news-triage
Scope: crates/*/src/**/*.rs, Cargo.toml workspace lint table, Dockerfile, .gitea/workflows/ci.yml, deploy/*.
REJECT
APPROVE
Blocking finding:
- crates/news-server/src/scheduler/cycle.rs computes relevance_percentile and importance_percentile from the current poll batch only (lines 141-146), using a local percentile_rank helper (lines 207-215). The implementation plan and draft spec require these percentiles to come from the trailing-7-day PercentileTracker distribution (crates/news-store/src/percentile.rs PercentileTracker::percentile). This is a core-algorithm deviation that changes notification gating decisions, so the F2 verdict is REJECT.
Blocking finding from prior review: RESOLVED
- The prior REJECT was based on crates/news-server/src/scheduler/cycle.rs computing relevance_percentile and importance_percentile from the current poll batch only, using a local percentile_rank helper. That local helper is gone.
Non-blocking findings (correct but noted):
- percentile_rank helper is duplicated in crates/news-server/src/api/stories.rs:268, crates/news-server/src/scheduler/cycle.rs:207, and crates/news-cli/src/replay.rs:232.
- Deterministic source UUID generation uses Uuid::from_u128(hash) in crates/news-server/src/bin_support.rs:162 and crates/news-cli/src/replay.rs:202. Functionally round-trips; non-standard but acceptable for stable fixtures/default sources.
- Stub sweep passed: no todo!/unimplemented! in production code; panic! and unwrap/expect are only in tests.
- Workspace lints inherited by all six crates via [lints] workspace = true.
- cargo clippy --workspace --all-targets -- -D warnings passes.
- Math formulas match the draft spec: importance = 1/(1+prominence_rank) + source_trust_weight + (1 - 1/(1+corroboration_count)); relevance = bm25 + bayes_score (Bayes term omitted until ≥20 samples).
- Deployment units (deploy/news-update, deploy/news-update.service, deploy/news-update.timer, deploy/ntfy-access-snippet.md) are present and consistent with CI/Dockerfile paths.
Verification of the fix:
- crates/news-store/src/percentile.rs:108-137 implements PercentileTracker::percentile_rank(metric_kind, value, window_days). It queries percentile_stats for rows WHERE metric_kind = ?1 AND computed_at >= ?2 over the trailing window, returns StoreError::InsufficientData when the window is empty, and computes rank as (strictly below + half of equal values) / count * 100.
- crates/news-store/src/percentile.rs:232-276 contains unit tests asserting empty-window -> InsufficientData, exact rank semantics (rank of 5 in 1..10 == 45.0), and out-of-window exclusion.
- crates/news-server/src/scheduler/cycle.rs:142-154 now calls tracker.percentile_rank(MetricKind::Relevance, relevance, 7) and tracker.percentile_rank(MetricKind::Importance, importance, 7), mapping errors (including InsufficientData) to 0.0.
- crates/news-server/src/api/stories.rs:269-280 provides percentile_rank_option, which maps InsufficientData to None and is then unwrapped to 0.0 at lines 132-133.
- crates/news-cli/src/replay.rs:114-119 calls tracker.percentile_rank(..., 7) for both relevance and importance.
- No local percentile_rank helpers remain in cycle.rs, stories.rs, or replay.rs.
Required fix:
- In scheduler/cycle.rs, call PercentileTracker::percentile(MetricKind::Relevance, ...) and PercentileTracker::percentile(MetricKind::Importance, ...) for each cluster, then map the returned Option<f64> or StoreError::InsufficientData to the appropriate percentile value, instead of computing batch-local percentiles.
F2 checklist re-execution:
- Stub/suppression grep: `grep -rn "todo!\|unimplemented!\|#\[allow(clippy::unwrap_used)\]\|#\[allow(clippy::expect_used)\]" crates` returned zero matches.
- Workspace lint table (Cargo.toml:17-24) denies dead_code, unused_must_use, unsafe_code, unwrap_used, and expect_used; no crate overrides or suppressions were found.
- cargo clippy --workspace --all-targets -- -D warnings: exit 0.
- cargo test --workspace: all tests pass.
- Gate math matches .omo/drafts/news-triage.md:
- Bypass lane: crates/news-server/src/gate.rs:248-258 checks importance_percentile >= bypass_importance_percentile (default 99.0 at gate.rs:66) OR cluster.source_count >= 2, with an independent bypass_daily_ceiling (default 4 at gate.rs:67).
- Notify threshold: gate.rs:263 uses relevance_percentile < notify_percentile (default 90.0 at gate.rs:64).
- Digest band: gate.rs:266 uses digest_floor_percentile <= relevance_percentile < notify_percentile (defaults 75.0 and 90.0 at gate.rs:65/64).
- Quiet hours: gate.rs:262 uses hour >= 22 || hour < 7 in America/Louisville.
- Normal token bucket is independent of bypass lane (bypass never touches budget.tokens; normal lane decrements it at gate.rs:221).
Next step:
- After the fix is merged, re-run this F2 review to verify the scheduler uses the trailing-7-day distribution and update this evidence file to APPROVE.
Non-blocking findings (not affecting F2 verdict):
- crates/news-store/src/percentile.rs:28-37 contains kind_from_str, currently unused but #[allow(dead_code)]; it is not a stub and does not affect runtime behavior.
- crates/news-server/src/notify/ntfy.rs:49 has #[allow(dead_code)] on NtfyPublisher; the struct is used in production (cycle.rs:84), and clippy is clean.
- Deterministic source UUID generation via Uuid::from_u128(hash) remains in crates/news-cli/src/replay.rs:203-212 and crates/news-server/src/bin_support.rs:162; functionally round-trips and acceptable for stable fixtures.
Final verdict: APPROVE. The percentile-rank fix is correctly landed, the F2 checklist is clean, and the gate math matches the draft spec.
+96 -118
View File
@@ -1,181 +1,159 @@
F3 Final Verification - news-triage
====================================
Reviewer: F3 manual end-to-end QA
Reviewer: F3 manual end-to-end QA (re-run after NaN/null relevance fix)
Date: 2026-09-01
Workspace: /home/connor/docs/projects/news
Scratch environment: /tmp/opencode/news-triage-scratch
Target evidence file: /home/connor/docs/projects/news/.omo/evidence/f3-news-triage.txt
Verdict: REJECT
Verdict: APPROVE
Reason for rejection: A real functional bug was discovered during end-to-end
verification. When a blocklisted source is present in the configuration, the
backend serializes NaN relevance values as JSON null in /api/stories. The
frontend expects `relevance: f64` and fails to deserialize the response,
rendering the entire story list unusable. This breaks the core user journey.
The bug was worked around for testing by removing the blocklisted source from
the scratch configuration, but the underlying defect remains in the codebase
and must be fixed before release.
The previously rejecting defect (blocklist-vetoed stories serializing
"relevance":null and crashing the frontend decode) has been fixed and
verified live.
Environment
-----------
- Container image reused: news-triage:test (built previously; `podman image exists news-triage:test` returned true).
- Host binary rebuilt during F3: `cargo build -p news-server --locked` succeeded.
- Mock ntfy server: http://127.0.0.1:31337 (Python http.server logging POSTs to /news-triage).
- Fixture server: http://127.0.0.1:31338 (Python http.server serving /tmp/opencode/news-triage-scratch/fixtures).
- UI proxy server: http://127.0.0.1:8080 (serves crates/news-web/dist and proxies /api to backend).
- Backend container: http://127.0.0.1:3001 (network_mode: host).
- Database: sqlite::memory: for dry-run tests; container uses mounted news.db.
Test fixtures
-------------
- /tmp/opencode/news-triage-scratch/fixtures/source-a-rss.xml (RSS, recent-dated stories)
- /tmp/opencode/news-triage-scratch/fixtures/source-d-rss.xml (RSS)
- /tmp/opencode/news-triage-scratch/fixtures/source-b-sitemap.xml (news-sitemap)
- source-c-rss.xml (celebrity gossip) was created but removed from active configs
after the relevance/NaN bug was discovered.
- Fresh scratch directory: /tmp/opencode/news-triage-f3-final (removed after run)
- Host binaries rebuilt: `cargo build -p news-server -p news-cli --locked`
- Web dist rebuilt: `trunk build --release` in crates/news-web/
- Backend port: 32100
- Mock ntfy server: http://127.0.0.1:32101 (Python http.server logging POSTs)
- Fixture server: http://127.0.0.1:32102 (python3 -m http.server)
- UI proxy server: http://127.0.0.1:32103 (serves crates/news-web/dist and proxies /api)
- Database: /tmp/opencode/news-triage-f3-final/db/news.db (scratch, seeded)
Configurations used
-------------------
1. /tmp/opencode/news-triage-scratch/config/config.toml
- Enabled sources: source-a, source-b, source-d (source-c removed).
1. /tmp/opencode/news-triage-f3-final/config/config.toml
- Enabled sources: source-a, source-b, source-c, source-d (source-c contains
blocklisted "celebrity"/"gossip" content).
- Default real sources explicitly disabled.
- Used for containerized backend + UI verification.
2. /tmp/opencode/news-triage-scratch/config/config-dryrun.toml
- Default sources (Al Jazeera, BBC, AP) explicitly disabled.
- Only source-a enabled (http://127.0.0.1:31338/source-a-rss.xml).
2. /tmp/opencode/news-triage-f3-final/config/config-dryrun.toml
- Default sources disabled.
- Only source-a enabled (http://127.0.0.1:32102/source-a-rss.xml).
- Used for `news-server --dry-run --once` verification.
Null-relevance fix verification
-------------------------------
Seeded the scratch DB with:
- A blocklisted raw item: "Celebrity gossip roundup for the week" (matches
topics.blocklist = ["celebrity", "gossip"])
- A sibling raw item: "Major trade deal signed at summit"
- Corresponding story_clusters and cluster_members rows.
Backend /api/stories response included the vetoed row with:
"relevance": null
and sibling rows still decoded correctly with finite relevance values.
No frontend decode errors were observed in the browser console.
UI browser verification
-----------------------
- Loaded http://localhost:32103/ in Chromium via Playwright.
- Full story list rendered (11 rows), including the vetoed blocklisted row.
- The vetoed row displayed "—" as the relevance placeholder.
- Screenshot saved: .omo/evidence/ui-f3-final.png
- Clicked the "Interested" feedback button on "Major trade deal signed at summit".
- Browser dev tools confirmed POST /api/feedback returned HTTP 200.
Backend API verification
------------------------
All checks passed against the running container:
All checks passed against the running backend:
$ curl -s http://127.0.0.1:3001/healthz
ok
$ curl -i http://localhost:32100/healthz
HTTP/1.1 200 OK
{"db":"ok","status":"ok"}
$ curl -s http://127.0.0.1:3001/metrics | head -3
# HELP news_uptime_seconds Process uptime
# TYPE news_uptime_seconds gauge
news_uptime_seconds 123
$ curl -i http://localhost:32100/metrics
HTTP/1.1 200 OK
# HELP news_notify_total Total stories the gate decided to notify
...
$ curl -s http://127.0.0.1:3001/api/config
{"sources":[{"name":"Source A","url":"http://127.0.0.1:31338/source-a-rss.xml",...},...]}
$ curl -i http://localhost:32100/api/config
HTTP/1.1 200 OK
{"sources":[{"name":"Source A",...
$ curl -s http://127.0.0.1:3001/api/stories
[{"id":"...","title":"Major trade deal signed at summit",...},...]
$ curl -i http://localhost:32100/api/stories
HTTP/1.1 200 OK
{"stories":[{"cluster_id":"...","title":"Celebrity gossip roundup for the week","relevance":null,...},...]}
$ curl -s -X POST http://127.0.0.1:3001/api/feedback \
$ curl -i -X POST http://localhost:32100/api/feedback \
-H 'Content-Type: application/json' \
-d '{"story_id":"test-123","verdict":"interested"}'
-d '{"story_cluster_id":"<valid-uuid>","kind":"interested"}'
HTTP/1.1 200 OK
{"status":"recorded"}
Ntfy notification verification
------------------------------
Mock ntfy log captured 3 bypass-lane POSTs when stories exceeded the bypass
importance threshold. Each request included:
Mock ntfy log captured 4 bypass-lane POSTs to /news-triage when stories
exceeded the bypass importance threshold. Each request included:
- Title matching the story headline
- Priority: 5
- Tags: newspaper
- Authorization: Basic <token>
Sample captured request (verbatim from mock-ntfy.log):
Sample captured request:
POST /news-triage HTTP/1.1
Host: 127.0.0.1:31337
Authorization: Basic bmV3cy10cmlhZ2U6bXktc2VjcmV0LWFwaS1rZXk=
Content-Type: application/json
Content-Length: ...
{"title":"Major trade deal signed at summit","message":"...","priority":5,"tags":["newspaper"]}
POST /news-triage Title=Election officials certify final results Priority=5 Tags=newspaper Markdown=yes Click=... Actions=... Authorization=Basic bmV3cy10cmlhZ2U6bXktc2VjcmV0LWFwaS1rZXk=
BODY: The final vote count confirms the winner of the national election.
This confirms the notification publisher correctly authenticates and formats
bypass notifications.
UI browser verification
-----------------------
- Story list rendered at least one row.
- Clicked the "Interested" button on a story.
- Browser dev tools confirmed POST /api/feedback returned 200 with
{"status":"recorded"}.
- Screenshot saved: /home/connor/docs/projects/news/.omo/evidence/ui-interested.png
- After stopping the backend container and reloading the page, the UI showed
"Could not load stories" with an HTTP 502 from the proxy.
- Screenshot saved: /home/connor/docs/projects/news/.omo/evidence/ui-error.png
bypass notifications against the mock endpoint; no traffic reached production
ntfy.rcjohnstone.com.
Dry-run verification
--------------------
Command:
$ : > /tmp/opencode/news-triage-scratch/mock-ntfy.log
$ RUST_LOG=info \
NEWS_DATABASE_URL=sqlite::memory: \
NEWS_CONFIG_PATH=/tmp/opencode/news-triage-scratch/config/config-dryrun.toml \
NTFY_URL=http://127.0.0.1:31337 \
$ : > /tmp/opencode/news-triage-f3-final/logs/mock-ntfy-dryrun.log
$ NEWS_DATABASE_URL=sqlite::memory: \
NEWS_CONFIG_PATH=/tmp/opencode/news-triage-f3-final/config/config-dryrun.toml \
NTFY_URL=http://127.0.0.1:32101 \
timeout 30 ./target/debug/news-server --dry-run --once
Result:
- exit=0
- /tmp/opencode/news-triage-scratch/mock-ntfy.log contained 0 entries.
- Only the fixture stories were processed (default real sources were disabled
in config):
- /tmp/opencode/news-triage-f3-final/logs/mock-ntfy-dryrun.log contained 0 entries.
- Only fixture stories were processed:
Major trade deal signed at summit 2.400 1.187 83.333 Digest
Coastal storm forces evacuations 2.400 0.730 50.000 Suppress:BelowThreshold
Major trade deal signed at summit 2.400 1.187 83.333 Digest
Election officials certify final results 2.400 0.689 16.667 Suppress:BelowThreshold
This confirms `--dry-run` does not emit ntfy notifications.
This confirms --dry-run makes no ntfy calls.
Replay determinism verification
-------------------------------
Command:
$ ./target/debug/news-cli replay crates/news-cli/tests/fixtures/sample-week > replay1b.out
$ ./target/debug/news-cli replay crates/news-cli/tests/fixtures/sample-week > replay2b.out
$ cmp replay1b.out replay2b.out && echo identical
$ ./target/debug/news-cli replay crates/news-cli/tests/fixtures/sample-week > replay1.out
$ ./target/debug/news-cli replay crates/news-cli/tests/fixtures/sample-week > replay2.out
$ cmp replay1.out replay2.out && echo identical
identical
Result: byte-identical output across two replay runs.
Result: byte-identical output across two replay runs. The blocklisted fixture
item is shown with REL=-inf and decision Suppress:BelowThreshold, matching the
backend behavior exercised above.
Bug discovered: NaN relevance for blocklisted items
---------------------------------------------------
When source-c (celebrity gossip) was enabled, the backend produced a story
whose title contained blocklist keywords. The relevance scorer returned
`f64::NEG_INFINITY` or `NaN` for the relevance field. The `/api/stories`
endpoint serialized this as JSON `null`.
Frontend error (from browser console):
could not decode response: invalid type: null, expected f64
This comes from:
- Frontend: crates/news-web/src/api.rs defines `StoryRow { relevance: f64 }`.
- Backend: crates/news-server/src/api/stories.rs returns the raw f64 value
without replacing NaN/Infinity.
Impact: any configuration that includes a blocklisted source renders the
entire /api/stories response undecodable by the frontend. The UI shows a
blank or error state and the user cannot interact with stories at all.
Workaround used for remaining tests: removed source-c from the scratch
config. This is NOT a fix; the code still has the defect.
Additional observations
Code-level confirmation
-----------------------
- Default sources are always seeded as enabled by `SourceRepo::seed_default_sources()`
(crates/news-store/src/sources.rs). To run an offline test, the operator must
explicitly disable Al Jazeera, BBC, and AP in config. This is surprising but
documented behavior.
- Initial F3 dry-run attempts with an outdated host binary hung silently at
startup when configured with localhost fixture URLs. Rebuilding with
`cargo build -p news-server --locked` resolved the hang.
- crates/news-server/src/api/stories.rs line 151:
`relevance: relevance.is_finite().then_some(relevance)`
- crates/news-web/src/api.rs line 36:
`pub relevance: Option<f64>`
- crates/news-web/src/api.rs lines 211-231:
unit test `story_row_with_null_relevance_decodes` passes.
Conclusion
----------
The notification path, API surface, container health, and dry-run mode all
function as intended. UI interaction works under the workaround config.
However, the NaN/null relevance bug is a showstopper for the end-to-end user
experience whenever a blocklisted source is present. F3 therefore rejects the
release until the bug is fixed and re-verified.
The null-relevance bug that caused the frontend to blank the entire story list
is fixed end-to-end. All API endpoints respond correctly, the UI renders the
full list including vetoed rows with a placeholder, the feedback flow completes,
ntfy publishes reach the mock server with correct formatting and auth, replay
output is deterministic, and dry-run emits no notifications.
F3 Verdict: REJECT
F3 Verdict: APPROVE
Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

+4 -4
View File
@@ -262,19 +262,19 @@ Your next move: run `$start-work news-triage` to begin execution, or ask for a h
## Final verification wave
> Runs in parallel after ALL todos. ALL must APPROVE. Surface results and wait for the user's explicit okay before declaring complete.
- [ ] F1. Plan compliance audit
- [x] F1. Plan compliance audit
What to verify: every todo 1-21 is checked, every "Must NOT do" clause was honored (grep the diff/repo for violations: no LLM/LiteLLM/llama.cpp references, no edits to `~/compose.yml`/`~/Caddyfile`/`~/data/ntfy/etc/server.yml`/`~/config/prometheus/prometheus.yml`, no article-body scraping code, no backend code serving `dist`), and every Scope-IN item from the plan's `## Scope` section has a corresponding implemented component.
Tool + invocation: `grep -riE "liteLLM|llama.cpp|gpt-oss|gemma3" /home/connor/docs/projects/news/crates` expect zero matches; diff-style check that the 4 protected files' mtimes are unchanged from before implementation started; read `.omo/plans/news-triage.md` and count `- [x]` vs `- [ ]` across todos 1-21.
Verdict: APPROVE only if all 21 todos checked, zero forbidden-string matches, zero protected-file mtime changes.
- [ ] F2. Code quality review
- [x] F2. Code quality review
What to verify: read every file under `crates/*/src/`, `Dockerfile`, `.gitea/workflows/ci.yml`, `deploy/` line by line; confirm no stubs/TODOs/`unimplemented!()`/`todo!()` remain, no `unwrap()`/`expect()` outside test modules (workspace lint should already deny this — confirm the lint is not suppressed anywhere via `#[allow(clippy::unwrap_used)]`), and that the importance/relevance/gate math matches the formulas documented in the draft.
Tool + invocation: `grep -rn "todo!\|unimplemented!\|#\[allow(clippy::unwrap_used)\]\|#\[allow(clippy::expect_used)\]" /home/connor/docs/projects/news/crates` expect zero matches; `cargo clippy --workspace --all-targets -- -D warnings` exit 0.
Verdict: APPROVE only if zero stub/suppression matches and clippy is clean.
- [ ] F3. Real manual QA
- [x] F3. Real manual QA
What to verify: the full stack actually runs end-to-end. Build the image, run it via `podman-compose` against a scratch compose file in `/tmp/opencode`, hit `/healthz`, `/metrics`, `/api/stories`, `/api/config`, `POST /api/feedback`, and load the web UI in a real browser via `/playwright`, exercising the feedback-button flow and confirming a real ntfy publish attempt is made (point `NTFY_*` env at a scratch/test ntfy topic or a mock, not the production `ntfy.rcjohnstone.com` topic).
Tool + invocation: `curl -i http://localhost:3000/healthz`, `curl -i http://localhost:3000/metrics`, `/playwright` browser session against the served UI, `cargo run -p news-cli -- replay <fixtures>` for a deterministic end-to-end check.
Verdict: APPROVE only if every endpoint responds correctly, the UI renders and the feedback flow completes, and the replay harness produces sane, deterministic output.
- [ ] F4. Scope fidelity
- [x] F4. Scope fidelity
What to verify: nothing beyond the plan's Scope IN was added (no extra dependencies not named in any todo, no extra API endpoints, no extra config keys), and nothing in Scope OUT was violated (re-check the 8 Scope-OUT bullets one by one against the actual code).
Tool + invocation: `cargo tree --workspace -e normal` diffed against the dependency names actually referenced by todos 1-21 (flag any unexplained addition); manual read of `Cargo.toml` per crate.
Verdict: APPROVE only if every dependency and endpoint traces back to a specific todo, and every Scope-OUT bullet is confirmed unviolated.
@@ -0,0 +1,10 @@
{
"sessionID": "ses_fa0c5b480ffefuEgw30UYjrGiU",
"updatedAt": "2026-09-01T23:15:40.191Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-09-01T23:15:40.191Z"
}
}
}
@@ -0,0 +1,10 @@
{
"sessionID": "ses_fa0c60672ffe6eRDOgX6y2S7rA",
"updatedAt": "2026-09-01T23:09:03.016Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-09-01T23:09:03.016Z"
}
}
}
@@ -1,10 +1,10 @@
{
"sessionID": "ses_fa68cd6a7ffey9dPiMrqx8nxlG",
"updatedAt": "2026-09-01T22:52:34.671Z",
"updatedAt": "2026-09-02T01:22:03.249Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-09-01T22:52:34.671Z"
"updatedAt": "2026-09-02T01:22:03.249Z"
}
}
}
+11
View File
@@ -21,3 +21,14 @@
{"event":"task-completed","plan":".omo/plans/news-triage.md","task":"19b","session_id":"ses_fa15bdb2fffeH06NS8ddqULaf0","commands":["cd crates/news-web && trunk build --release","cargo fmt --all --check","cargo clippy --workspace --all-targets -- -D warnings","cargo test --workspace --no-fail-fast","grep for absolute API URLs in news-web/src (none found)"],"artifact":".omo/evidence/task-19-news-triage.txt + task-19-news-triage.png + task-19-news-triage-failure.png","adversarial_classes":{"probed":["misleading_success_output: independently re-ran trunk build (exit 0, 537081-byte wasm, under 1.8MB budget), fmt, clippy, and the full workspace suite (87 green); verified evidence screenshots exist on disk and no absolute URLs in frontend source","runnable_surface: playwright QA evidence covers story render, feedback 200 round-trip, and backend-down error state"],"not_applicable":["malformed_input: UI consumes only typed JSON from our own endpoints"]},"cleanup":"none required","note":"root package.json added mirroring runway layout (Tailwind v4 + playwright dev deps)"}
{"event":"task-completed","plan":".omo/plans/news-triage.md","task":"20","session_id":"ses_fa1353850ffefFoVW4C2ZnlDaI","commands":["podman run --rm news-triage:test news-server --version","podman run --rm news-triage:test ls /srv/dist","cargo fmt --all --check","cargo clippy --workspace --all-targets -- -D warnings","cargo test --workspace --no-fail-fast"],"artifact":".omo/evidence/task-20-news-triage.txt","adversarial_classes":{"probed":["misleading_success_output: independently re-ran podman smoke (news-server 0.1.0, exit 0), container ls /srv/dist (index.html+wasm+js+css), fmt, clippy, full workspace suite (89 green, +2 NEWS_DATABASE_URL parsing tests)","side_effects: runtime stage purity mirrored from runway (only ca-certificates+tzdata; no Node/npm/Trunk/Rust in final image)"],"not_applicable":["malformed_input: NEWS_DATABASE_URL parsing covered by unit tests incl. bare-path passthrough"]},"cleanup":"temp 2MB wasm inflation fully reverted; wasm back to 537081 bytes","followup":"NEWS_DATABASE_URL is now the primary env source (sqlite:///abs/path form), NEWS_DB_PATH fallback kept for dev"}
{"event":"task-completed","plan":".omo/plans/news-triage.md","task":"21","session_id":"ses_fa126fcffffeYJpjQhdFoxlwVY","commands":["test -f deploy/README.md + greps (ntfy access/internal/prometheus.yml)","podman-compose -f /tmp/opencode/compose.yml config","stat protected files before/after"],"artifact":".omo/evidence/task-21-news-triage.txt","adversarial_classes":{"probed":["misleading_success_output: independently re-ran acceptance greps (PASS) and podman-compose config on scratch copy (exit 0); protected-file mtimes all predate work window","side_effects: caddy validate required sudo only for /var/log/caddy perms, not syntax; subagent used sudo validate + non-root adapt as parse proof"],"not_applicable":["malformed_input: docs-only todo"]},"cleanup":"scratch copies remain in /tmp/opencode (ephemeral dir)","followup":"README has 8 setup steps vs runway's 6 (no signing key; adds config creation, ntfy ACL, prometheus); config example referenced from config.rs since no standalone example ships"}
{"event": "review-verdict", "plan": ".omo/plans/news-triage.md", "task": "F1", "session_id": "ses_fa11f377effeEvI7EW84KPdlnq", "verdict": "APPROVE", "artifact": ".omo/evidence/f1-news-triage.txt", "note": "plan compliance audit; verdict stands per plan rule (subsequent fixes did not touch the audited surface)", "adversarial_classes": {"probed": ["misleading_success_output: verdict evidence read and cross-checked by orchestrator before acceptance"]}, "cleanup": "none"}
{"event": "review-verdict", "plan": ".omo/plans/news-triage.md", "task": "F2", "session_id": "ses_fa11ed7fbffeaJBe8ZaZpPm7Yb", "verdict": "REJECT", "artifact": ".omo/evidence/f2-news-triage.txt", "note": "batch-local percentile_rank in cycle.rs ranked against current poll batch only; required fix: PercentileTracker::percentile_rank (trailing 7d) used by cycle/stories/replay; reviewer timed out after delivering partial evidence containing full verdict", "adversarial_classes": {"probed": ["misleading_success_output: orchestrator independently read cycle.rs:141-146/207-215 confirming the batch-local helper before accepting verdict"]}, "cleanup": "none"}
{"event": "review-verdict", "plan": ".omo/plans/news-triage.md", "task": "F3", "session_id": "ses_fa11e5f84ffe9yfwJq2szYJcYj", "verdict": "REJECT", "artifact": ".omo/evidence/f3-news-triage.txt", "note": "blocklist veto NEG_INFINITY serialized as JSON null; news-web relevance:f64 decode failure blanked entire story list; all other QA passed (endpoints, mock-ntfy publish proof, feedback 200, replay determinism, dry-run isolation)", "adversarial_classes": {"probed": ["runnable_surface: reviewer exercised real server + browser + mock ntfy, not just code reads"]}, "cleanup": "none"}
{"event": "review-verdict", "plan": ".omo/plans/news-triage.md", "task": "F4", "session_id": "ses_fa11e06ebffejux5OpskmbyLUF", "verdict": "APPROVE", "artifact": ".omo/evidence/f4-news-triage.txt", "note": "scope fidelity; verdict stands", "adversarial_classes": {"probed": ["misleading_success_output: evidence read and cross-checked by orchestrator"]}, "cleanup": "none"}
{"event": "task-completed", "plan": ".omo/plans/news-triage.md", "task": "fix-f2f3", "session_id": "ses_fa0f22fcaffelGhy6E2TAbA6IS", "commands": ["grep percentile_rank in cycle.rs/percentile.rs/stories.rs/replay.rs", "grep Option<f64> in stories.rs + news-web/api.rs", "git log + git show 676a0b7 --stat", "cargo clippy --workspace --all-targets -- -D warnings", "cargo test --workspace --no-fail-fast"], "artifact": ".omo/evidence/fix-f2f3-news-triage.txt", "note": "fixes verified in tree: trailing-7d percentile_rank in news-store used by cycle/stories/replay (cold start 0.0); relevance Option<f64> at API boundary via is_finite().then_some(); frontend Option<f64> + decode regression test; 97/97 orchestrator-verified. Subagent's 'already present in initial commit' claim was false - fixes were committed mid-run by user auto-commit (676a0b7 'Updating', 4242373); orchestrator confirmed actual code state directly.", "adversarial_classes": {"probed": ["misleading_success_output: rejected subagent self-report, verified via git history + direct greps + independent test run"], "not_applicable": ["malformed_input"]}, "cleanup": "none"}
{"event": "task-completed", "plan": ".omo/plans/news-triage.md", "task": "ci-fix", "session_id": "orchestrator-direct", "commands": ["cargo machete", "cargo deny check", "cargo clippy --workspace --all-targets -- -D warnings", "cargo test --workspace --no-fail-fast"], "artifact": "deny.toml + crates/{news-core,news-ingest,news-store,news-cli}/Cargo.toml + workspace Cargo.toml", "note": "removed 6 unused deps flagged by machete (thiserror/news-core, serde/news-ingest, serde/news-store, chrono-tz+sqlx+thiserror/news-cli); prometheus 0.13->0.14 fixes RUSTSEC-2024-0437 (protobuf); deny.toml allow-list +CC0-1.0/CDLA-Permissive-2.0/BSL-1.0 (non-substitutable transitive deps: base16/leptos, webpki-roots/reqwest, xxhash-rust/leptos); advisories ignore RUSTSEC-2024-0436 paste + RUSTSEC-2026-0173 proc-macro-error2 (leptos-pinned compile-time proc-macros, no upgrade path). Verified: machete clean, deny all-ok exit 0, clippy exit 0, 97/97 tests. Uncommitted - user commits.", "adversarial_classes": {"probed": ["misleading_success_output: full test suite green proves removed deps genuinely unused; deny re-run after edits shows all-ok not just locally cached pass"]}, "cleanup": "none"}
{"event": "plan-completed", "plan": ".omo/plans/news-triage.md", "note": "user explicitly authorized marking complete while F2/F3 re-run reviewers were still in flight (F2 bg_4cba0920/ses_fa0c60672ffe6eRDOgX6y2S7rA, F3 bg_cefbb615/ses_fa0c5b480ffefuEgw30UYjrGiU); F2/F3 checkboxes marked per that instruction and final verdicts will be appended when reviewers land. State at completion: F1/F4 APPROVE; both REJECT fixes verified in tree by orchestrator; 97/97 tests, clippy exit 0, cargo machete clean, cargo deny all-ok; CI fixes (unused deps, prometheus 0.14, deny policy) uncommitted - user commits.", "sessions": {"F1": "ses_fa11f377effeEvI7EW84KPdlnq", "F2": "ses_fa11ed7fbffeaJBe8ZaZpPm7Yb", "F3": "ses_fa11e5f84ffe9yfwJq2szYJcYj", "F4": "ses_fa11e06ebffejux5OpskmbyLUF", "fix": "ses_fa0f22fcaffelGhy6E2TAbA6IS"}, "adversarial_classes": {"probed": ["misleading_success_output: every subagent claim independently re-verified by orchestrator across all 21 todos + fix + CI (gates re-run, live surfaces exercised, git history inspected)"]}, "cleanup": "F2/F3 re-run verdicts pending append on arrival"}
{"event": "review-verdict", "plan": ".omo/plans/news-triage.md", "task": "F2-rerun", "session_id": "ses_fa0c60672ffe6eRDOgX6y2S7rA", "verdict": "APPROVE", "artifact": ".omo/evidence/f2-news-triage.txt", "note": "percentile fix verified: PercentileTracker::percentile_rank (percentile.rs:108-137, trailing window, InsufficientData on empty) called from cycle.rs:142-154, stories.rs:269-280, replay.rs:114-119; no local helpers remain; stub/suppression grep zero matches; clippy exit 0; tests pass; gate math matches draft (bypass p99 OR source_count>=2, notify p90, digest [75,90), quiet 22-07)", "adversarial_classes": {"probed": ["misleading_success_output: reviewer independently re-ran grep/clippy/tests and read fix sites line-by-line"]}, "cleanup": "none"}
{"event": "review-verdict", "plan": ".omo/plans/news-triage.md", "task": "F3-rerun", "session_id": "ses_fa0c5b480ffefuEgw30UYjrGiU", "verdict": "APPROVE", "artifact": ".omo/evidence/f3-news-triage.txt + ui-f3-final.png", "note": "null-relevance fix verified LIVE: seeded blocklisted item returns relevance:null while siblings decode; UI renders full list with em-dash placeholder, no console errors; feedback POST 200; all endpoints correct; mock ntfy captured 4 bypass POSTs (Priority 5, newspaper tag, Basic auth), zero production traffic; replay byte-identical x2; dry-run made no ntfy call", "adversarial_classes": {"probed": ["runnable_surface: real server + real browser + mock ntfy + CLI replay, not code reads"]}, "cleanup": "reviewer killed all spawned processes and removed scratch dir /tmp/opencode/news-triage-f3-final"}
{"event": "plan-verified", "plan": ".omo/plans/news-triage.md", "note": "all four final-wave reviewers now APPROVE (F1 compliance, F2 code quality, F3 live QA, F4 scope fidelity); the completion the user authorized while re-runs were in flight is retroactively fully verified. Remaining handoff: CI fixes (4 crate manifests, workspace Cargo.toml, Cargo.lock, deny.toml) uncommitted - user commits."}
{"event": "task-completed", "plan": ".omo/plans/news-triage.md", "task": "ci-image", "session_id": "orchestrator-direct", "commands": ["read runway/.gitea/workflows/release.yml (mirror reference)", "python yaml.safe_load .gitea/workflows/release.yml", "grep NEWS_IMAGE deploy/news-update"], "artifact": ".gitea/workflows/release.yml", "note": "user noticed CI never builds an image; deploy/news-update pulls git.rcjohnstone.com/connor/news:latest ('the image is whatever CI pushed') but no workflow built or pushed it - broken chain. Added release.yml mirroring runway's exactly: on push to main, buildx + login (vars.REGISTRY/vars.USERNAME/secrets.DOCKER_PASSWORD) + build-push ./Dockerfile, tags latest+github.sha, gha cache. Requires REGISTRY/USERNAME vars and DOCKER_PASSWORD secret to exist for the news repo (inherited if org-scoped like runway's). Uncommitted - user commits.", "adversarial_classes": {"probed": ["misleading_success_output: yaml parsed, trigger/steps/tags asserted, Dockerfile context path verified present, tag matched against news-update default IMAGE"], "not_applicable": ["runnable_surface: workflow runs only on push to main; local podman build already proven in todo 20 verification"]}, "cleanup": "none"}