Cloudflare migration - #45
Conversation
…lare Computer - Complete architecture for POC implementation - Step-by-step implementation plan (5 phases, 10 days) - Code templates for all components - Testing and performance measurement strategy - Success criteria and next steps Closes: Migration planning for Cloudflare Computer Co-authored-by: AndreiDrang <AndreiDrang@users.noreply.github.com>
- Minimal setup instructions - Step-by-step deployment guide - Troubleshooting section - Performance measurement tips Related to Cloudflare Computer migration Co-authored-by: AndreiDrang <AndreiDrang@users.noreply.github.com>
- Add poc/ directory with full Cloudflare Worker implementation - Add src/index.js - Main Worker with webhook handling - Add src/lib/github.js - GitHub API client - Add src/lib/commands.js - Command parsing utilities - Add src/lib/handlers/help.js - Help command handler - Add src/lib/logging.js - Logging utilities - Add src/config/constants.js - Application constants - Add tests/test.js - Unit tests for POC - Add wrangler.toml - Cloudflare configuration - Add package.json - Project dependencies - Add README.md - Complete POC documentation POC implements only /zai help command to validate Cloudflare Computer architecture. Ready for deployment and performance testing. Related to Cloudflare Computer migration plan Co-authored-by: AndreiDrang <AndreiDrang@users.noreply.github.com>
- Complete summary of POC implementation - Performance expectations and metrics - Success criteria checklist - Next steps for full migration - All documentation ready for deployment Final commit for cloudflare-migration branch setup Co-authored-by: AndreiDrang <AndreiDrang@users.noreply.github.com>
Why:
* split the flat single-worker poc into a hybrid topology so heavy
commands (review, impact) run in their own worker lifetime, decoupled
from the ~10s GitHub webhook response limit
* add a shared library layer so both workers reuse one set of
command/auth/crypto/github/logging modules
What:
* replace poc/src (flat) with poc/workers/{shared,zai-main-worker,zai-heavy-worker}
* main worker: webhook sig check -> parse -> auth -> route; runs light
commands (help, describe) inline and delegates heavy commands to the
heavy worker via a service binding (env.HEAVY_WORKER) under ctx.waitUntil
* heavy worker: internal-token gate -> 202 ack -> ctx.waitUntil dispatch
* shared/crypto.js: Web Crypto HMAC-SHA256 (no nodejs_compat flag needed)
* heavy worker sets workers_dev=false (binding-only, no public ingress);
main worker gets a public route (zai-worker.tokenbel.info)
* migrate + expand tests to workers/tests/test.js (36 assertions)
Changes:
* poc/src/**, poc/wrangler.toml, poc/tests/test.js: removed (flat layout)
* poc/workers/shared/*.js: shared lib (6 modules)
* poc/workers/zai-main-worker/**: main worker (8 files)
* poc/workers/zai-heavy-worker/**: heavy worker (6 files)
* poc/workers/tests/test.js: migrated test suite
* poc/README.md: rewritten as hybrid architecture doc
* poc/package.json + package-lock.json: hybrid worker scripts + lockfile
Stats:
* 32 files changed, +1659/-1564 lines
Co-authored-by: pi <noreply@earendil-works.com>
Why: * give the hybrid workers a single entry point for deploy, dry-run, test and formatting so contributors don't memorize per-worker wrangler flags * enforce a consistent JS style via Prettier What: * Makefile: targets for dependencies, test, refactor-js (install + format), format-check, deploy-dry-run (heavy first, then main so the service binding resolves), deploy, per-worker dev/tail, and clean * .prettierrc.json: style matching the existing worker source * .prettierignore: exclude node_modules/.wrangler/dist * note: the format:js scripts + prettier devDep live in poc/package.json (committed with the restructure); this Makefile wires them up Changes: * poc/Makefile: added (tokenbel-wiki conventions: /bin/sh, .PHONY, tab recipes) * poc/.prettierrc.json: added * poc/.prettierignore: added Stats: * 3 files changed, +76 lines Co-authored-by: pi <noreply@earendil-works.com>
Why: * per-worker `wrangler secret put` secrets don't scale across two workers and can't be shared; one Cloudflare Secrets Store centralizes all four values What: * bind GITHUB_WEBHOOK_SECRET, GITHUB_TOKEN, ZAI_INTERNAL_TOKEN, ZAI_API_KEY in zai-main-worker (4) and zai-heavy-worker (3, no webhook secret) from store 629e5dd6594845a889e6ddabb26cc009 via [[secrets_store_secrets]] * store bindings expose plain env.* strings, so delegator/crypto/handlers keep reading env.GITHUB_TOKEN etc. unchanged — zero code changes * add per-worker .dev.vars.example for `wrangler dev`; gitignore the real .dev.vars plus node_modules, .wrangler, dist * document the binding table, deploy webhook URL, and roadmap in the README Changes: * workers/zai-main-worker/wrangler.toml: 4 secrets_store_secrets bindings * workers/zai-heavy-worker/wrangler.toml: 3 secrets_store_secrets bindings * workers/zai-main-worker/.dev.vars.example: local-dev dotenv template (4 keys) * workers/zai-heavy-worker/.dev.vars.example: local-dev dotenv template (3 keys) * .gitignore: ignore .dev.vars, node_modules/, .wrangler/, dist/ * README.md: secrets-store table, deploy webhook URL, roadmap note Stats: * 6 files changed * +114/-37 lines Co-authored-by: pi <noreply@earendil-works.com>
Why:
* Secrets Store bindings can arrive as string | {get()} | Promise at
runtime; passing them straight to crypto/encode stringified to
"[object Object]" and broke every webhook signature (401).
* GitHubClient parsed 204 No Content (collaborator check) as JSON,
throwing "Unexpected end of JSON input" (500) on the success path.
What:
* Add resolveSecretValue() to normalize any binding shape to a trimmed
string; resolve at every read site (webhook secret, GitHub token,
internal token).
* GitHubClient.request() short-circuits empty bodies (204) to null and
wraps JSON.parse to preserve the error.status contract.
* Tighten the heavy-worker internal-token gate to reject missing bindings.
Changes:
* shared/secrets.js: new resolveSecretValue() helper
* shared/auth.js: resolve GITHUB_TOKEN before constructing GitHubClient
* shared/github.js: empty-body + parse-error handling in request()
* zai-main-worker/src/index.js: resolve webhook secret + GitHub token
* zai-main-worker/src/delegator.js: resolve internal token inside send()
* zai-heavy-worker/src/index.js: resolve token gate + GitHub token
* README.md: document the binding-type ambiguity and resolution pattern
* tests/test.js: resolveSecretValue + 204 empty-body regression coverage
Stats:
* 8 files changed, +215/-17
Co-authored-by: pi <noreply@earendil-works.com>
Why: * Retire the @zai-bot mention and /zai-bot slash forms; "/zai" is now the single invocation prefix. * Credit the author in the help footer. What: * COMMAND_REGEX accepts only "/zai"; drop MENTION_REGEX and its parse branch (capture groups shifted to type=match[1], args=match[2]). * formatHelp(): remove @zai-bot usage notes; add an AndreiDrang credit linking to https://github.com/AndreiDrang. Changes: * shared/commands.js: /zai-only regex, mention branch removed, footer * tests/test.js: /zai-bot + @zai-bot now rejected; footer link asserted Stats: * 2 files changed, +13/-21 Co-authored-by: pi <noreply@earendil-works.com>
Why: * the bot is migrating from GitHub Actions to Cloudflare Workers (poc/workers) * the Action-based deployment workflow (uses: ./) is now obsolete What: * delete the legacy zai-code-bot.yml workflow (PR / issue_comment / pull_request_review_comment triggers are now handled by the Cloudflare Worker webhook) Changes: * .github/workflows/zai-code-bot.yml: removed Stats: * 1 file changed, -33 lines Co-authored-by: pi <noreply@earendil-works.com>
Why: * port the Vitest + v8-coverage stack from tbel/cf_workers (tb-bcse-securities-collector) for Workers-runtime-fidelity testing * the POC previously used a hand-rolled assert runner with no coverage gating What: * add vitest@^2.1.8, @vitest/coverage-v8@^2.1.8, vitest-environment-miniflare@^2.14.4 * wire npm test -> vitest run --coverage and add test:watch * add vitest.config.js (globals, miniflare env via inline environmentOptions, 80% thresholds on workers/shared + main-worker router) * add make test / make test-watch targets * ignore coverage/, .env, *.log, .DS_Store Changes: * package.json: devDeps + test/test:watch scripts * package-lock.json: lockfile for new deps * vitest.config.js: new config * Makefile: test/test-watch targets + help * .gitignore: coverage/, .env, *.log, .DS_Store Stats: * 5 files changed, +3019/-7 lines Co-authored-by: pi <noreply@earendil-works.com>
Why: * replace the hand-rolled assert runner with a real Vitest suite mirroring tb-bcse-securities-collector (one .test.js per shared module) * document the new test stack in the POC README What: * add 7 Vitest spec files using describe/it/expect + vi.spyOn(globalThis,'fetch') against the miniflare environment: auth, commands, crypto, github, logging, router, secrets * delete the old monolithic workers/tests/test.js runner * document the Vitest + v8 coverage stack in README (layout, dev, testing) Changes: * workers/tests/*.test.js (7 new): 99 tests, 100% stmts/lines/funcs, 97.9% branches * workers/tests/test.js: removed (replaced by the Vitest suite) * README.md: document the test stack Stats: * 9 files changed, +633/-261 lines Co-authored-by: pi <noreply@earendil-works.com>
Why: * the cloudflare migration is now implemented under poc/workers/; these planning artifacts (architecture sketches, quick-starts, starter code, reports) are obsolete and risk misleading future readers What: * delete the entire plans/ directory (10 files) Stats: * 10 files deleted * 6280 deletions Co-Authored-By: Claude <noreply@anthropic.com>
Why:
* GitHub webhooks time out in ~10s, but PR analysis must run longer. PR
events are now recorded durably in D1 and processed asynchronously via a
Queue, so the main worker acks fast and the heavy worker runs to completion
What:
* D1 storage adapters: deliveries, jobs (lease-based claims), artifacts,
config, keys, database helpers — D1 is the single source of truth
* migrations 0001 (schema) + 0002 (job leases, publication state machine)
* queue producer in main worker (job-enqueuer) + consumer in heavy worker
(queue.js) with a 3-attempt retry budget; terminal failures ack without a DLQ
* PR preview handler: bounded stats fetch, R2 manifest + result artifacts,
one live bot-owned comment via D1 publication lease, head-sha freshness check
* 5-min cron on main worker: recover expired leases, replay outbox, sweep
expired storage (30-day retention)
* shared comments/pr-preview/pr-stats helpers + github.js getPullRequest/getPrFiles
* wrangler bindings: BOT_DB, BOT_ARTIFACTS (bot-storage), BOT_CACHE (bot-cache),
BOT_JOBS (bot-jobs), R2_RETENTION_DAYS=30
Changes:
* shared/storage/{database,keys,artifacts,config,deliveries,jobs}.js: new adapters
* shared/{comments,pr-preview,pr-stats}.js: comment publication + rendering + stats
* shared/{constants,github}.js: markers/event types + PR REST methods
* main/src/{job-enqueuer,pr-events}.js: queue publisher + recovery, event extraction
* main/src/index.js: pull_request durable path + scheduled() cron sweep
* main/wrangler.toml: BOT_DB/ARTIFACTS/CACHE/JOBS bindings + cron trigger
* main/migrations/000{1,2}_*.sql: schema + hardening
* heavy/src/queue.js: claim -> run -> ack/retry/fail state machine
* heavy/handlers/pr-preview.js: durable preview job handler
* heavy/src/index.js + handlers/index.js + wrangler.toml: queue consumer wiring
* tests/{storage,storage-state,storage-runtime,github-storage,queue}.test.js
Stats:
* 27 files changed
* +2286/-12 lines
Co-Authored-By: Claude <noreply@anthropic.com>
Why: * the readme described the pre-storage v0.2 layout and referenced now-removed planning docs; it did not document the queue, cron, or durable PR-preview flow What: * rewrite as v0.3: durable PR-preview path as the primary flow (mermaid) * document the job lifecycle state machine and 3-attempt retry budget * document the 5-min cron self-healing sweep (lease/outbox/retention) * actualize the file tree, service bindings, and handler status table * remove all plan-file references Stats: * 1 file changed * +279/-160 lines Co-Authored-By: Claude <noreply@anthropic.com>
Why:
* ask/explain/describe make Z.ai LLM calls, so they cannot run inline in the
main worker within GitHub's ~10s webhook window — only help is truly light
(pure formatting, no API call). The old split would block the webhook once
real handlers landed
What:
* constants.js: LIGHT_COMMANDS=[help], HEAVY_COMMANDS=[ask,explain,describe,
review,impact]
* move describe out of the main (light) handler registry; only help remains
* add ask/explain/describe heavy stub handlers (payload interface) in the
heavy worker, matching the existing review/impact stub pattern
* mark ask/explain/describe as *(heavy)* in the help text
* update README file tree, routing examples, command-path sections, status
Changes:
* shared/constants.js: split arrays — help is the only light command
* shared/commands.js: help text heavy annotations
* zai-main-worker/src/handlers/index.js: drop describe case (help only)
* zai-main-worker/src/handlers/describe.js: deleted (orphaned light handler)
* zai-heavy-worker/src/handlers/{ask,explain,describe}.js: new heavy stubs
* zai-heavy-worker/src/handlers/index.js: register the 3 new heavy handlers
* README.md: reflect the corrected classification (v0.3.1)
Stats:
* 9 files changed
* +183/-72 lines
Co-Authored-By: Claude <noreply@anthropic.com>
Why:
* The poc/ workers implement a durable, multi-resource architecture (D1, R2,
Queue, KV, two-worker split) whose business rules and contracts were only
implicit in source code and the README. An OKF bundle makes the domain
knowledge explicit, navigable, and maintainable.
What:
* Create a 22-file OKF bundle at repo root (okf/): root index (okf_version 0.1),
update log, 6 directory indexes, and 14 concept documents across architecture,
workflows, state, rules, contracts, and datasets.
* Wire the bundle into AGENTS.md via the canonical managed OKF guidance block.
Concepts documented:
* architecture: two-worker-split, storage-authority-model
* workflows: webhook-ingress, command-routing, pr-preview-pipeline,
cron-self-healing
* state: job-lifecycle (bounded leases), comment-publication (one-live-comment)
* rules: retry-budget (3 attempts, no DLQ), r2-retention (30-day),
authorization (collaborator gate)
* contracts: queue-message (job-id only), transactional-outbox
* datasets: d1-storage-schema (9 tables, migrations 0001+0002)
All source_paths verified to exist on disk; all internal cross-links resolve.
Changes:
* AGENTS.md: append managed OKF guidance block
* okf/index.md, okf/log.md: bundle root and changelog
* okf/architecture/{index,two-worker-split,storage-authority-model}.md
* okf/workflows/{index,webhook-ingress,command-routing,pr-preview-pipeline,cron-self-healing}.md
* okf/state/{index,job-lifecycle,comment-publication}.md
* okf/rules/{index,retry-budget,r2-retention,authorization}.md
* okf/contracts/{index,queue-message,transactional-outbox}.md
* okf/datasets/{index,d1-storage-schema}.md
Stats:
* 23 files changed
* +additions lines
Co-authored-by: Claude <noreply@anthropic.com>
Why: * bot comments had inconsistent or missing attribution — help carried the full footer, pr-preview said "Powered by Z.ai", stubs and errors had none What: * add a shared BOT_FOOTER constant and apply it uniformly before each message's hidden marker so every bot comment ends with identical attribution Changes: * shared/constants.js: add BOT_FOOTER constant * shared/commands.js: use BOT_FOOTER in formatHelp and formatCommandNotAvailable * heavy handlers (ask/describe/explain/impact/review): add footer to stub notices * heavy index.js: add footer to the heavy-command failure comment * main help.js: add footer to the /zai help error comment * main index.js: add footer to the unauthorized-command comment Stats: * 10 files changed, +29/-18 Co-authored-by: pi <noreply@earendil-works.com>
Why: * the auto-preview brief only needs PR identity; per-file stats are the job of the heavy /zai review pipeline, and computing them cost up to 30 paginated getPrFiles calls per large PR What: * render a metadata-only comment (repo/pr/title/author/head) and adopt the shared footer; stop computing, rendering, or persisting per-file data Changes: * shared/pr-preview.js: rewrite renderPrPreview to metadata-only, use BOT_FOOTER * shared/pr-stats.js: deleted (fetchPrStats + MAX_PR_FILES_API_LIMIT, no consumers) * shared/storage/keys.js: retire unused prFilesArtifactKey * heavy handlers/pr-preview.js: drop fetchPrStats call and the per-file R2 artifact * tests/storage.test.js: assert metadata-only rendering, drop the pagination test Stats: * 5 files changed, +26/-139 Co-authored-by: pi <noreply@earendil-works.com>
Why: * lock in the dedup contract — a PR gets exactly one preview comment, and new commits (synchronize) update it instead of creating duplicates What: * exercise handlePrPreviewJob across opened, synchronize, and stale-skip paths and assert update-not-create via the shared comment marker and the presence of the footer on every published body Changes: * tests/pr-preview-sync.test.js: add regression suite (4 cases) Stats: * 1 file changed, +165 Co-authored-by: pi <noreply@earendil-works.com>
Why: * the bundle still described the old per-file stats preview and had no concept for the shared BOT_FOOTER applied to every bot comment What: * add a unified bot comment footer rule concept and rework the PR-preview pipeline plus cross-referenced concepts to the metadata-only brief; record the refresh in the log Changes: * rules/comment-footer.md: new Business Rule for the shared BOT_FOOTER * workflows/pr-preview-pipeline.md: metadata-only steps, drop file-manifest + pr-stats source, fix failure step range * contracts/queue-message.md: rendered results, not manifests * rules/r2-retention.md: preview results, not file manifests * architecture/storage-authority-model.md: R2 role is rendered preview results * state/comment-publication.md: add Preview body section + footer link * rules/index.md, index.md: register the footer concept * log.md: record footer create + metadata-only updates Stats: * 9 files changed, +83/-17 Co-authored-by: pi <noreply@earendil-works.com>
Why:
* the README still described the pr-stats / file-manifest flow and the old
12-file/130-test suite, and did not mention the unified footer
What:
* update the file tree, the preview flow diagram, and the storage/GitHub tables
to the metadata-only brief, and bump the doc version
Changes:
* README.md: drop pr-stats.js + GET /pulls/{n}/files; single result artifact in the mermaid; metadata-only tree descs; 13 files/133 tests; version 0.3.2
Stats:
* 1 file changed, +19/-21
Co-authored-by: pi <noreply@earendil-works.com>
Why: * the PR preview posted a NEW comment on every synchronize instead of updating the previous one What: * findMarkerComment now accepts the exact comment we previously published (matched by the stored github_comment_id) regardless of author type, so the update path resolves for both GitHub Apps and PAT-owned bots even when GITHUB_BOT_LOGIN is not configured; the Bot/login filter remains for the marker-only fallback * add a real upsertComment regression suite (in-memory D1 fake, comments.js NOT mocked) covering PAT and App identity, fresh-create, and a stray-comment non-adoption guard Changes: * shared/comments.js: accept stored comment id in findMarkerComment filter * tests/comments-upsert.test.js: real-path update-vs-create regression tests Stats: * 2 files changed, +284/-0 Co-authored-by: pi <noreply@earendil-works.com>
Why: * editing a PR title sent action=edited, which the preview gate did not accept, so the title change triggered no refresh What: * accept the pull_request "edited" action but gate it on changes.title, since the preview is metadata-only (repository/PR/title/author/head) and body/base edits don't affect it; this avoids wasteful re-renders on unrelated edits Changes: * zai-main-worker/src/pr-events.js: add "edited" to SUPPORTED_PR_ACTIONS and gate it on changes.title in isSupportedPullRequestEvent(event, action, payload) * zai-main-worker/src/index.js: pass payload to the gate at the call site * tests/pr-events.test.js: cover edited+title vs body/base/no-change, plus extractPullRequestEvent title extraction Stats: * 3 files changed, +124/-3 Co-authored-by: pi <noreply@earendil-works.com>
Why: * closing a PR sent action=closed, which the preview gate rejected, so nothing happened and the PR state in D1 never advanced to closed What: * accept pull_request "closed", persist state=closed + closed_by (the webhook sender) on pull_requests, and post an idempotent "PR closed by @x" lifecycle comment that skips the supersede guard and leaves the preview untouched Changes: * pr-events.js: gate "closed"; extractPullRequestEvent adds closedBy (sender) * storage/deliveries.js: UPSERT closed_by via COALESCE; JOB_SELECT += p.state, p.closed_by * shared/constants.js: PR_CLOSED_MARKER * shared/pr-preview.js: renderPrClosed * handlers/pr-preview.js: closed branch -> publishClosedComment (no getPullRequest) * migrations/0003_pr_closed_by.sql: ALTER TABLE pull_requests ADD closed_by * tests: closed gate + closedBy extraction, renderPrClosed, handler closed branch Stats: * 9 files changed, +277/-12 Co-authored-by: pi <noreply@earendil-works.com>
Why: * the OKF bundle trailed the code: it still listed only opened/reopened/ synchronize/ready_for_review and had no concept of the close branch or the new closed_by column What: * add the closed lifecycle to the preview pipeline, the pr_closed comment kind, and migration 0003 (closed_by) to the schema concept; log the change Changes: * workflows/pr-preview-pipeline.md: trigger actions += edited/closed; Closed lifecycle section * state/comment-publication.md: comment kinds table (pr_preview, pr_closed) * datasets/d1-storage-schema.md: migrations 0001-0003, pull_requests.closed_by, 0003 section * log.md: 2026-08-07 entry for closed lifecycle + edited action Stats: * 4 files changed Co-authored-by: pi <noreply@earendil-works.com>
|
/zai help |
🤖 Z.ai Code Bot HelpAvailable commands: Code Review & Analysis
Documentation
Help
Usage Notes
Powered by AndreiDrang, Z.ai and Cloudflare Workers |
|
🤖 Reviewing |
Z.ai HelpAvailable commands:
|
|
/zai review |
|
🤖 Reviewing |
|
Error: No file path provided. Usage: /zai review |
🔍 /zai reviewSummaryThis PR migrates the zai-code-bot to Cloudflare Workers, adding worker infrastructure, durable storage, PR context gathering, and live Findings[Medium] Hardcoded paths to a different project throughout skills documentation The cd /workspace/Red-Panda-Dev__tbel/cf_workers/{worker-name}This appears in [Medium] TokenBel-specific conventions baked into reusable skills The
If these skills are intended to guide development in this repository, they will produce misleading documentation. If they're meant as generic Cloudflare reference material, they should be de-branded. [Low] Large volume of vendored reference documentation The PR adds substantial Cloudflare documentation files (Agents SDK, AI Gateway, and dozens of sub-references) directly into [Low] Missing trailing newline in The file ends without a trailing newline ( Notes
Powered by AndreiDrang, Z.ai and Cloudflare Workers |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
zai-main-worker | 5a4cc62 | Aug 16 2026, 08:17 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
zai-heavy-worker | 5a4cc62 | Aug 16 2026, 08:17 PM |
|
/zai help |
|
|
🤖 Reviewing |
Z.ai HelpAvailable commands:
|
|
/zai help |
🤖 Z.ai Code BotSupported commands:
The review and describe commands run asynchronously through Cloudflare Workers. Powered by AndreiDrang, Z.ai and Cloudflare Workers |
|
🤖 Reviewing |
Z.ai HelpAvailable commands:
|
PR detailed description from owner