@golden-frijoles/kit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,473 @@
1
+ # Claude Routines — stand-up runbook
2
+
3
+ The approved **Claude Code Routines** (research preview) for this project, as committed,
4
+ reviewable prompt artifacts plus the steps to stand them up. Routines are saved cloud Claude Code
5
+ configurations (prompt + repos + triggers) that run **autonomously on Anthropic-managed infra, as
6
+ the product owner** — so they keep running with the laptop closed. Created/managed in the product owner's account at
7
+ `claude.ai/code/routines` (or `/schedule` in the CLI).
8
+
9
+ > **TEMPLATE FILL-IN — read this first.** These seven prompts were proven in the project this template
10
+ > was extracted from, and ported by dobby-foundation's plugin-audit-and-extraction epic (Sprint 2) with
11
+ > their doctrine intact. The dated incidents below are that project's history, kept because they are the
12
+ > evidence for each rule. To adopt them:
13
+ > 1. Replace every `<your-org>/<root-repo>`, `<app-repo>`, `<api-repo>`, `<appDir>`, `<PROD_DOMAIN>` /
14
+ > `<PROD_URL>` and `TEMPLATE FILL-IN` in these files (`grep -rn "TEMPLATE FILL-IN\|<[a-z-]*-repo>" .`).
15
+ > The reporting routines read their repos from `reporting.config.json`, not from this file.
16
+ > 2. Stand each routine up from its prompt, and record its trigger id where this runbook shows
17
+ > `<trigger-id>` — rule 3 below is why that matters.
18
+ > 3. To drop a routine you won't run, delete its prompt AND its name from `routines.test.mjs`'s expected
19
+ > list. That is a visible, reviewed diff on purpose; the one-merge-authority rule holds regardless.
20
+
21
+ This repo commits the *prompts + this runbook*. **The account stand-up itself is operational, owed to
22
+ the product owner** — installing the GitHub App, creating the routines from these prompts, and setting B's
23
+ secrets/allow-list. Nothing here provisions infra or changes any account.
24
+
25
+ ## Stand one up
26
+
27
+ Fill this project's values in `golden-frijoles.config.json` under `routines`, then render a reviewed prompt:
28
+
29
+ ```bash
30
+ node scripts/routine-bootstrap.mjs <name>
31
+ ```
32
+
33
+ Paste the output into `/schedule` in Claude Code (or `claude.ai/code/routines`). Scheduled routines have a
34
+ **minimum interval of one hour** and each account has a **daily run cap**, so consolidate related work before
35
+ adding another schedule. A routine that posts to Telegram needs a **custom network environment**: the default
36
+ Trusted network allowlist does not include `api.telegram.org`.
37
+
38
+ ## The three rules that hold for all of them
39
+
40
+ 1. **Advisory only — never a required check.** Every routine's output is a PR comment or a `claude/`
41
+ PR for a human to review. None gates a merge, deploy, or money path. A plain PR **comment carries no
42
+ commit-status**, so it *structurally cannot* be added as a required check in branch protection —
43
+ keep it that way (Routine A must stay comment-only; never wire any status reporting). The
44
+ deterministic layers (CI, `browser-smoke.yml`, `notion-sync.yml`, the deploy
45
+ notifiers) remain the sole sources of truth.
46
+
47
+ > ⚠️ **ONE EXCEPTION, added 2026-08-17 by the product owner: Routine B may merge its own PR** — and only
48
+ > when `scripts/smoke-triage-scope.mjs` returns ALLOW, meaning the diff is test scaffolding only
49
+ > and weakens nothing. See B's own section. Rule 1 is otherwise unchanged, and the "never a
50
+ > required check" half is unchanged for *every* routine including B: the browser smoke remains the
51
+ > sole detector and nothing a routine emits gates anything.
52
+
53
+ 2. **Leave push at the `claude/` default.** A routine may only push `claude/`-prefixed branches unless
54
+ unrestricted push is explicitly enabled — don't enable it. A only comments (no push); B/C open
55
+ `claude/` PRs. (Merging a `claude/` PR needs no wider push scope — B stays inside this default.)
56
+
57
+ 3. **🚨 A prompt file in this directory is NOT what the cloud stores. Keep the two reconciled, or you changed
58
+ nothing.** The cloud routine stores its own copy of the prompt in the product owner's account; editing the
59
+ committed file here does not touch it. Both known routines had silently diverged:
60
+
61
+ | Routine | Drift found | State |
62
+ |---|---|---|
63
+ | `smoke-triage` (`<trigger-id>`) | Stored prompt was a short ad-hoc text that had **never** matched `smoke-triage.prompt.md` — no diagnosis rubric, no advisory banner, no failure ping, and branch `claude/smoke-fix-<date>` instead of `claude/smoke-triage-<date>` | re-synced 2026-08-17 |
64
+ | `ops-nightly` (`<trigger-id>`) | Stored prompt still called `skills/<name>/SKILL.md` paths that no longer exist (skills moved to the `golden-frijoles` plugin) and still described step 4 as a simple skill call, not the 3-phase CPO prose write with its mechanical guard | re-synced 2026-08-17 |
65
+
66
+ The ops-nightly divergence was caught by the routine itself — it followed the repo file instead of
67
+ its stored prompt and reported the mismatch. That worked because the run had the repo in front of
68
+ it. **Do not rely on that.** A drifted prompt is invisible from the codebase, which is the same
69
+ failure shape as a fixture pinned to an Actions secret.
70
+
71
+ ### The fix: the stored prompt is a BOOTSTRAP, not a copy
72
+
73
+ Copying 9KB of prose into the cloud on every edit is a sync ritual, and a sync ritual is a thing
74
+ people forget — which is how both routines drifted in the first place. So as of 2026-08-17 the
75
+ stored prompt for `smoke-triage` and `ops-nightly` no longer *contains* the instructions. It is a
76
+ short bootstrap that says: **read `scripts/routines/<name>.prompt.md` from the repo and follow
77
+ everything after the first `---`.** The repo file is the only copy, so it cannot disagree with
78
+ itself.
79
+
80
+ Three things make that safe, and none of them is optional:
81
+
82
+ - **A bootstrap that cannot read its file must STOP**, not improvise from memory. Both say so
83
+ explicitly, and both fall back to a Telegram `FAILED` line rather than a guessed run. An
84
+ improvised triage that merges something is far worse than a night with no triage.
85
+ - **The few rules that must survive a bad checkout are restated in the bootstrap itself** — never
86
+ weaken a spec, never merge app code, never `vercel-prune --apply`, write the standup prose in
87
+ session. Belt and braces on the ones where being wrong is expensive.
88
+ - **`smoke-triage` now lists the root repo as a second source**, because its prompt and its merge
89
+ gate (`scripts/smoke-triage-scope.mjs`) both live here while its work happens in the frontend
90
+ repo.
91
+
92
+ **What still needs doing by hand:** creating a routine, changing its schedule/env/sources, or
93
+ editing a bootstrap. Use the `RemoteTrigger` tool — `action: "list"` shows every trigger with its
94
+ stored prompt, `"get"` shows one, `"update"` writes. There is no CLI, so there is no `--check`
95
+ script to add; a script that could not actually read the triggers would be one that exits green
96
+ having checked nothing.
97
+
98
+ **Not yet converted:** `roadmap-hygiene` and `prod-smoke` still carry full stored prompts.
99
+ `roadmap-hygiene`'s has the same drift (it predates the committed `roadmap-hygiene.prompt.md`).
100
+ Convert them the same way when either is next touched.
101
+
102
+ > ⚠️ **Schedule drift, recorded not fixed:** this file says smoke-triage runs "~10:00 UTC"; the
103
+ > live trigger is `0 11 * * *`. 11:00 is the correct one and the doc is wrong — scheduled
104
+ > `browser-smoke.yml` runs have been observed *starting* as late as 10:14, so a 10:00 triage would
105
+ > regularly find the detector still in progress and correctly no-op. Left at 11:00 deliberately;
106
+ > ops-nightly at 11:55 is the constraint on moving it later.
107
+
108
+ ---
109
+
110
+ ## Routine A — Review on every PR *(stand up first)*
111
+ **Prompt:** [`pr-review.prompt.md`](pr-review.prompt.md) · **Repos:** `<your-org>/<app-repo>`
112
+ + `<api-repo>` (add root `<root-repo>` only if you want Roadmap-doc PRs
113
+ reviewed too).
114
+
115
+ 1. **Install the Claude GitHub App** on `<your-org>/<app-repo>` and `<api-repo>`.
116
+ 2. **Create the routine** from `pr-review.prompt.md`.
117
+ 3. **Trigger:** GitHub → `pull_request`, action **`opened`**, filter **Is draft = `false`**.
118
+ ⚠️ A GitHub trigger takes **one specific action OR all-actions-in-category — you cannot combine
119
+ `opened` + `ready_for_review`** ([docs](https://code.claude.com/docs/en/routines)). Pick **`opened`**:
120
+ it matches directly-opened non-draft PRs (incl. Dependabot), which is how PRs land here. (`opened`
121
+ does **not** fire on a draft→ready flip — `ready_for_review` does; pick that instead only if you
122
+ habitually open drafts first, or pick **all pull_request actions** + the draft filter for full
123
+ coverage, at the cost of also firing on label/sync/close.)
124
+ 4. **Env/connectors:** GitHub App only. No Notion. Network = GitHub. Push left at the `claude/`
125
+ default (the routine only comments).
126
+ 5. **Output:** one advisory review comment per PR (mirrors the `cross-review.prompt.md` rubric — five
127
+ AGENTS rules, single pass). **Comment-only; never a status check.**
128
+
129
+ Revives the descoped `cross-agent-review-always` goal: a cloud session runs *as the product owner*, so it
130
+ sidesteps the CI foreign-CLI auth blocker that forced that epic local-only. (This is the
131
+ **Claude-family** reviewer, not codex/agy — a different second-opinion family, but auto-on-every-PR is
132
+ restored.)
133
+
134
+ ## Routine C — Weekly roadmap/Notion hygiene *(stand up second)*
135
+ **Prompt:** [`roadmap-hygiene.prompt.md`](roadmap-hygiene.prompt.md) · **Repo:** root
136
+ `<root-repo>`.
137
+
138
+ 1. **Install the Claude GitHub App** on `<root-repo>` (root repo).
139
+ 2. **Create the routine** from `roadmap-hygiene.prompt.md`.
140
+ 3. **Trigger:** Schedule, **weekly — Mon 14:00 UTC** (after the 08:00 nightly `notion-sync.yml`).
141
+ 4. **Env/connectors:** **No Notion connector** — no `.mcp.json`, no `NOTION_TOKEN`. The routine grooms
142
+ the `00-ideas` funnel, flags status-drift, runs `node scripts/build-order.mjs`, invokes the
143
+ `doc-hygiene` skill (`node scripts/doc-hygiene.mjs` — always-read-set size + LEARNINGS/poster
144
+ dedupe-staleness candidates), and opens a `claude/` **docs PR** with the regenerated
145
+ `BUILD-ORDER.md` + any new `DOC-HYGIENE-REPORT-*.md` + a drift report. Network = GitHub.
146
+ 5. **Propagation:** after the product owner merges the docs PR, the existing `notion-sync.yml` propagates
147
+ docs→Notion as usual. **Augments, does not replace** that workflow.
148
+
149
+ ## Routine B — Nightly smoke triage *(now-unblocked fast-follow)*
150
+ **Prompt:** [`smoke-triage.prompt.md`](smoke-triage.prompt.md) · **Repo:**
151
+ `<your-org>/<app-repo>` (frontend). **Gate:** `devops-reliability-cleanup` Story 1 (the
152
+ smoke fix) — done, so B is unblocked.
153
+
154
+ 1. **Install the Claude GitHub App** on `<app-repo>` (already done for A).
155
+ 2. **Create the routine** from `smoke-triage.prompt.md`.
156
+ 3. **Trigger:** Schedule, **nightly ~10:00 UTC** (after the `0 9 * * *` `browser-smoke.yml`).
157
+ 4. **Env:** the `TEST_*` secrets (the project's per-role test identities and fixture ids — TEMPLATE FILL-IN) so authed smokes light up; **Allowed-domains:**
158
+ `<PROD_DOMAIN>` + the auth provider's domains + the backend's URL.
159
+ 5. **Output:** on a **green** smoke, nothing. On a **red** smoke, a fix on `claude/smoke-triage-<date>`
160
+ that either **merges and is verified on `main` after deploy**, or stays a **draft** PR plus a
161
+ Telegram ping. **Augments, never replaces** the deterministic smoke, which stays the sole detector.
162
+
163
+ ### B's teeth — the 2026-08-17 change
164
+
165
+ For its first two months B stopped at a draft PR. Nobody merged them, so the nightly stayed red for
166
+ three nights (08-15/16/17) and the 08-16 and 08-17 drafts **independently re-diagnosed the same
167
+ defect from scratch**. A fixer that cannot land its fix re-does its work every night. The product owner's call:
168
+ give it merge authority, bounded.
169
+
170
+ Two independent gates decide, and **both** live in
171
+ [`scripts/smoke-triage-scope.mjs`](../smoke-triage-scope.mjs) (root repo, `node:test`-covered) rather
172
+ than in the prompt — because a merge-to-production predicate must not be a model reading its own diff
173
+ at 10:00 UTC with nobody watching:
174
+
175
+ - **Surface** — every changed path is test scaffolding (`e2e/`, `playwright.config.ts`,
176
+ `browser-smoke.yml`, `scripts/browser-smoke-*`). An allow-list, not a deny-list. **Any** change to
177
+ `app/`, `lib/`, `components/`, database migrations — anything reaching production — stays a draft.
178
+ - **No weakening** — the diff must not make the smoke pass by testing less: no added
179
+ `test.skip`/`fixme`/`only`/serial mode, no deleted spec file, no removed assertion. "Never weaken
180
+ the test" has been in B's prompt since it was written, and a prompt rule is a request; deleting a
181
+ red spec is the cheapest way for any agent to turn a nightly green and it looks like a fix in every
182
+ summary it would write about itself. `test.slow()` is deliberately **allowed** — raising a timeout
183
+ budget is a legitimate shipped fix (#349), and a guard that rejects correct output is worse than one
184
+ that misses a rare fault.
185
+
186
+ Exit codes are three-valued: `0` allow, `1` block, `2` **undecidable** (part of the diff could not be
187
+ read — treated as block, and the ping must say the word). A blocked diff is never discarded or
188
+ trimmed to fit; it becomes exactly the draft PR B used to produce, which is a safe degradation to
189
+ yesterday's behaviour.
190
+
191
+ After merging, B **re-runs the smoke on `main` once the deploy lands** — a branch run tests
192
+ production's old code, and three green branch runs once missed a live PWA-nav regression. If `main`
193
+ goes red, B opens a **draft revert PR** and pings with `REVERT`; it never merges the revert itself and
194
+ never attempts a second fix in the same run.
195
+
196
+ **Still true:** B never merges application code, never marks ready outside a `claude/` branch, never
197
+ becomes a required check, and never weakens a spec.
198
+
199
+ ## Routine ops-nightly — Daily standup + the nightly fixers *(fourth routine, stand up any time after B)*
200
+ **Prompt:** [`ops-nightly.prompt.md`](ops-nightly.prompt.md) · **Repo:** root `<root-repo>`
201
+ (reads/writes `gh` across every repo in `reporting.config.json`).
202
+
203
+ **Sprint 1** shipped step 4 alone (the standup). **Sprint 2**
204
+ (`ops-routines-reporting` (the origin project's `ops-routines-reporting` epic) S2) added
205
+ steps 1–3 — the routine now runs, in order: `build-order-sync` (regen + docs PR on drift),
206
+ `vercel-prune` (dry-run report only — **never** `--apply`), `babysit-pr` (once per open PR across all 3
207
+ repos — silent on a clean PR), then `standup-post` (reports what happened). Still **one** scheduled
208
+ routine (cap-safe) — see the budget table below.
209
+
210
+ 1. **Install the Claude GitHub App** on `<root-repo>` (root repo) if not already done for
211
+ Routine C.
212
+ 2. **Create the routine** from `ops-nightly.prompt.md`.
213
+ 3. **Trigger:** Schedule, **nightly ~10:30 UTC** — after both the frontend's `browser-smoke.yml`
214
+ (`0 9 * * *`) and Routine B (smoke-triage, ~10:00 UTC) have had a chance to complete, so the standup's
215
+ smoke signal reflects the night's actual result.
216
+ 4. **Env:**
217
+ - **`TELEGRAM_BOT_TOKEN`** in the routine's environment — this routine's Telegram use is
218
+ **LOAD-BEARING** (step 4's actual output), not the optional failure-ping the other three routines
219
+ use.
220
+ - **`TELEGRAM_CHAT_ID`** in the routine's environment — **this is the one that actually works for an
221
+ unattended routine run.** The committed `reporting.config.json` carries repos and signals; a chat
222
+ id there works too, but a PUBLIC repo keeps it in the gitignored `reporting.config.local.json`,
223
+ which never exists in a routine's fresh checkout — so `standup.mjs` uses this env var, and it's the same one the optional failure-ping already
224
+ needed, so one setting covers both.
225
+ - **Network access → Custom**, with **`api.telegram.org`** allow-listed (same requirement as the
226
+ other three routines' optional ping — here it's required for step 4 to do anything at all).
227
+ - **⚠️ The `gh` CLI is NOT pre-installed in a routine's cloud sandbox — every step here shells out to
228
+ it, so this is the #1 thing that blocks the whole routine if skipped.** (Confirmed live, 2026-07-02:
229
+ the first two `ops-nightly` runs both failed at step 2 with "no `gh` CLI binary at all" — only the
230
+ built-in GitHub tools were present, and those are read-oriented and scoped to whichever repo the
231
+ routine cloned, not the multi-repo `gh pr list`/`gh run rerun`/`gh pr comment` calls these scripts make.)
232
+ Fix, in the routine's **environment** settings (Edit routine → environment icon → settings gear):
233
+ 1. **Setup script** — add:
234
+ ```bash
235
+ apt update || true
236
+ apt install -y gh
237
+ ```
238
+ **Not** the naive `apt update && apt install -y gh`, which failed live (exit 100, 2026-07-02):
239
+ the base image ships two THIRD-PARTY PPAs pre-configured (`deadsnakes`, `ondrej/php` — unrelated
240
+ to `gh`), and Launchpad returned `403 Forbidden` on both (a shared-sandbox-IP reputation issue,
241
+ not a network-access misconfiguration — the request reached Launchpad's server and got a real
242
+ 403, not a proxy-block). `apt update` treats ANY repo failure as fatal even though the Ubuntu
243
+ archives `gh` actually needs (`noble-updates/universe`, where `gh` ships on 24.04) fetched fine.
244
+ `|| true` makes the update non-fatal so the install step still runs. This runs once and is
245
+ cached (~7-day expiry); it does not re-run every session.
246
+ 2. **`GH_TOKEN`** env var — a GitHub Personal Access Token with access to every configured repo
247
+ (`<root-repo>`, `<app-repo>`, `<api-repo>`). `gh` reads
248
+ `GH_TOKEN` automatically — no `gh auth login` step needed. A **fine-grained PAT** scoped to just
249
+ every configured repo with **Contents: Read & Write**, **Pull requests: Read & Write**, **Actions: Read &
250
+ Write** (Metadata: Read is auto-included) is the least-privilege choice; a classic PAT with the
251
+ `repo` scope also works if simpler. Mint one at
252
+ [github.com/settings/tokens](https://github.com/settings/tokens) (or
253
+ [github.com/settings/personal-access-tokens/new](https://github.com/settings/personal-access-tokens/new)
254
+ for fine-grained).
255
+ 3. `github.com`/`api.github.com` are already in the **Trusted** network-access default — no domain
256
+ change needed there, only the two steps above.
257
+ - **`gh` write scope sufficient for `run rerun`, `pr comment`, and `pr create` on a `claude/`-branch.**
258
+ Steps 1 and 3 write (build-order-sync opens a docs PR; babysit-pr re-runs failed workflow runs and
259
+ posts a PR comment) — both stay inside the routine's **default** `claude/`-prefix push scope (step
260
+ 1's branch is `claude/build-order-sync-<date>`; step 3 never pushes a branch at all, only comments
261
+ + re-runs).
262
+ - **No "Allow unrestricted branch pushes" permission is needed for ANY step, including the standup's
263
+ own log persistence.** `scripts/standup.mjs` (step 4) used to commit `scripts/standups.log` straight
264
+ to `main`, which needed that broader permission — live 2026-07-02/03, its Save button failed
265
+ ("Failed to save changes") in the claude.ai Routines UI. Fixed by moving the log onto a dedicated
266
+ `claude/standup-log` branch instead (`scripts/lib/log-branch.mjs`, git plumbing only — hash-object/
267
+ mktree/commit-tree/push, no checkout), which is already inside the routine's **default** push scope.
268
+ Nothing to configure here anymore; if that Permissions-tab toggle still won't save for you, it no
269
+ longer matters for this routine.
270
+ - **`VERCEL_API_TOKEN`** so step 2's richer per-branch report actually resolves. Not load-bearing —
271
+ `standup.mjs`'s own simpler stale-preview count (step 4) already degrades gracefully to
272
+ "unavailable" without it (confirmed live in S1), same fallback the standup has always had; this is
273
+ purely so step 2's own dry-run report has real numbers to show, rather than an auth error.
274
+ 5. **Output:** one Telegram message per night (the standup) — either the delta lines or a one-line
275
+ "quiet night, no change" post, now with a `Deck standup:` story-deck link appended for mobile
276
+ reading/forwarding — **plus, only when there's something to act on:** a `claude/` docs PR from step 1
277
+ (board was stale) and/or an advisory comment on a PR from step 3 (it had a conflict or a retryable
278
+ failing check). **Never** an `--apply` run, **never** a merge, **never** a required check.
279
+ - ⚠️ **First-live-action gate (owed to the product owner, per the epic's risk-tier rule):** before this routine
280
+ runs unattended on schedule, confirm the first live `vercel-prune --apply` (run only by explicit
281
+ ask, never by this routine) and the first live `babysit-pr` action (a real retry/comment on a real
282
+ PR) each look correct — see
283
+ `sprint-2.md` (the origin project's `ops-routines-reporting` epic)'s walkthrough.
284
+
285
+ ## Routine weekly-recap — Weekly executive recap *(fifth routine, the longer-horizon complement to ops-nightly)*
286
+ **Prompt:** [`weekly-recap.prompt.md`](weekly-recap.prompt.md) · **Repo:** root `<root-repo>`
287
+ (reads `gh` + `git log` across every repo in `reporting.config.json`).
288
+
289
+ Shipped by `ops-routines-reporting` (the origin project's `ops-routines-reporting` epic)
290
+ S3 — a dedicated **weekly** routine (mirroring Routine C's precedent of a standalone weekly schedule,
291
+ rather than a day-of-week-gated step folded into the nightly `ops-nightly` routine). One step: the
292
+ `weekly-recap` skill (`scripts/weekly-recap.mjs`) gathers the week's merged PRs (every configured repo),
293
+ shipped/closed epics (README frontmatter `status:` flips), a merges-to-main deploy count per app repo,
294
+ and a short retro digest per shipped epic — then posts one Telegram message.
295
+
296
+ 1. **Install the Claude GitHub App** on `<root-repo>` (root repo) if not already done for
297
+ Routine C / `ops-nightly`.
298
+ 2. **Create the routine** from `weekly-recap.prompt.md`.
299
+ 3. **Trigger:** Schedule, **weekly — Mon 15:30 UTC** (after Routine C's Mon 14:00 roadmap-hygiene and
300
+ comfortably after the prior night's `ops-nightly` run, so the recap can reflect that week's tail end).
301
+ 4. **Env:**
302
+ - **The `gh` CLI setup script + `GH_TOKEN`** — same requirement as `ops-nightly`'s (see its own env
303
+ section above for the exact setup script and PAT scopes). `weekly-recap.mjs` shells out to
304
+ `gh pr list` across every configured repo, so this is load-bearing here too. If `ops-nightly` and
305
+ `weekly-recap` share the same cloud environment (the usual setup), provisioning it once for
306
+ `ops-nightly` covers this routine too — nothing to redo.
307
+ - **`TELEGRAM_BOT_TOKEN`** in the routine's environment — this routine's Telegram use is
308
+ **LOAD-BEARING** (its one step's actual output), same as `ops-nightly`'s.
309
+ - **`TELEGRAM_CHAT_ID`** in the routine's environment — **this is the one that actually works for an
310
+ unattended routine run** (same reasoning as `ops-nightly`'s: a gitignored local chat id never reaches a
311
+ routine's fresh checkout — `weekly-recap.mjs` uses this env var, the same one the optional failure-ping
312
+ already needed).
313
+ - **Network access → Custom**, with **`api.telegram.org`** allow-listed (same requirement as the
314
+ other routines).
315
+ - **No "Allow unrestricted branch pushes" permission needed** — same as `ops-nightly`'s. The window
316
+ log lives on a dedicated `claude/weekly-recap-log` branch (`scripts/lib/log-branch.mjs`), already
317
+ inside the routine's default `claude/`-prefix push scope, so there's nothing to configure here.
318
+ 5. **Output:** one Telegram message per week — merged PRs, deploys (merge counts), shipped/closed epics,
319
+ and a short retro digest per shipped epic, or a one-line "quiet week" post when there's nothing to
320
+ report. **Never** a PR, **never** a code change, **never** a required check.
321
+
322
+ ## Routine pmo-report — Weekly PMO operational report *(sixth routine, stakeholder report delivery)*
323
+ **Prompt:** [`pmo-report.prompt.md`](pmo-report.prompt.md) · **Repo:** root `<root-repo>`
324
+ (reads `gh` + `git log` across every configured repo; posts to Telegram; writes `claude/pmo-reports-log`).
325
+
326
+ Shipped by `pmo-operational-reports` (the origin project's `pmo-operational-reports` epic)
327
+ S3. One step: the `pmo-report` skill (`scripts/pmo-report.mjs --weekly`) gathers scrum/DORA/doc-ops
328
+ metrics, renders a story-deck URL, posts headline numbers plus the deck link to Telegram, and
329
+ then advances the PMO window log.
330
+
331
+ 1. **Install the Claude GitHub App** on `<root-repo>` if not already done for Routine C /
332
+ `ops-nightly`.
333
+ 2. **Create the routine** from `pmo-report.prompt.md`.
334
+ 3. **Trigger:** Schedule, **weekly — Mon 16:30 UTC** (after roadmap-hygiene and weekly-recap, so the PMO
335
+ deck reflects the current Roadmap/doc state).
336
+ 4. **Env:**
337
+ - **The `gh` CLI setup script + `GH_TOKEN`** — same requirement as `ops-nightly`'s. `pmo-report.mjs`
338
+ reads merged/open PRs across every configured repo through the REST rail.
339
+ - **`TELEGRAM_BOT_TOKEN`** in the routine's environment — load-bearing; the Telegram post is the
340
+ routine's actual output.
341
+ - **`TELEGRAM_CHAT_ID`** in the routine's environment — the unattended path. A local run can put the
342
+ chat id in the gitignored `reporting.config.local.json`; it never reaches a routine session.
343
+ - **Network access -> Custom**, with **`api.telegram.org`** allow-listed.
344
+ - **No unrestricted branch push needed** — the PMO window log lives on `claude/pmo-reports-log`
345
+ through `scripts/lib/log-branch.mjs`.
346
+ 5. **Output:** one Telegram message per week with headline PMO numbers plus a story-deck link.
347
+ **Never** a PR, **never** a merge, **never** a required check.
348
+
349
+ ## Routine prod-smoke — Daily production watchdog *(seventh routine — a REWRITE of the oldest one)*
350
+ **Prompt:** [`prod-smoke.prompt.md`](prod-smoke.prompt.md) · **Repo:** root `<root-repo>`
351
+ (runs `scripts/prod-smoke.mjs` against live prod; posts to Telegram; may open a `claude/` draft PR).
352
+
353
+ **This one replaces an existing routine rather than adding a new load.** The original *"prod
354
+ smoke (daily)"* (trigger id `<trigger-id>`) predates all six routines above and was
355
+ never committed — its six curl checks lived only in the cloud prompt, with no git source. So no epic
356
+ could update them, and two failures followed on 2026-08-05:
357
+
358
+ - **`market-architecture-foundation`** (shipped 07-31) moved `/l` behind a one-hop 308 to `/mx/l`.
359
+ The watchdog went red against a route that had been correct for five days. The epic updated
360
+ `lib/markets.ts` in both repos and the e2e specs — and had no file here to edit.
361
+ - The same cutover turned `/` from the marketplace into the market **selector**. That check kept
362
+ returning 200 and stayed green **while testing a different page**; `/mx` lost coverage entirely
363
+ with no signal at all. A green check that changed meaning is worse than a red one.
364
+
365
+ The checks now live in the project's `scripts/prod-smoke.checks.mjs` (the shared engine is
366
+ [`scripts/prod-smoke.mjs`](../prod-smoke.mjs), with `node:test` coverage), so a
367
+ route change is a reviewable diff and the epic that moves a route updates the smoke in the same PR.
368
+ Because a 200 proves only that *something* answered, every check guarding a rendered page also
369
+ asserts a structural body marker — status alone could not tell the selector and the marketplace apart.
370
+
371
+ > ### ✅ STOOD UP 2026-08-06 — `<trigger-id>`, daily `17 13 * * *` UTC
372
+ >
373
+ > Created via the remote-trigger API (`RemoteTrigger` / `POST /v1/code/triggers`), **not** the web UI,
374
+ > and confirmed with a live manual run against production. Steps 1–3 below are kept as the record of
375
+ > how it was stood up; they are done.
376
+ >
377
+ > **🚨 The ORIGINAL watchdog was NOT replaced, because it could not be found.** It does not appear in
378
+ > `GET /v1/code/triggers` for this account (which lists only `ops-nightly`, `smoke-triage` and
379
+ > `roadmap-hygiene`), and no workflow in any of the project's repos performs these checks. The trigger id
380
+ > recorded in team memory, `<trigger-id>`, returns **404**. So it is running from
381
+ > somewhere this session cannot see or reach.
382
+ >
383
+ > **Until someone finds and disables it, a red night pages TWICE** — once from the old prompt
384
+ > asserting `/l` → 200 (a false alarm; that route legitimately 308s), once from this one. Disabling
385
+ > the old routine is owed, and it is the only remaining step.
386
+
387
+ 1. **Install the Claude GitHub App** on `<root-repo>` if not already done for Routine C /
388
+ `ops-nightly` / `pmo-report`.
389
+ 2. **Create the routine** from `prod-smoke.prompt.md`. (Originally written as "replace the old
390
+ routine's prompt, keep its trigger" — that turned out to be impossible; see the box above.)
391
+ 3. **Trigger:** Schedule, **daily 13:17 UTC** (~07:17 Mexico City) — a first-thing-in-the-morning
392
+ check. It has no dependency on CI or the other routines; it probes live production directly, so
393
+ its hour is not coupled to anything.
394
+ 4. **Env:**
395
+ - **Network access → Custom**, with **`<PROD_DOMAIN>`** allow-listed (the checks) and
396
+ **`api.telegram.org`** (the alert). Without the first, every check reports **unavailable** —
397
+ which the script reports as `UNAVAILABLE`, never as a production outage.
398
+ - **`TELEGRAM_BOT_TOKEN`** + **`TELEGRAM_CHAT_ID`** — load-bearing on a red run; silent on green.
399
+ - Push left at the `claude/` default (it only ever opens a draft PR against the smoke itself).
400
+ 5. **Output:** **nothing on green.** On red, one Telegram alert naming each failing check with
401
+ observed-vs-expected, plus — only when the change is *provably* deliberate — a `claude/` **draft**
402
+ PR re-pointing the check. **Never** a merge, **never** a required check, and **never** a check
403
+ weakened to turn a red run green.
404
+
405
+ ---
406
+
407
+ ## Daily-cap budget (Pro)
408
+
409
+ The **daily routine-run cap (Pro = 5/day) bites the SCHEDULED runs** — GitHub-event and API triggers
410
+ have their **own separate per-routine/per-account hourly caps**, not the scheduled daily cap, and
411
+ **one-off `Run now` runs don't count** at all ([docs](https://code.claude.com/docs/en/routines) ·
412
+ [blog](https://claude.com/blog/introducing-routines-in-claude-code)).
413
+
414
+ | Routine | Trigger | Counts against the 5/day scheduled cap? |
415
+ |---|---|---|
416
+ | A — review-on-PR | GitHub `pull_request.opened` | **No** — GitHub-event, separate hourly caps |
417
+ | C — roadmap hygiene | Schedule, weekly | Yes, but ~0.14/day |
418
+ | B — smoke triage | Schedule, nightly | Yes, 1/day |
419
+ | ops-nightly — standup + nightly fixers | Schedule, nightly | Yes, 1/day (still one routine, now 4 steps) |
420
+ | weekly-recap — weekly exec recap | Schedule, weekly | Yes, but ~0.14/day |
421
+ | pmo-report — weekly PMO deck delivery | Schedule, weekly | Yes, but ~0.14/day |
422
+ | prod-smoke — daily production watchdog | Schedule, daily | Yes, 1/day |
423
+
424
+ - **Scheduled load = B (1/day) + ops-nightly (1/day) + prod-smoke (1/day) + C (~0.14/day) + weekly-recap (~0.14/day) + pmo-report (~0.14/day) ≈ 3.4/day**
425
+ — under the 5/day cap.
426
+ - ⚠️ **That figure assumes the old watchdog is gone, and it is not** (see the box above — it could not
427
+ be located to disable). While both run, the real load is ≈ **4.4/day**: still under the cap, but the
428
+ headroom is one routine, not two. This is a second reason to find and kill the old one, beyond the
429
+ duplicate paging.
430
+ - **A is effectively uncapped for our volume** (GitHub events, hourly caps only); it does **not** eat
431
+ the scheduled budget. On a busy day it's bounded by the preview hourly cap, not the daily 5.
432
+ - **No upgrade pressure:** everything here runs on **Pro**. Higher daily run counts are the only
433
+ routines-relevant Max/Team/Enterprise upsell, and our scheduled load doesn't approach the Pro cap.
434
+
435
+ ## Routine D — Deploy verification: **OUT** (recorded, do not re-litigate)
436
+ Held by decision (in the origin project — re-check it for yours). **Both** deploys already ping Telegram with terminal status — backend (its build
437
+ pipeline's completion notifier) and frontend (`notify-telegram.yml`'s Vercel-poll job: ✅ READY
438
+ / ❌ ERROR / ❌ CANCELED). There is **no notifier gap** to fill, so D's only delta would be a deeper
439
+ go/no-go (run smoke + scan logs after the ping) — thin. **Hold** unless those pings prove too thin; if
440
+ ever revived, it *augments* the existing notifiers and triggers via `/fire` from CD (API triggers
441
+ don't consume the scheduled 5/day cap).
442
+
443
+ ## Run-failure visibility (optional Telegram ping)
444
+
445
+ Routines have **no built-in failure alert** — *"a green status means the session started and exited
446
+ without an infrastructure error. It does not mean the task succeeded"* ([docs](https://code.claude.com/docs/en/routines)).
447
+ Your **actionable output is already visible via GitHub** (A's PR comment, B/C's `claude/` PRs all
448
+ trigger GitHub notifications). The gap is a **run that fails to complete** (network blocked, auth,
449
+ hourly cap) — that shows only on `claude.ai/code/routines` / the transcript unless you check.
450
+
451
+ This section is about that **optional** ping for A/B/C — none of them has a Telegram post as its actual
452
+ output. **ops-nightly, weekly-recap, and pmo-report are the exceptions**: their Telegram post IS the routine's
453
+ output, so their Telegram setup is load-bearing, not optional (see each one's own section above for the
454
+ full env list). They still use this same failure-ping *pattern* for the "couldn't even attempt the
455
+ post" case.
456
+
457
+ To close it without checking the app daily, each prompt has an **optional, best-effort Telegram
458
+ ping-on-failure** step, gated on two env vars being present (so it degrades to a no-op where unset).
459
+ To enable it on a routine:
460
+ 1. Add env vars to the routine's environment: **`TELEGRAM_BOT_TOKEN`** + **`TELEGRAM_CHAT_ID`** (the
461
+ same the project's ops chat bot/chat the deploy notifiers use).
462
+ 2. Set the environment's **Network access → Custom** and add **`api.telegram.org`** to Allowed-domains
463
+ (the Default "Trusted" allowlist does not include it), keeping the default package-manager list.
464
+
465
+ The ping fires **only on a blocking failure** (never on a healthy run — those reach you via GitHub),
466
+ naming the routine + what failed. Skip steps 1–2 to leave a routine silent-except-GitHub.
467
+
468
+ ## Notes
469
+ - **Research preview:** limits/API may change. All three routines are advisory/observability; if the
470
+ feature breaks, the deterministic layers above remain the SSOT and are untouched. The only standing
471
+ discipline: never let a routine become load-bearing.
472
+ - **Smoke walkthrough** for verifying the stand-up:
473
+ `sprint-1.md` (the origin project's `routines-enablement` epic) → *Smoke walkthrough*.
@@ -0,0 +1,125 @@
1
+ <!--
2
+ ops-nightly.prompt.md — Routine ops-nightly (the daily standup + the nightly fixers), the Claude-
3
+ Routines prompt.
4
+
5
+ This is the prompt for a nightly Claude Code *Routine* (cloud session, research preview) on the
6
+ ROOT repo (<root-repo>), running as the product owner ~after the frontend's browser-smoke
7
+ (`0 9 * * *`) and Routine B (smoke-triage, ~10:00 UTC) have both had a chance to complete, so the
8
+ standup's smoke signal reflects the night's actual result. Sprint 1 shipped step 4 alone (the
9
+ standup). Sprint 2 (ops-routines-reporting) adds steps 1–3: regenerate the build-order board on
10
+ drift, report stale Vercel previews (dry-run only), and babysit open PRs (retry flaky CI, surface
11
+ conflicts) — THEN the standup reports what happened. Still ONE scheduled routine (cap-safe).
12
+
13
+ Every step here is advisory/observability or a docs-only PR — none merges, none gates, none is a
14
+ required check, and NOTHING here ever runs a destructive `--apply` (that stays a separate,
15
+ human-confirmed action per the `vercel-prune` skill — this routine only ever runs its dry-run
16
+ report step).
17
+
18
+ Skills below come from the `golden-frijoles` plugin (golden-frijoles marketplace,
19
+ golden-frijoles/skills) — invoke each by name, not
20
+ by a repo-local `skills/<name>/SKILL.md` path (that path no longer exists in this repo).
21
+
22
+ Reuse, don't rebuild:
23
+ - `build-order-sync` skill → scripts/build-order-sync.mjs (check/regen/branch/PR on drift)
24
+ - `vercel-prune` skill → scripts/vercel-prune-previews.mjs (dry-run report only, never --apply)
25
+ - `babysit-pr` skill → scripts/babysit-pr.mjs (one open PR at a time; silent when clean)
26
+ - `standup-post` skill → scripts/standup.mjs (the aggregation, diffing, and actual Telegram
27
+ send — including its own independent CI-red / merge-conflict read, taken AFTER steps 1–3 have run)
28
+ - gh CLI (every repo in reporting.config.json's `repos`), scripts/build-order.mjs --check,
29
+ scripts/vercel-prune-previews.mjs (dry-run, --project <vercelProject> --age <stalePreviewAgeDays>), the configured `smoke` workflow.
30
+
31
+ Stand-up + guardrails: scripts/routines/README.md. Decision: made in the origin project — see this directory's README preamble.
32
+
33
+ The HTML comment above is not part of the prompt; a routine runs everything below the first `---`.
34
+ -->
35
+
36
+ ---
37
+
38
+ You are a nightly **ops-nightly** Claude Code Routine on the root repo (`<root-repo>`),
39
+ running as the product owner. Your job is to run four steps, in order, then stop. Everything you do is
40
+ **advisory only** — you never approve, merge, block, or auto-apply anything, and any code/doc change
41
+ you make lands only as a `claude/`-branch PR for a human to review.
42
+
43
+ ## Step 1 — `build-order-sync`
44
+ Use the `build-order-sync` skill: run `node scripts/build-order-sync.mjs`. If the board was
45
+ stale, it opens a `claude/` docs PR with the regenerated `Roadmap/00-ideas/BUILD-ORDER.md` — nothing
46
+ else to do. If it was already current, no PR — move on.
47
+
48
+ ## Step 2 — `vercel-prune` (dry-run report only)
49
+ Use the `vercel-prune` skill **through Stage 2 only** — the dry-run report. **Never run its
50
+ Stage 3 (`--apply`)** from this routine, under any circumstance; that is a separate, human-initiated
51
+ action gated on the product owner explicitly asking for it in a live conversation, which this unattended nightly
52
+ run structurally cannot be. Note the stale-preview count/list in your own reasoning — no PR, no
53
+ comment; the standup (step 4) will report it independently.
54
+
55
+ ## Step 3 — `babysit-pr` (once per open PR, across every configured repo)
56
+ For each repo in `reporting.config.json`'s `repos`: list open PRs (`gh pr list --repo <repo> --state open --json
57
+ number`), then use the `babysit-pr` skill once per open PR (`node scripts/babysit-pr.mjs <PR#>
58
+ --repo <repo>`). A clean PR gets no comment — that's correct, not a skipped step. Never merge, never
59
+ rebase a conflicting branch, never touch any commit-status/check-run API.
60
+
61
+ ## Step 4 — `standup-post`, written by YOU in three phases
62
+
63
+ The standup is no longer a dump of delta lines: **you write it, as the CPO persona, and a mechanical
64
+ guard checks your draft before it posts.**
65
+
66
+ **You must write the prose yourself, in this session. Do NOT call `devin`, `agy`, `codex`, or
67
+ `prose-draft.mjs`** — none of them is authenticated here, and reaching for one produces no report at
68
+ all rather than an obvious failure. This is the single most important line in this file.
69
+
70
+ Config/secrets first: the committed `reporting.config.json` (repos and signals) plus the chat id — from the
71
+ `TELEGRAM_CHAT_ID` env var in this unattended session (a gitignored `reporting.config.local.json` exists
72
+ only on a local machine, never here). If `TELEGRAM_CHAT_ID` is unset (and nothing is configured), that's
73
+ a hard stop — use the failure ping below instead of guessing; never `AskUserQuestion`, no interactive
74
+ human is present. `TELEGRAM_BOT_TOKEN` must be set.
75
+
76
+ **Phase 1 — get the brief.**
77
+ ```
78
+ node scripts/standup.mjs --brief
79
+ ```
80
+ This prints your persona, the accumulated lessons, and a deterministic evidence pack. It writes
81
+ nothing and sends nothing. **If it returns the "Nothing to report" brief**, do NOT write prose — run
82
+ `node scripts/standup.mjs --post --quiet` instead. That posts the one-line quiet message **and
83
+ advances the log**, which matters: stopping without it leaves the window unadvanced and the next
84
+ run re-counts the same period. A quiet night is a successful run — do not manufacture an update.
85
+
86
+ **Phase 2 — write the prose.** Follow the persona and the task block in the brief exactly. Lead with
87
+ what is now true that was not true before, in product terms. Note the constraints that trip drafts
88
+ most often: never name files, tools or frameworks; never state a deadline or sign-off; never call a
89
+ capability live unless the evidence pack lists its flag as on; and if the work was internal, say so
90
+ plainly rather than inventing a customer benefit. Save your draft to a temp file.
91
+
92
+ **Phase 3 — guard, then post.**
93
+ ```
94
+ node scripts/standup.mjs --post --prose-file <your-file>
95
+ ```
96
+ - **Exit 0** → posted. Done.
97
+ - **Non-zero exit** → it prints a numbered revision note. **Rewrite the draft in full against every
98
+ point**, then re-run once with `--force-post` added. A flagged-but-posted report beats a missing
99
+ one, and the second attempt is labelled so a human can see it did not converge.
100
+ - Do not loop more than that. One revision, then post.
101
+
102
+ The posted message is prose first, then the compact actionable signals (CI red, conflicts, stale
103
+ board), then the `Deck standup:` deck link. Its CI-red and merge-conflict signals are read fresh
104
+ at this point — after steps 1–3 had a chance to fix/flag things — so they reflect current state, not a
105
+ stale pre-run snapshot.
106
+
107
+ ## Nothing else
108
+ No PR beyond what steps 1 and 3 produce as their normal output; no extra comment; no code change of
109
+ your own. **Advisory only — not a gate.** The standup's Telegram post is the routine's user-facing
110
+ output; steps 1–3's PRs/comments (when they exist) are its other outputs. If everything was clean
111
+ (board current, no stale previews worth noting beyond the dry-run count, every open PR clean), that's
112
+ a fully successful, quiet run — do not manufacture an update.
113
+
114
+ ## If the run can't complete (optional failure ping)
115
+ A healthy run reaches the product owner via the standup's Telegram post (plus any PR/comment steps 1–3 produced)
116
+ — no extra notice needed. But a run that **fails to complete** (missing `TELEGRAM_BOT_TOKEN`,
117
+ missing/unconfigured chat id, `gh` unauthenticated, a step erroring out, the log push failing) would
118
+ otherwise be silent. So, **only on a blocking failure**, if **both** `TELEGRAM_BOT_TOKEN` and
119
+ `TELEGRAM_CHAT_ID` are set in the environment, best-effort POST a one-line alert:
120
+ `curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" -d chat_id="$TELEGRAM_CHAT_ID" --data-urlencode text="⚠️ Routine ops-nightly failed at step <N>: <one-line reason>"`
121
+ If either var is unset (or `api.telegram.org` isn't allow-listed), skip it silently — never block on it,
122
+ and **never** ping after a run that completed successfully, even a fully quiet one.
123
+
124
+ Note: `TELEGRAM_CHAT_ID` here (the failure-ping env var, matching the other three routines' convention)
125
+ is also what `standup.mjs` posts the standup to in this session — one variable, both call sites.