From f5c895eda507d7972288a28c7c6652f369ecf803 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 9 May 2026 11:29:07 +0700 Subject: [PATCH] docs: CHANGELOG v0.1.0 + planning artifacts --- CHANGELOG.md | 20 +++ .../phase-01-repo-scaffold.md | 72 +++++++++ .../phase-02-worker-handler-fire-client.md | 142 ++++++++++++++++++ .../phase-03-wrangler-config-secrets.md | 109 ++++++++++++++ .../phase-04-tests-ci.md | 101 +++++++++++++ .../phase-05-docs-release.md | 89 +++++++++++ .../plan.md | 94 ++++++++++++ 7 files changed, 627 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 plans/260509-1046-claude-code-routine-trigger-worker/phase-01-repo-scaffold.md create mode 100644 plans/260509-1046-claude-code-routine-trigger-worker/phase-02-worker-handler-fire-client.md create mode 100644 plans/260509-1046-claude-code-routine-trigger-worker/phase-03-wrangler-config-secrets.md create mode 100644 plans/260509-1046-claude-code-routine-trigger-worker/phase-04-tests-ci.md create mode 100644 plans/260509-1046-claude-code-routine-trigger-worker/phase-05-docs-release.md create mode 100644 plans/260509-1046-claude-code-routine-trigger-worker/plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f801ad5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2026-05-09 + +### Added +- Cloudflare Workers `scheduled` handler that POSTs to the Claude Code routine `/fire` endpoint. +- Default 5×daily cron at UTC+7 (00:07 / 05:13 / 10:19 / 15:23 / 20:37) — scattered minutes for shared-infra hygiene. +- Token-substitution template with `{ISO}`, `{LocalTime}`, `{Cron}` (default: `Scheduled trigger at {LocalTime}`). +- IANA timezone support via `TZ` env var (default: `UTC`). +- Structured JSON logs for fire success / non-2xx / network failure. +- Vitest test suite covering template substitution, fire paths, header correctness, token redaction. +- GitHub Actions CI: `npm test` + `wrangler deploy --dry-run` on push and PR. +- Apache-2.0 license. diff --git a/plans/260509-1046-claude-code-routine-trigger-worker/phase-01-repo-scaffold.md b/plans/260509-1046-claude-code-routine-trigger-worker/phase-01-repo-scaffold.md new file mode 100644 index 0000000..c152607 --- /dev/null +++ b/plans/260509-1046-claude-code-routine-trigger-worker/phase-01-repo-scaffold.md @@ -0,0 +1,72 @@ +--- +phase: 1 +title: Repo scaffold +status: completed +priority: P1 +effort: 1h +dependencies: [] +--- + +# Phase 1: Repo scaffold + +## Overview +Greenfield repo init. Apache-2.0 license, README skeleton, JS + wrangler toolchain, gitignore. No transpile step — Workers runtime executes ES modules natively. + +## Requirements +- **Functional:** `npm install` succeeds; `wrangler deploy --dry-run` validates an empty `worker.js` handler. +- **Non-functional:** No secrets in repo. Pin wrangler major version. kebab-case file names. + +## Architecture +Flat layout — single-purpose worker, no need for src/ subdirs (KISS). + +``` +claude-code-routine-trigger-worker/ +├── .github/workflows/ci.yml # phase 4 +├── .gitignore +├── LICENSE # Apache-2.0 +├── README.md # phase 5 +├── package.json +├── wrangler.toml # phase 3 +├── worker.js # phase 2 +└── worker.test.js # phase 4 +``` + +## Related Code Files +- Create: `LICENSE` (Apache-2.0 verbatim) +- Create: `.gitignore` (node_modules, .dev.vars, .wrangler, dist, .env*) +- Create: `package.json` (name, version 0.0.0, `"type": "module"`, scripts: test, deploy, dev) +- Create: `README.md` (one-liner placeholder; full content in phase 5) + +## Implementation Steps +1. `cd /config/workspace/tiennm99/claude-code-routine-trigger-worker && git init -b main` +2. Write `LICENSE` (copy Apache-2.0 from sibling `claude-code-routine-cron/LICENSE`). +3. Write `.gitignore`: + ``` + node_modules/ + dist/ + .wrangler/ + .dev.vars + .env + .env.* + *.log + ``` +4. Write `package.json` with `"type": "module"`, devDeps: `wrangler` (^4), `vitest` (phase 4), `@cloudflare/vitest-pool-workers` (phase 4). + Scripts: + - `dev`: `wrangler dev` + - `deploy`: `wrangler deploy` + - `test`: `vitest run` +5. Run `npm install` — confirm lockfile generated. +6. Stub `README.md` with one-liner: `> Cloudflare Workers cron port of claude-code-routine-cron — fires Claude Code routines on a schedule.` +7. First commit: `chore: init repo scaffold`. + +## Success Criteria +- [ ] `npm install` exits 0 +- [ ] `wrangler deploy --dry-run` validates empty `worker.js` exporting `export default {}` +- [ ] `git status` clean after first commit +- [ ] LICENSE file matches Apache-2.0 exactly +- [ ] `package.json` has `"type": "module"` + +## Risk Assessment +- **Wrangler version drift:** pin to current major (e.g. `^4.0.0`); document upgrade path in README. +- **Node version:** wrangler 4 needs Node ≥18; document in README prereqs. +- **No type safety:** mitigate by JSDoc type hints in `worker.js` for `Env` and handler signatures (cheap, optional, no toolchain). diff --git a/plans/260509-1046-claude-code-routine-trigger-worker/phase-02-worker-handler-fire-client.md b/plans/260509-1046-claude-code-routine-trigger-worker/phase-02-worker-handler-fire-client.md new file mode 100644 index 0000000..d87b77f --- /dev/null +++ b/plans/260509-1046-claude-code-routine-trigger-worker/phase-02-worker-handler-fire-client.md @@ -0,0 +1,142 @@ +--- +phase: 2 +title: Worker handler + fire client +status: completed +priority: P1 +effort: 2-3h +dependencies: + - 1 +--- + +# Phase 2: Worker handler + fire client + +## Overview +Implement the `scheduled` handler in plain JS with JSDoc type annotations: render text template, POST to `/fire`, log structured JSON. Single `worker.js` file. No `fetch` handler (per locked decision: scheduled-only). + +## Requirements +- **Functional:** + - On cron tick, POSTs to `ROUTINE_FIRE_URL` with bearer token, beta header, JSON body. + - Logs `claude_code_session_url` on 2xx; logs status + body on non-2xx; never throws (would crash CF retry). + - Renders `TEXT_TEMPLATE` with `{LocalTime}`, `{Cron}`, `{ISO}` placeholders. + - No retry (each `/fire` = new session). +- **Non-functional:** + - Token never logged. + - Use `console.log(JSON.stringify({...}))` for structured logs (CF picks these up). + - JSDoc `@typedef Env` for environment bindings + `@param`/`@returns` on all exports. + - Total file ≤ 200 lines (split if exceeds). + +## Architecture + +``` +ScheduledEvent (cron, scheduledTime) + │ + ▼ +fireRoutine(env, controller) + ├── renderText(template, vars) # simple {Tok} substitution + ├── fetch(URL, POST, headers, body) + └── log(json) # success or error +``` + +### Mapping from Go daemon +| Go (claude-code-routine-cron) | JavaScript (this worker) | +|---|---| +| `Config.FireURL` env | `env.ROUTINE_FIRE_URL` secret | +| `Config.Token` env | `env.ROUTINE_FIRE_TOKEN` secret | +| `Config.Schedules` env | `wrangler.toml` `[triggers].crons` (literal) | +| `Config.Location` (TZ) | `env.TZ` plain var, default `UTC` | +| `Config.Template` (Go text/template) | `env.TEXT_TEMPLATE` plain string + `{Tok}` substitution | +| `FireClient.Fire()` | `fireRoutine()` function | +| `cron.New()` scheduler | CF Workers scheduled handler (built-in) | +| `slog` JSON logs | `console.log(JSON.stringify(...))` | + +### JSDoc typedefs +```js +/** + * @typedef {object} Env + * @property {string} ROUTINE_FIRE_URL - secret: Anthropic /fire endpoint + * @property {string} ROUTINE_FIRE_TOKEN - secret: per-routine bearer token + * @property {string} [TEXT_TEMPLATE] - plain var: prompt template, supports {LocalTime} {Cron} {ISO} + * @property {string} [TZ] - plain var: IANA tz, default 'UTC' + */ + +/** + * @typedef {object} FireResponse + * @property {string} type + * @property {string} claude_code_session_id + * @property {string} claude_code_session_url + */ +``` + +### Headers (verbatim from siblings) +``` +Authorization: Bearer ${token} +anthropic-version: 2023-06-01 +anthropic-beta: experimental-cc-routine-2026-04-01 +Content-Type: application/json +``` + +### Body +```json +{ "text": "" } +``` + +### Template substitution (KISS — not Go text/template) +| Token | Value | +|---------------|-------------------------------------------------------| +| `{ISO}` | `new Date(scheduledTime).toISOString()` | +| `{LocalTime}` | formatted via `Intl.DateTimeFormat(env.TZ ?? 'UTC')` | +| `{Cron}` | `controller.cron` | + +Default template: `Scheduled trigger at {LocalTime}` — same default as Go daemon. + +## Related Code Files +- Create: `worker.js` (export default `{ scheduled }`, JSDoc-annotated) +- (Optional split if >200 lines) `fire-client.js`, `template-renderer.js` + +## Implementation Steps +1. Top of `worker.js`: write JSDoc `@typedef Env` + `@typedef FireResponse` blocks. +2. Implement `renderText(template, vars)` with JSDoc: + ```js + /** + * @param {string} template + * @param {Record} vars + * @returns {string} + */ + ``` + Replaces `{Key}` tokens, leaves unknown tokens intact. +3. Implement `formatLocalTime(date, tz)` using `Intl.DateTimeFormat`. JSDoc: `@param {Date} date`, `@param {string} tz`, `@returns {string}`. +4. Implement `fireRoutine(env, controller)` with JSDoc `@param {Env} env`, `@param {ScheduledController} controller`, `@returns {Promise}`: + - Build `text` via `renderText`. + - `fetch(env.ROUTINE_FIRE_URL, { method: 'POST', headers, body: JSON.stringify({text}) })`. + - On `!response.ok`: `console.log(JSON.stringify({level:'error', cron, status, body}))`. + - On `ok`: parse JSON, log `{level:'info', cron, session_url, session_id}`. + - Wrap in try/catch — log network errors, don't rethrow. +5. Export default with JSDoc `@type {ExportedHandler}`: + ```js + /** @type {ExportedHandler} */ + export default { + async scheduled(controller, env, ctx) { + ctx.waitUntil(fireRoutine(env, controller)); + }, + }; + ``` +6. `npx tsc --noEmit --allowJs --checkJs worker.js --target es2022 --module esnext --moduleResolution bundler --types @cloudflare/workers-types` (one-shot smoke check; not added to scripts to avoid TS dependency creep). Optional — skip if undesired. +7. Local smoke: `wrangler dev --test-scheduled`, then `curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"` — confirm log line. + +## Success Criteria +- [ ] `worker.js` ≤ 200 lines (split if needed) +- [ ] All exported functions have JSDoc with `@param` + `@returns` +- [ ] `wrangler deploy --dry-run` validates module +- [ ] Token never appears in logged output (manual grep on `wrangler dev` output) +- [ ] Local `__scheduled` test produces structured JSON log + +## Risk Assessment +- **`Intl.DateTimeFormat` TZ support on Workers runtime:** supported since 2023; verify with smoke test. +- **`ctx.waitUntil` vs awaiting in `scheduled`:** `waitUntil` ensures fire completes even if handler returns; preferred per CF docs. +- **No compile-time type errors:** JSDoc + editor LSP catches most; runtime tests (phase 4) catch the rest. +- **Template injection:** `text` is sent only to Anthropic API; no XSS / SQL surface — safe. + +## Security Considerations +- Token comes only from `env.ROUTINE_FIRE_TOKEN` (CF Secret), never bundled. +- `console.log` body must not include `Authorization` header value. +- TLS to `api.anthropic.com` is automatic (Workers `fetch` validates certs). diff --git a/plans/260509-1046-claude-code-routine-trigger-worker/phase-03-wrangler-config-secrets.md b/plans/260509-1046-claude-code-routine-trigger-worker/phase-03-wrangler-config-secrets.md new file mode 100644 index 0000000..bb209f1 --- /dev/null +++ b/plans/260509-1046-claude-code-routine-trigger-worker/phase-03-wrangler-config-secrets.md @@ -0,0 +1,109 @@ +--- +phase: 3 +title: wrangler config + secrets +status: completed +priority: P1 +effort: 1h +dependencies: + - 1 + - 2 +--- + +# Phase 3: wrangler config + secrets + +## Overview +Author `wrangler.toml` with literal `[triggers].crons`, define plain vars vs secrets, document deployment flow. CF Workers cron triggers fire from `wrangler.toml` only — same constraint as GH Actions. + +## Requirements +- **Functional:** + - `wrangler deploy` succeeds with valid `wrangler.toml`. + - At least one cron expression in `[triggers]` (default 5×daily mirroring sibling repos). + - Secrets uploaded out-of-band (not in `wrangler.toml`). +- **Non-functional:** + - All comments above each block explain the field. + - Default crons use scattered minutes (avoid minute `0`) per `claude-code-routine-trigger` README warning — though CF cron is more reliable than GH, scattering is still good hygiene. + +## Architecture + +### `wrangler.toml` shape +```toml +name = "claude-code-routine-trigger-worker" +main = "worker.js" +compatibility_date = "2026-05-09" +compatibility_flags = ["nodejs_compat"] # only if Intl needs polyfill — verify in phase 2 + +# Plain vars (visible in dashboard, fine for non-secrets) +[vars] +TEXT_TEMPLATE = "Scheduled trigger at {LocalTime}" +TZ = "Asia/Ho_Chi_Minh" + +# Cron triggers — LITERAL, cannot read from env/secrets/vars +[triggers] +crons = [ + "7 17 * * *", # 00:07 UTC+7 + "13 22 * * *", # 05:13 UTC+7 + "19 3 * * *", # 10:19 UTC+7 + "23 8 * * *", # 15:23 UTC+7 + "37 13 * * *", # 20:37 UTC+7 +] + +# Observability — opt-in, free tier supports basic logs +[observability] +enabled = true +``` + +### Secrets (uploaded via `wrangler secret put`) +| Name | Source | +|----------------------|-------------------------------------| +| `ROUTINE_FIRE_URL` | Anthropic routine editor → API trigger | +| `ROUTINE_FIRE_TOKEN` | Anthropic routine editor (shown once) | + +### Local dev: `.dev.vars` +``` +ROUTINE_FIRE_URL=https://api.anthropic.com/v1/claude_code/routines/trig_.../fire +ROUTINE_FIRE_TOKEN=sk-ant-oat01-... +``` +Gitignored. Used by `wrangler dev` only. + +### `wrangler.toml.example` +Committed copy with placeholder values for documentation; real `wrangler.toml` ships with author's defaults but secrets are NEVER literal. + +## Related Code Files +- Create: `wrangler.toml` +- Create: `wrangler.toml.example` (or document in README — KISS, prefer README) +- Create: `.dev.vars.example` with placeholder secret names + dummy values + +## Implementation Steps +1. Write `wrangler.toml` per architecture block above. + - Pick `compatibility_date` = today (2026-05-09). + - Decide on `nodejs_compat` flag — only enable if phase 2 needs it (`Intl` is built-in, likely **don't need it**). +2. Write `.dev.vars.example`: + ``` + ROUTINE_FIRE_URL=https://api.anthropic.com/v1/claude_code/routines/trig_REPLACE/fire + ROUTINE_FIRE_TOKEN=sk-ant-oat01-REPLACE + ``` +3. Verify `.gitignore` includes `.dev.vars` (added in phase 1). +4. Local validation: `wrangler dev` — must boot without error. +5. Document secret-upload flow in phase 5 README: + ```bash + echo -n 'https://...' | wrangler secret put ROUTINE_FIRE_URL + echo -n 'sk-ant-oat01-...' | wrangler secret put ROUTINE_FIRE_TOKEN + ``` +6. Commit: `feat: wrangler config with default 5x-daily crons`. + +## Success Criteria +- [ ] `wrangler dev` starts cleanly with `.dev.vars` populated +- [ ] `wrangler deploy --dry-run` validates `wrangler.toml` without errors +- [ ] All 5 default crons match sibling repos' cadence (UTC+7 daily 00/05/10/15/20) +- [ ] No secret values in any committed file +- [ ] `.dev.vars` confirmed in `.gitignore` + +## Risk Assessment +- **CF cron expression syntax:** CF Workers cron supports standard 5-field; `*/N` and ranges work. Avoid `?` (Quartz-only). +- **Minute-`0` GH issue does not apply to CF** but scattering minutes is still recommended (free tier shared infra benefits). +- **Free tier limit:** 5 cron triggers per worker. Default config uses exactly 5 — at limit. Document that adding a 6th requires another worker. + +## Security Considerations +- Secrets never in `wrangler.toml` (only `[vars]` for non-secrets). +- `.dev.vars` is gitignored. +- `wrangler secret put` accepts stdin, avoiding shell history leak (use `echo -n ... | wrangler secret put`). diff --git a/plans/260509-1046-claude-code-routine-trigger-worker/phase-04-tests-ci.md b/plans/260509-1046-claude-code-routine-trigger-worker/phase-04-tests-ci.md new file mode 100644 index 0000000..85205b6 --- /dev/null +++ b/plans/260509-1046-claude-code-routine-trigger-worker/phase-04-tests-ci.md @@ -0,0 +1,101 @@ +--- +phase: 4 +title: Tests + CI +status: completed +priority: P2 +effort: 2h +dependencies: + - 2 + - 3 +--- + +# Phase 4: Tests + CI + +## Overview +Vitest with `@cloudflare/vitest-pool-workers` for in-Workers-runtime tests. Cover template rendering, fire client behavior (success / non-2xx / network error), token redaction. GitHub Actions CI on push. + +## Requirements +- **Functional:** + - Tests cover: template substitution, missing-token rendering, fire success path, non-2xx path, fetch throws path. + - CI runs `npm test` on push to `main` and PRs. +- **Non-functional:** + - Coverage report (text summary in CI logs). + - Tests use mocked `fetch` — no real network calls (don't burn user's Anthropic quota). + - Tests written in plain JS with JSDoc, matching production code style. + - Race-free is N/A on Workers; instead assert `console.log` output never contains token. + +## Architecture + +### Test layout +``` +worker.test.js # all tests in one file initially (KISS) +``` +Split if exceeds 200 lines. + +### Test cases (mirror Go daemon's test coverage) +| # | Case | Assertion | +|---|------|-----------| +| 1 | `renderText` substitutes `{LocalTime}`, `{Cron}`, `{ISO}` | output equals expected string | +| 2 | `renderText` leaves unknown `{Foo}` tokens intact | output contains `{Foo}` | +| 3 | `fireRoutine` 2xx response logs `session_url` | console captured contains `session_url` | +| 4 | `fireRoutine` 401 response logs `level:error` | captured log has `level:'error'`, `status:401` | +| 5 | `fireRoutine` `fetch` rejects (network) logs error, doesn't throw | no thrown exception, error log present | +| 6 | Token redaction | for cases 3, 4, 5: stringified logs do NOT contain token value | +| 7 | Headers correctness | mocked `fetch` receives `Authorization: Bearer ${token}`, `anthropic-version`, `anthropic-beta` | +| 8 | Body shape | request body parses as `{ "text": }` | +| 9 | Default template when `TEXT_TEMPLATE` unset | output starts with `Scheduled trigger at ` | +| 10 | TZ defaults to UTC when `env.TZ` unset | local time formatting uses UTC | + +### CI workflow +`.github/workflows/ci.yml` — single job, Node 20, runs test + wrangler dry-run as smoke gate. + +## Related Code Files +- Create: `worker.test.js` +- Create: `vitest.config.js` (uses `@cloudflare/vitest-pool-workers`) +- Create: `.github/workflows/ci.yml` + +## Implementation Steps +1. Install: `npm i -D vitest @cloudflare/vitest-pool-workers`. +2. Write `vitest.config.js`: + ```js + import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + export default defineWorkersConfig({ + test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' } } } }, + }); + ``` +3. Write `worker.test.js` covering all 10 cases. Use `vi.fn()` mock for `globalThis.fetch`. JSDoc-annotate test helper functions for IDE assist. +4. Run `npm test` — all green. +5. Write `.github/workflows/ci.yml`: + ```yaml + name: ci + on: + push: { branches: [main] } + pull_request: + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: '20', cache: 'npm' } + - run: npm ci + - run: npm test + - run: npx wrangler deploy --dry-run + ``` +6. Push to GitHub; verify green CI. + +## Success Criteria +- [ ] All 10 test cases pass locally +- [ ] `npm test` exits 0 in CI +- [ ] `wrangler deploy --dry-run` exits 0 in CI +- [ ] No real HTTP traffic generated by tests (verify by mock spy) +- [ ] Coverage ≥ 80% (track via vitest `--coverage`; not enforced as blocking gate) + +## Risk Assessment +- **Workers test pool quirks:** `@cloudflare/vitest-pool-workers` requires wrangler config; ensure `wrangler.toml` validates first. +- **Mocking `fetch` inside Workers runtime:** verify pattern works — fall back to dependency-injected fetch if not. +- **Beta header pinning in tests:** assert exact value to catch accidental edits. + +## Security Considerations +- Test fixtures use placeholder token `sk-ant-oat01-test-only` — never a real value. +- Test asserts logs **don't contain** the token (regression guard). diff --git a/plans/260509-1046-claude-code-routine-trigger-worker/phase-05-docs-release.md b/plans/260509-1046-claude-code-routine-trigger-worker/phase-05-docs-release.md new file mode 100644 index 0000000..c58a2b1 --- /dev/null +++ b/plans/260509-1046-claude-code-routine-trigger-worker/phase-05-docs-release.md @@ -0,0 +1,89 @@ +--- +phase: 5 +title: Docs + release +status: completed +priority: P2 +effort: 1.5h +dependencies: + - 1 + - 2 + - 3 + - 4 +--- + +# Phase 5: Docs + release + +## Overview +Full README mirroring sibling repos' tone, comparison-table back-link in both siblings, v0.1.0 GitHub release, public deployment of the author's own instance. + +## Requirements +- **Functional:** + - README covers: why-this-vs-siblings, quickstart, env vars, secret upload, customize schedule, beta header policy, security, license. + - GitHub release `v0.1.0` tagged. + - Both sibling READMEs updated with a row pointing to this repo. +- **Non-functional:** + - Same prose voice as siblings (`> [!TIP]` / `> [!WARNING]` callouts, terse). + - Code blocks copy-pasteable. + - All claims about CF cron precision sourced (link CF docs). + +## Architecture + +### README sections (in order) +1. **Header + tagline** — one-liner. +2. **Why this vs siblings** — three-column comparison table. +3. **Quickstart** — `wrangler deploy` + 2 secret-puts + done. +4. **Environment variables** — table (name, required, default, notes). +5. **Multiple schedules** — `wrangler.toml` `[triggers].crons` array. +6. **Templates** — `{LocalTime}`, `{Cron}`, `{ISO}`. +7. **Customize schedule** — edit `wrangler.toml`, `wrangler deploy`. +8. **Local development** — `.dev.vars` + `wrangler dev --test-scheduled`. +9. **Secret rotation** — `wrangler secret put` overwrites. +10. **CF Workers free tier** — link, note 5-cron-per-worker limit. +11. **Beta header** — pinned value, bump policy. +12. **Operational notes** — log access (`wrangler tail`), no idempotency. +13. **License** — Apache-2.0. + +### Sibling repo updates +- Edit `claude-code-routine-trigger/README.md` — add row in the "no longer used by author" callout listing `trigger-worker` as another option. +- Edit `claude-code-routine-cron/README.md` — extend "Why this vs `claude-code-routine-trigger`" table to a 3-way comparison including `trigger-worker`. + +## Related Code Files +- Modify: `README.md` (this repo) +- Modify (out of repo, requires PR/commit): `/config/workspace/tiennm99/claude-code-routine-trigger/README.md` +- Modify (out of repo): `/config/workspace/tiennm99/claude-code-routine-cron/README.md` +- Create: `CHANGELOG.md` (Keep-A-Changelog format) +- Optional: `.github/release.yml` for release-notes automation + +## Implementation Steps +1. Draft README per architecture sections — use sibling READMEs verbatim where applicable (Apache-2.0 license clauses, beta header note, security note about per-routine token scope). +2. Write `CHANGELOG.md` with `## [0.1.0] - 2026-MM-DD` initial entry listing features. +3. Local proof: deploy author's instance via `wrangler deploy`, attach a non-prod routine, observe one cron tick, capture `session_url` for README screenshot. +4. Tag and release: + ```bash + git tag v0.1.0 + git push origin v0.1.0 + gh release create v0.1.0 --generate-notes + ``` +5. Open PRs (or direct commits if author owns) to update sibling READMEs with the comparison row. +6. Final commit: `docs: README + v0.1.0 release notes`. + +## Success Criteria +- [ ] README renders cleanly on GitHub +- [ ] All code blocks in README are runnable (copy-paste tested) +- [ ] Comparison table accurately reflects all 3 options +- [ ] Both sibling READMEs reference this repo +- [ ] `v0.1.0` tag pushed and GitHub release published +- [ ] Author's own deployment fired at least one successful cron tick (proof-of-life) + +## Risk Assessment +- **Beta header drift:** if Anthropic ships new dated beta between phase 2 and release, bump in code + CHANGELOG before tagging. +- **Sibling repo edits cause merge conflicts:** branch + PR if user has unmerged work; otherwise direct commit. +- **CF dashboard URL format changes:** don't hardcode dashboard URLs in README; use `wrangler tail` (CLI) which is stable. + +## Security Considerations +- README must NOT include real `ROUTINE_FIRE_URL` or token — only `sk-ant-oat01-...` placeholder format. +- Document that pushing to a public fork preserves `.dev.vars` exclusion (gitignore line). + +## Next Steps (post v0.1.0) +- v0.2: optional `WEBHOOK_URL` to mirror fire output to Slack/Discord (low priority — most users use `wrangler tail`). +- v0.3: `wrangler.toml.example` with multi-routine deployment helper script. diff --git a/plans/260509-1046-claude-code-routine-trigger-worker/plan.md b/plans/260509-1046-claude-code-routine-trigger-worker/plan.md new file mode 100644 index 0000000..4d907e0 --- /dev/null +++ b/plans/260509-1046-claude-code-routine-trigger-worker/plan.md @@ -0,0 +1,94 @@ +--- +title: claude-code-routine-trigger-worker — CF Workers cron port +description: >- + Cloudflare Workers port of claude-code-routine-cron: scheduled handler fires + Claude Code routine /fire endpoint, zero infra, free tier +status: completed +completed: 2026-05-09 +priority: P2 +created: 2026-05-09T00:00:00.000Z +target_repo: /config/workspace/tiennm99/claude-code-routine-trigger-worker +reference_repos: + - /config/workspace/tiennm99/claude-code-routine-trigger + - /config/workspace/tiennm99/claude-code-routine-cron +license: Apache-2.0 +blockedBy: [] +blocks: [] +--- + +# claude-code-routine-trigger-worker — CF Workers cron port + +## Problem +Two existing siblings cover the routine-fire space, both with trade-offs: +- `claude-code-routine-trigger` (GH Actions): free, but cron unreliable (30 min – 2h delays, occasional drops at minute `0`). +- `claude-code-routine-cron` (Go daemon, Docker): sub-second precision, but requires self-hosted infra. + +Gap: a free, no-infra option with reliable timing. + +## Goal +Cloudflare Workers port: scheduled handler POSTs to the Claude Code routine `/fire` endpoint on a fixed cron. Free tier (CF Workers cron triggers cost $0 within free limits). Single TypeScript file, deploy via `wrangler deploy`. + +## Locked Decisions +| # | Decision | Choice | +|---|----------|--------| +| 1 | Repo name | Completed | +| 2 | Scope | Completed | +| 3 | Cron config | Completed | +| 4 | Manual HTTP fire | Completed | +| 5 | Language | Completed | +| 6 | Secret management | `wrangler secret put` — `ROUTINE_FIRE_URL`, `ROUTINE_FIRE_TOKEN` | +| 7 | Optional config | `TEXT_TEMPLATE` (env var, plain string with placeholders), `TZ` (env var) | +| 8 | License | Apache-2.0 (matches siblings) | + +## Non-goals +- Manual `/fire` HTTP endpoint (KISS — scheduled only) +- Multiple routines per worker (deploy N workers for N routines) +- Retry on failure (each POST = new session; retries multiply sessions) +- Durable Objects / KV / D1 (stateless) +- Custom log shipping (CF dashboard `wrangler tail` is enough) +- Go-style `text/template` (use simple `{token}` substitution — KISS) + +## Comparison Matrix (post-implementation) +| | trigger (GH Actions) | cron (Go daemon) | **trigger-worker (this)** | +| ---------------- | -------------------- | ------------------------ | ------------------------- | +| Runs on | GitHub runners | self-hosted Docker | Cloudflare edge | +| Cost | free (GH minutes) | minimal (own infra) | free (CF free tier) | +| Cron precision | ±30 min – 2 h | sub-second | ±15 sec (CF SLA) | +| Setup | fork + 2 secrets | env vars + Docker | wrangler deploy + secrets | +| Audit trail | Actions runs page | container stdout | CF dashboard / `tail` | + +## Phases + +| Phase | Name | Status | +|-------|------|--------| +| 1 | [Repo scaffold](./phase-01-repo-scaffold.md) | Completed | +| 2 | [Worker handler + fire client](./phase-02-worker-handler-fire-client.md) | Completed | +| 3 | [wrangler config + secrets](./phase-03-wrangler-config-secrets.md) | Completed | +| 4 | [Tests + CI](./phase-04-tests-ci.md) | Completed | +| 5 | [Docs + release](./phase-05-docs-release.md) | Completed | + +## Dependencies +None. Greenfield repo. Reference repos read-only. + +## Risk Register +| Risk | Severity | Mitigation | +|---|---|---| +| CF Workers cron precision in practice | Low | Documented ±15 sec — adequate for routine triggering | +| Wrangler config drift with new CF features | Low | Pin wrangler major version in `package.json` | +| Beta header (`experimental-cc-routine-2026-04-01`) churn | Medium | Constant in code; bump via release; siblings have same exposure | +| Free tier cron limit (5 unique cron expressions per worker) | Low | One-routine-per-worker model means single-digit crons typical | +| Secrets accidentally committed | High | `.dev.vars` gitignored; secrets only via `wrangler secret put` in CI | + +## Success Criteria +- [ ] `wrangler deploy` ships worker; first cron tick fires `/fire` and logs `claude_code_session_url`. +- [ ] `wrangler tail` shows JSON-structured logs for each fire. +- [ ] All CF Workers cron expressions in `wrangler.toml` validated by `wrangler dev` locally. +- [ ] CI runs typecheck + tests on push. +- [ ] README documents quickstart, env vars, secret rotation, beta header policy. +- [ ] Release `v0.1.0` published with `wrangler.toml` example. + +## References +- Sibling Go daemon: `/config/workspace/tiennm99/claude-code-routine-cron` +- Sibling GH Actions: `/config/workspace/tiennm99/claude-code-routine-trigger` +- Anthropic `/fire` API: https://platform.claude.com/docs/en/api/claude-code/routines-fire +- CF Workers cron: https://developers.cloudflare.com/workers/configuration/cron-triggers/