@lifeaitools/rdc-skills 0.25.0 → 0.25.2

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.
Files changed (61) hide show
  1. package/.claude-plugin/plugin.json +1560 -1518
  2. package/.github/workflows/self-test.yml +34 -34
  3. package/CHANGELOG.md +322 -310
  4. package/MANIFEST.md +224 -221
  5. package/README.md +379 -377
  6. package/commands/build.md +181 -181
  7. package/commands/collab.md +180 -180
  8. package/commands/deploy.md +148 -148
  9. package/commands/fixit.md +150 -150
  10. package/commands/handoff.md +173 -173
  11. package/commands/overnight.md +220 -220
  12. package/commands/plan.md +158 -158
  13. package/commands/preplan.md +131 -131
  14. package/commands/prototype.md +145 -145
  15. package/commands/report.md +99 -99
  16. package/commands/review.md +120 -120
  17. package/commands/status.md +86 -86
  18. package/commands/workitems.md +127 -127
  19. package/git-sha.json +1 -1
  20. package/guides/agent-bootstrap.md +195 -195
  21. package/guides/agents/backend.md +102 -102
  22. package/guides/agents/content.md +94 -94
  23. package/guides/agents/cs2.md +56 -56
  24. package/guides/agents/data.md +86 -86
  25. package/guides/agents/design.md +77 -77
  26. package/guides/agents/frontend.md +91 -91
  27. package/guides/agents/infrastructure.md +81 -81
  28. package/guides/agents/setup.md +272 -272
  29. package/guides/agents/verify.md +119 -119
  30. package/guides/agents/viz.md +106 -106
  31. package/package.json +57 -57
  32. package/scripts/install-rdc-skills.js +1401 -1289
  33. package/scripts/self-test.mjs +1460 -1460
  34. package/scripts/validate-publish-manifests.js +502 -502
  35. package/skills/build/SKILL.md +574 -559
  36. package/skills/channel-formatter/SKILL.md +538 -538
  37. package/skills/collab/SKILL.md +239 -239
  38. package/skills/convert/SKILL.md +167 -167
  39. package/skills/deploy/SKILL.md +541 -541
  40. package/skills/design/SKILL.md +205 -205
  41. package/skills/env/SKILL.md +141 -0
  42. package/skills/fixit/SKILL.md +203 -203
  43. package/skills/fs-mcp/SKILL.md +37 -2
  44. package/skills/handoff/SKILL.md +236 -236
  45. package/skills/new-model/SKILL.md +49 -0
  46. package/skills/onramp/SKILL.md +1459 -248
  47. package/skills/overnight/SKILL.md +251 -251
  48. package/skills/plan/SKILL.md +345 -345
  49. package/skills/preplan/SKILL.md +90 -90
  50. package/skills/prototype/SKILL.md +150 -150
  51. package/skills/regen-media/SKILL.md +94 -0
  52. package/skills/release/SKILL.md +140 -140
  53. package/skills/report/SKILL.md +100 -100
  54. package/skills/review/SKILL.md +159 -151
  55. package/skills/self-test/SKILL.md +108 -108
  56. package/skills/status/SKILL.md +99 -99
  57. package/skills/tests/MATRIX.md +55 -54
  58. package/skills/tests/onramp.test.json +87 -87
  59. package/skills/tests/rdc-regen-media.test.json +29 -0
  60. package/skills/watch/SKILL.md +84 -84
  61. package/skills/workitems/SKILL.md +151 -151
@@ -1,541 +1,541 @@
1
- ---
2
- name: rdc:deploy
3
- description: "Usage `rdc:deploy <slug> [new|diagnose|audit|promote|convert] [--fix|--hotfix <sha>]` — Deploy an app to Coolify (production) or PM2 (staging), promote a verified dev change to production (one-command cherry-pick → PR → admin-merge → explicit Coolify trigger → verify), convert a prod app's runtime in place static→Next, add a new Coolify app, diagnose a failed deploy, or audit watch paths. Handles DNS, branch protection, health checks, and post-deploy verification."
4
- ---
5
-
6
- > **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
7
- > Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
8
- > One checklist upfront, updated in place, shown again at end with a 1-line verdict.
9
-
10
-
11
- # rdc:deploy — Coolify Operations
12
-
13
- **READ FIRST:** `guides/output-contract.md`. Checklist-only output. No narration.
14
- No raw MCP dumps. No UUIDs unless asked.
15
-
16
- > **Sandbox contract:** This skill honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
17
- >
18
- > *Under `$RDC_TEST=1`:* Modes 1 (deploy), 2 (new), and 5 (promote) are **entirely skipped** — echo `[RDC_TEST] skipping Coolify deploy/create/promote` and mark every `[ ]` line in those checklists as `[~]`. Modes 3 (diagnose) and 4 (audit without `--fix`) are **read-only and run normally**. Mode 4 with `--fix` skips all remediation — echo `[RDC_TEST] skipping audit --fix remediation` and report findings only. Registry SELECTs, Coolify status reads, HTTP gate probes, TLS checks, and DNS lookups are NOT destructive and run normally. Anything that writes (create app, set watch_paths, deploy trigger, **PR/admin-merge to main**, env var write, DNS write, CF cache purge, registry UPDATE/INSERT) is gated.
19
-
20
- ## When to Use
21
- - Project lead says "deploy", "ship it", "push to production", "update the server"
22
- - Project lead says "promote", "patch prod", "hotfix production", "push this fix live" — use `rdc:deploy <slug> promote`
23
- - Dev is a Next.js app but prod is still a static site for the same slug ("dev is XJS, prod is static", "make prod Next") — use `rdc:deploy <slug> convert` (Mode 6)
24
- - A new app needs to be registered and deployed for the first time (`rdc:deploy new`)
25
- - A deployed app is behaving unexpectedly and needs diagnosis (`rdc:deploy diagnose`)
26
- - Running a compliance/health audit of all deployed apps (`rdc:deploy audit`)
27
-
28
- ## Arguments
29
-
30
- - `rdc:deploy <slug>` — deploy existing app (latest commit on its watched branch)
31
- - `rdc:deploy <slug> <build-id>` — deploy specific commit/tag
32
- - `rdc:deploy <slug> promote` — promote the verified `develop` change for this app to production (Mode 5)
33
- - `rdc:deploy <slug> promote --hotfix <sha>` — promote a specific commit (cherry-pick just that sha to `main`)
34
- - `rdc:deploy <slug> convert` — convert a prod app's runtime in place from static→Next (Mode 6); use when dev is Next but prod is still a static Coolify build
35
- - `rdc:deploy new <slug>` — create a new Coolify app from registry
36
- - `rdc:deploy diagnose <slug>` — debug why an app is broken
37
- - `rdc:deploy audit` — fleet-wide scan for missed failures
38
- - `rdc:deploy audit --fix` — fleet scan + auto-remediate safe issues
39
- - `rdc:deploy` (no args) — print mode menu, ask which
40
-
41
- ## Modes
42
-
43
- ### Mode 1 — deploy <slug> [build-id]
44
-
45
- ```
46
- rdc:deploy: <slug> → <domain>
47
- [ ] Registry lookup (slug, uuid, branch, type, env_vars_needed)
48
- [ ] Runtime-source guard: if the source being deployed is static (`sites/<name>`, nixpacks/static) but the registry `runtime` for <slug> is `next`, BLOCK — never deploy a flat/static source over a Next app. A static prototype deploys only under its OWN slug (never an existing Next app's slug), dev only. Changing a slug's runtime is architectural. (`.claude/rules/production-stack-nextjs.md`)
49
- [ ] Git state verified (branch matches Coolify, commit pushed)
50
- [ ] Build-id resolved (default: HEAD of watched branch)
51
- [ ] Env vars present in Coolify (compare to registry)
52
- [ ] Type-specific preflight (see docs/runbooks/coolify-deploy-checklist.md)
53
- [ ] Mandatory pre-deploy code-review (pr-review-toolkit:code-reviewer on `git diff <last-deployed-sha>..HEAD` for this app's paths). Block deploy on `critical`/`high` findings; record `medium`/`low` and proceed.
54
- [ ] PUBLISH.md read from app root (warn if absent; fail if present but invalid)
55
- [ ] watch_paths derived from PUBLISH.md surfaces (union of all surface watch_paths arrays) and updated in app_deployments
56
- [ ] Deploy triggered
57
- [ ] Deployment reached "finished" state
58
- [ ] Gate: HTTP 200
59
- [ ] Gate: TLS valid (no SSL cipher mismatch)
60
- [ ] Gate: cache headers correct on HTML
61
- [ ] Gate: container running on declared port
62
- [ ] Gate: metadata audit (see § Metadata Audit below) — warn on gaps, do not block deploy
63
- [ ] Cloudflare cache purged (if proxied)
64
- [ ] artifact_registry INSERT per PUBLISH.md surface (if PUBLISH.md present)
65
- [ ] deployment_registry updated (last_deploy_at, status)
66
- ✅ rdc:deploy: <slug> deployed in Nm Ns
67
- ```
68
-
69
- #### Static PM2 dev sites — do NOT trust the push webhook (SSH-reset + served-hash gate)
70
-
71
- For a **static** PM2 dev site, `git push` succeeding does NOT mean the live site
72
- updated: the push webhook skips/instruments static apps unreliably, so the host
73
- working tree (and the committed `dist/` it serves) can stay STUCK across several
74
- pushes while HTTP stays 200 and `origin/develop` looks shipped (lesson
75
- 2026-06-11-deploy-static-host-stuck: issho served the v1.10.0 bundle across three
76
- pushes because `/srv/regen/regen-root` never pulled). The only signal is the
77
- SERVED bundle hash vs the local `dist/` hash.
78
-
79
- After pushing committed `dist/`, SSH-reset the host explicitly, then verify the
80
- served hash:
81
- ```bash
82
- _K=$(mktemp); curl -s http://127.0.0.1:52437/v/vultr-dev-ssh > "$_K"; printf '\n' >> "$_K"; chmod 600 "$_K"
83
- ssh -i "$_K" root@64.237.54.189 'cd /srv/regen/regen-root && git fetch -q origin develop && git reset -q --hard origin/develop && pm2 restart <app> --update-env'
84
- rm -f "$_K"
85
- # Then verify SERVED hash == local build hash (HTTP 200 is NOT proof):
86
- curl -s https://<app>.dev.place.fund/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' # served
87
- grep -oE 'index-[A-Za-z0-9_-]+\.js' sites/<app>/dist/index.html # local
88
- ```
89
- Only when the two hashes match do the content/screenshot gates mean anything.
90
-
91
- ### Mode 2 — new <slug>
92
-
93
- **MANDATORY:** All new apps are created from `docs/runbooks/coolify-app-templates.json`. Read that file first — pick the right template, substitute the required vars, POST the exact payload. No manual field configuration. No improvisation. The template encodes all learned lessons (base_directory, build_pack, watch_paths, health_check, ports). Deviating from it breaks things.
94
-
95
- Template selection:
96
- - `nextjs-app` — for `apps/<name>/` (Dockerfile, turbo filter, port 3000)
97
- - `static-site` — for `sites/<name>/` (nixpacks, publish_directory=out, no packages)
98
- - `mcp-server` — for `mcp-servers/<name>/` (Dockerfile, health check enabled, custom port)
99
-
100
- ```
101
- rdc:deploy new: <slug>
102
- [ ] .dockerignore present at project root (`ls {PROJECT_ROOT}/.dockerignore` — STOP if missing)
103
- [ ] Template loaded from docs/runbooks/coolify-app-templates.json (pick nextjs-app / static-site / mcp-server)
104
- [ ] Required vars substituted: NAME, APP_PATH, DOMAIN, BRANCH, PROJECT_UUID, ENVIRONMENT_UUID [+ TURBO_FILTER / PORT]
105
- [ ] DNS path chosen (A: staging wildcard B: apex C: other zone)
106
- [ ] DNS record verified or wildcard confirmed
107
- [ ] Cloudflare proxy setting correct for DNS path
108
- [ ] Application created via POST /applications/private-github-app (template payload)
109
- [ ] UUID recorded from response
110
- [ ] watch_paths verified via GET /api/v1/applications/<uuid> — must match template
111
- [ ] Env vars set in Coolify (from deployment_registry.env_vars_needed)
112
- [ ] First deploy triggered
113
- [ ] Deployment reached "finished" state
114
- [ ] Gate: HTTP 200 on <domain>
115
- [ ] Gate: TLS valid
116
- [ ] deployment_registry row inserted
117
- ✅ rdc:deploy new: <slug> live at <domain>
118
- ```
119
-
120
- ### Mode 3 — diagnose <slug>
121
-
122
- ```
123
- rdc:deploy diagnose: <slug>
124
- [ ] App located (uuid, domain, last deploy)
125
- [ ] Container state (running / restarting / stopped)
126
- [ ] Last 100 log lines scanned for known error patterns
127
- [ ] Port mismatch check (declared vs actual)
128
- [ ] Env var drift check (registry vs Coolify)
129
- [ ] watch_paths sanity check
130
- [ ] HTTP / TLS reachability
131
- [ ] Cloudflare proxy state check
132
- [ ] Disk space on server
133
- [ ] Branch mismatch check (Coolify git_branch vs expected)
134
- ⚠️ rdc:deploy diagnose: <root cause in one sentence> — fix: <one command>
135
- ```
136
-
137
- #### `next start` crash-loop — check `.next/BUILD_ID` FIRST
138
-
139
- When a PM2 `next start` app is crash-looping (`pm2 jlist` shows high `restart_time`
140
- / "waiting restart"), check **`.next/BUILD_ID`** before chasing env/port theories —
141
- it is the single gate `next start` enforces (`Could not find a production build in
142
- the '.next' directory` repeats for every restart when it is missing). A `.next`
143
- tree can exist as a PARTIAL build (manifests + server artifacts but no `BUILD_ID`)
144
- — the signature of a `next build` that started but was interrupted/OOM-killed; the
145
- crash loop then churns CPU and can re-trigger the OOM (lesson
146
- 2026-06-13-deploy-nextstart-missing-buildid: ~7300 restarts, all `BUILD_ID`-missing,
147
- while the documented prior lead was the NEXT_PUBLIC env issue — which was NOT the
148
- cause here). Fix sequence:
149
- ```bash
150
- pm2 stop <app> # halt the loop so it stops competing for resources
151
- rm -rf apps/<name>/.next # clear the partial tree
152
- pnpm --filter @regen/<name> build # ONE clean scoped build (LOCAL BUILD SAFETY)
153
- pm2 restart <app> --update-env
154
- ```
155
- Guard: assert `apps/<name>/.next/BUILD_ID` exists before (re)starting any
156
- `next start` process.
157
-
158
- #### ⛔ On the 2nd IDENTICAL failed probe, STOP polling and diagnose full-state
159
-
160
- A repeated identical failure (same 404, same 502, same non-200) is a STRUCTURAL
161
- signal, not eventual-consistency — polling it will never clear it and reads to the
162
- user as stalling/throttling (lesson 2026-06-10-deploy-stop-polling-diagnose: a
163
- 12×-404 poll loop on a remote-managed tunnel that was structurally ignoring the
164
- local config — it could never clear). On the **2nd identical failed check**, stop
165
- polling and pull GROUND TRUTH instead: list ALL processes / the whole effective
166
- remote config / the full event log (`debugging-protocol.md` Rule 6 — full-state, not
167
- a narrow filter). Poll ONLY when something is genuinely in-progress with a known
168
- finish (a deploy build, a DNS first-create) — never to hope a structural error
169
- resolves itself.
170
-
171
- ### Mode 4 — audit
172
-
173
- ```
174
- rdc:deploy audit: fleet scan
175
- [ ] Inventory join: Coolify apps ⋈ deployment_registry
176
- [ ] Orphans (in one but not the other)
177
- [ ] Monorepo apps missing watch_paths
178
- [ ] Stale deploys (>14 days since last success)
179
- [ ] Registry rows with status='broken'
180
- [ ] Failed deployments in last 7 days
181
- [ ] HTTP gate sweep (non-200 per domain)
182
- [ ] TLS cert expiry <30 days
183
- [ ] Port mismatches (ports_exposes vs actual container port)
184
- [ ] Env var drift (registry.env_vars_needed vs Coolify env)
185
- [ ] Branch mismatches (Coolify git_branch ≠ expected)
186
- [ ] Disk space on 64.237.54.189
187
- [ ] DNS/proxy misconfigs on configured staging wildcard
188
- [ ] Duplicate apps (same repo, multiple UUIDs)
189
-
190
- Findings:
191
- | Severity | App | Issue | Fix |
192
- |----------|-----|-------|-----|
193
- | HIGH | ... | ... | ... |
194
- ⚠️ rdc:deploy audit: N HIGH · M MED · K LOW — run `rdc:deploy audit --fix` to auto-remediate safe issues
195
- ```
196
-
197
- Severity rules:
198
- - **HIGH** — user-facing down (HTTP non-200, TLS invalid, container not running)
199
- - **MED** — degraded or drifting (watch_paths missing, env var drift, stale deploy, branch mismatch)
200
- - **LOW** — cleanup (orphans, duplicates, registry status stale)
201
-
202
- `--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
203
-
204
- ### Mode 5 — promote <slug> [--hotfix <sha>]
205
-
206
- Promote a **verified `develop` change** for one app to production. This is the sanctioned production-patch fast path — one command instead of fighting branch protection, the main-push hook, and a flaky Coolify webhook by hand.
207
-
208
- **Authorization:** production promote requires explicit user go-ahead ("promote", "patch prod", "push live", "go"). A dev deploy does NOT. If the user has not given it for THIS promote, stop and ask first.
209
-
210
- ```
211
- rdc:deploy promote: <slug> → <prod-domain>
212
- [ ] Registry lookup: PROD row (uuid, prod branch=main, watch_paths, url)
213
- [ ] PUBLISH.md status gate: status=active AND prod in environments (block if not)
214
- [ ] Runtime-source guard: BLOCK promoting a static source to a prod row whose intended `runtime` is `next` — production apps are Next.js (DB-backed). (`.claude/rules/production-stack-nextjs.md`)
215
- [ ] Scope resolved: --hotfix <sha> → that commit; else the app-path commits on develop not on main
216
- [ ] Scope guard: promote ONLY this app's paths. NEVER merge develop→main wholesale (drags unrelated WIP to prod)
217
- [ ] Clean worktree off origin/main (never switch the dirty working tree)
218
- [ ] Export Supabase creds in the worktree BEFORE commit (the pre-commit `sync:docs` hook needs them, and a fresh worktree does NOT inherit the main checkout's `.env*`/shell env — without the key sync:docs silently regenerates a GUTTED `app-deployments.md` ("Registry: unavailable") and `git add`s it into the surgical promote; `--no-verify` is forbidden):
219
- `export NEXT_PUBLIC_SUPABASE_URL=https://uvojezuorjgqzmhhgluu.supabase.co`
220
- `export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(curl -s http://127.0.0.1:52437/v/supabase-anon)`
221
- Then ALWAYS `git show --stat HEAD` before pushing and confirm the file set is scoped — never ship the no-key gutted app-deployments.md. (lesson 2026-06-13-deploy-worktree-syncdocs-guts-app-deployments)
222
- [ ] Apply change: cherry-pick <sha> (or `git checkout <develop-sha> -- <app-paths>`); confirm diff = expected files only
223
- [ ] Mandatory pre-promote code-review (pr-review-toolkit:code-reviewer on the promote diff). Block on critical/high.
224
- [ ] Commit on promote branch; push branch (NOT main directly — the main-push hook blocks raw main pushes)
225
- [ ] Open PR base=main; merge with admin override (`gh pr merge --squash --admin --delete-branch`) — branch protection needs --admin
226
- [ ] EXPLICITLY trigger Coolify deploy — NEVER rely on the GitHub→Coolify webhook (it silently no-ops on some merges even with auto-deploy ON)
227
- [ ] Deployment reached "finished" state (poll coolify_events / deployment status)
228
- [ ] Gate: HTTP 200 on prod domain
229
- [ ] Gate: content-level check — assert the actual changed string is live (200 alone is NOT proof; origin may serve stale)
230
- [ ] Gate: TLS valid
231
- [ ] Gate: metadata audit (see § Metadata Audit below) — BLOCK promote on any missing required item
232
- [ ] Cloudflare cache purged (if proxied)
233
- [ ] deployment_registry updated (last_deploy_at, last_deploy_commit, status)
234
- ✅ rdc:deploy promote: <slug> live in prod — <changed-string> verified
235
- ```
236
-
237
- **The explicit Coolify trigger (the whole point — do not skip):**
238
- ```bash
239
- _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
240
- # Correct endpoint is GET /api/v1/deploy?uuid= — NOT POST /applications/<uuid>/deploy (that 404s)
241
- curl -s -H "Authorization: Bearer $_COOLIFY" \
242
- "$DEPLOY_API_BASE/api/v1/deploy?uuid=<PROD_UUID>&force=true"
243
- # → {"deployments":[{"deployment_uuid":"...","message":"...deployment queued."}]}
244
- ```
245
-
246
- **Why each guard exists (lessons from 2026-06-05 EF Hooper promote):**
247
- - `main` branch protection rejects PR merge without `--admin`; a raw `git push …:main` is blocked by the main-push hook → must go branch → PR → admin-merge.
248
- - Coolify auto-deploy was **ON** for the app yet the merge did **not** auto-deploy — a webhook-delivery flake. A promote must ALWAYS trigger the deploy explicitly and verify; never hope the webhook fired.
249
- - `develop` was 87 commits ahead of `main` (unrelated apps' WIP). Promoting must be surgical (this app's paths / one sha), never a develop→main merge.
250
- - HTTP 200 was returned by the stale origin the whole time — only a content-level assertion (`curl … | grep '<new string>'`) proves the promote landed.
251
-
252
- ### Mode 6 — convert <slug> (prod runtime static→Next, in place)
253
-
254
- One-time conversion of an existing **production** Coolify app from a static/nixpacks build to the Next.js (dockerfile) runtime, **in place** — same UUID, same fqdn, same DNS. After this, the slug is a normal Next app and every future ship is just `rdc:deploy <slug> promote`. This is the mode for "dev is Next, prod is still static" (the `vlas.earth` / `life.ai` class). `promote` alone CANNOT do this — it re-triggers the prod app's existing (static) build; only `convert` changes the build config.
255
-
256
- **Authorization:** this changes a production app's runtime — **architectural** per `.claude/rules/production-stack-nextjs.md` + `.claude/rules/architectural-change-approval.md`. Requires explicit user go-ahead for THIS slug. Stop and ask if not given.
257
-
258
- **Why in-place PATCH, not recreate:** keeps the application UUID, fqdn (apex + `www`), DNS binding, and project — zero DNS cutover, zero window where the domain is unbound. PATCH switches the build pack; the next deploy builds the Next app on the same application object.
259
-
260
- ```
261
- rdc:deploy convert: <slug> → <prod-domain> (static→Next, in place)
262
- [ ] Go-ahead confirmed for THIS slug (production runtime change — architectural)
263
- [ ] Registry lookup: PROD coolify_uuid, fqdn, project/env, current build_pack
264
- [ ] Confirm it IS a convert: dev runtime=next AND prod build_pack ∈ {static, nixpacks}. If prod build_pack already `dockerfile` → already converted, fall through to Mode 5 promote.
265
- [ ] Source Next-prod-readiness preflight (BLOCK on any miss):
266
- - apps/<name>/Dockerfile present (prod build) — BLOCK "source not Next-prod-ready: add Dockerfile" if missing
267
- - start/listen port reconciled with intended ports_exposes (no dev-only port like :3214 leaking to prod)
268
- - PUBLISH.md present, build_type=nextjs, status=active, prod in environments
269
- [ ] Mandatory pre-convert code-review (pr-review-toolkit:code-reviewer on the apps/<name> diff being promoted). Block on critical/high.
270
- [ ] Promote app code to main (Mode 5 path): clean worktree off origin/main → checkout apps/<name> paths (or cherry-pick) → confirm diff = expected files only → branch → PR base=main → `gh pr merge --squash --admin --delete-branch`
271
- [ ] LOCKFILE IMPORTER (mandatory for an app NEW to main): if `git show origin/main:pnpm-lock.yaml | grep -c "apps/<name>:"` is 0, the Docker `pnpm install --frozen-lockfile` WILL fail. In the clean main worktree run `pnpm install --lockfile-only` (adds only the apps/<name> importer; verify the diff is minimal — do NOT promote develop's whole lockfile, it carries unrelated drift) and include `pnpm-lock.yaml` in the SAME promote PR.
272
- [ ] In-place PATCH prod Coolify app <uuid> to nextjs-app template fields:
273
- build_pack=dockerfile · dockerfile_location=/apps/<name>/Dockerfile · base_directory=/ ·
274
- build_command="pnpm turbo run build --filter=<pkg>" · start_command="pnpm --filter=<pkg> start" ·
275
- install_command="pnpm install --frozen-lockfile" · ports_exposes=<port> ·
276
- watch_paths="apps/<name>/**\npackages/**"
277
- (clear publish_directory; static-only field)
278
- [ ] PROXY LABELS (mandatory — the static-app `custom_labels` are read-only and do NOT auto-update on a build_pack PATCH): fetch `custom_labels`, base64-decode, rewrite `loadbalancer.server.port=80`→`=<port>` (all routers) AND `upstreams 80}}`→`upstreams <port>}}`, and STRIP the static `caddy_*.try_files=…/index.html /index.php` lines (wrong for a Next app), re-base64, PATCH `custom_labels`. Skipping this = container serves on :<port> but the proxy still routes :80 → **502** even though the build "finished" and the app logs "Ready". (Note: the API rejects `is_container_label_readonly_enabled` and a non-base64 `custom_labels` — you must hand-rewrite the base64.)
279
- [ ] Env vars present in Coolify (compare registry.env_vars_needed); set any missing
280
- [ ] EXPLICITLY trigger deploy: GET /api/v1/deploy?uuid=<PROD_UUID>&force=true — never rely on the webhook
281
- [ ] Deployment reached "finished" state (poll coolify_events)
282
- [ ] Gate: SSR proof — prod HTML now contains `/_next/static` (it is the Next app, not the old static build)
283
- [ ] Gate: HTTP 200 + content-level assertion of a known string from the Next render
284
- [ ] Gate: TLS valid; cache headers correct on HTML
285
- [ ] Gate: metadata audit (see § Metadata Audit) — BLOCK on any required gap
286
- [ ] Cloudflare cache purged (apex + www)
287
- [ ] app_deployments updated: runtime intent → next, notes (converted static→Next <date>), last_deploy_at, last_deploy_commit, status=active
288
- [ ] `apps` row runtime=next confirmed
289
- ✅ rdc:deploy convert: <slug> now Next.js in prod — SSR verified at <prod-domain>
290
- ```
291
-
292
- **Rollback:** PATCH the app back to static (`build_pack=static`, `base_directory=/sites/<name>`, `publish_directory=/sites/<name>`, `ports_exposes=80`, `watch_paths=sites/<name>/**`) and redeploy. `sites/<name>` stays in the repo precisely so this rollback is always available.
293
-
294
- **PATCH command (in-place build-pack switch):**
295
- ```bash
296
- _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
297
- curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" -H "Content-Type: application/json" \
298
- -d '{"build_pack":"dockerfile","dockerfile_location":"/apps/<name>/Dockerfile","base_directory":"/","build_command":"pnpm turbo run build --filter=<pkg>","start_command":"pnpm --filter=<pkg> start","install_command":"pnpm install --frozen-lockfile","ports_exposes":"<port>","watch_paths":"apps/<name>/**\npackages/**"}' \
299
- "$DEPLOY_API_BASE/api/v1/applications/<PROD_UUID>"
300
- ```
301
-
302
- ## Metadata Audit
303
-
304
- After a successful deploy or promote, audit the live site's `<head>` metadata. Run this against the deployed URL (not a local file).
305
-
306
- **Check list** (curl the deployed URL, parse the HTML `<head>`):
307
-
308
- | # | Item | How to check | Required | Severity |
309
- |---|------|-------------|----------|----------|
310
- | 1 | `<title>` | grep `<title>` — non-empty, not default/placeholder | yes | block |
311
- | 2 | `<meta name="description">` | content attr ≥ 50 chars, ≤ 160 chars | yes | block on promote; warn on deploy |
312
- | 3 | `<link rel="canonical">` | href present, starts with `https://`, matches the deployed domain | yes | block on promote; warn on deploy |
313
- | 4 | `og:title` | `<meta property="og:title">` present | yes | block on promote; warn on deploy |
314
- | 5 | `og:description` | `<meta property="og:description">` present, ≥ 50 chars | yes | block on promote; warn on deploy |
315
- | 6 | `og:image` | `<meta property="og:image">` present, URL returns HTTP 200 | yes | block on promote; warn on deploy |
316
- | 7 | `og:url` | present, matches canonical | recommended | warn |
317
- | 8 | `twitter:card` | `summary_large_image` or `summary` | recommended | warn |
318
- | 9 | `twitter:image` | present, URL returns HTTP 200 | recommended | warn |
319
- | 10 | Favicon | `<link rel="icon">` present, href returns HTTP 200 | yes | block on promote; warn on deploy |
320
- | 11 | `sitemap.xml` | `<domain>/sitemap.xml` returns HTTP 200 | recommended | warn |
321
- | 12 | `robots.txt` | `<domain>/robots.txt` returns HTTP 200 | recommended | warn |
322
- | 13 | Version | `<meta name="version">` or visible version string in page | yes (project rule) | warn |
323
-
324
- **Behavior by mode:**
325
- - **Mode 1 (deploy):** run the audit after HTTP/TLS gates. Print a table of results. **Warn** on gaps but do NOT block the deploy — dev deploys are iterative.
326
- - **Mode 5 (promote):** run the audit after content-level check. **Block the promote** if any item marked "block on promote" is missing. Print the table and require the metadata to be fixed before re-attempting.
327
- - **Mode 4 (audit):** include the metadata sweep in the fleet scan (one row per app per missing item in the findings table).
328
-
329
- **Implementation — one-liner per check:**
330
- ```bash
331
- URL="https://<domain>"
332
- HEAD=$(curl -s "$URL" | sed -n '/<head/,/<\/head>/p')
333
- echo "$HEAD" | grep -ioE '<title>[^<]+</title>'
334
- echo "$HEAD" | grep -ioE 'name="description"[^>]*content="[^"]*"'
335
- echo "$HEAD" | grep -ioE 'rel="canonical"[^>]*href="[^"]*"'
336
- echo "$HEAD" | grep -ioE 'property="og:title"[^>]*content="[^"]*"'
337
- echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="[^"]*"'
338
- echo "$HEAD" | grep -ioE 'name="twitter:card"[^>]*content="[^"]*"'
339
- echo "$HEAD" | grep -ioE 'rel="icon"[^>]*href="[^"]*"'
340
- echo "$HEAD" | grep -ioE 'name="version"[^>]*content="[^"]*"'
341
- curl -s -o /dev/null -w "%{http_code}" "$URL/sitemap.xml"
342
- curl -s -o /dev/null -w "%{http_code}" "$URL/robots.txt"
343
- OG_IMG=$(echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="([^"]*)"' | grep -ioE 'https://[^"]*')
344
- [ -n "$OG_IMG" ] && curl -s -o /dev/null -w "%{http_code}" "$OG_IMG"
345
- ```
346
-
347
- **Why this exists (2026-06-05):** life.ai deployed to production with zero social/SEO metadata — no description, no OG, no favicon, no sitemap. HTTP 200 + TLS + content passed; metadata was invisible to the existing gates. This audit catches that class of defect.
348
-
349
- ## Hotlink referer allowlist — new media.place.fund consumer domains
350
-
351
- When onboarding a NEW brand domain that loads assets from `media.place.fund`, add
352
- that domain to the hotlink-protection Worker's referer allowlist **before
353
- go-live** (lesson 2026-06-14-deploy-issholiving-referer-hotlink-403: a new brand
354
- domain served its own HTML fine but every `media.place.fund` image returned 403
355
- because the hotlink Worker rejected the unknown `Referer`). The image 403 is
356
- invisible to the HTTP-200 / TLS / metadata gates — they probe the HTML document,
357
- not the cross-origin asset.
358
-
359
- - Add the new origin (apex + `www`, dev + prod) to the Worker's referer allowlist
360
- and redeploy the Worker before the brand domain goes live.
361
- - Verify with an explicit `Referer` probe (NOT a bare curl — a missing Referer can
362
- pass while the real browser Referer fails):
363
- ```bash
364
- curl -s -o /dev/null -w "%{http_code}" -e "https://<new-brand-domain>/" \
365
- "https://media.place.fund/<a-known-asset-path>" # expect 200, not 403
366
- ```
367
-
368
- ## PUBLISH.md Integration
369
-
370
- Every deploy reads `PUBLISH.md` from the app's source root to derive `watch_paths` and to register surfaces in Studio `artifact_registry`.
371
-
372
- ### Step 6 — Read PUBLISH.md from the app root
373
-
374
- ```bash
375
- MONOREPO_PATH=$(get_app_deployments_monorepo_path "$SLUG")
376
- PUBLISH_MD="$MONOREPO_PATH/PUBLISH.md"
377
-
378
- if [ ! -f "$PUBLISH_MD" ]; then
379
- echo "WARN: PUBLISH.md missing for $SLUG — using app_deployments.watch_paths only"
380
- # Deploy continues; watch_paths derivation and artifact_registry INSERT are skipped
381
- fi
382
- ```
383
-
384
- PUBLISH.md format: see `C:/Dev/rdc-skills/guides/publish-md-spec.md` (authoritative).
385
-
386
- Required frontmatter fields: `schema_version`, `entity_slug`, `artifact_type`, `environments`, `status`.
387
- One or more `<!-- SURFACE:<id> -->` … `<!-- /SURFACE:<id> -->` blocks per surface (each with `path`, `source_dir`, `build_type`, `visibility`, `cache`, `watch_paths`).
388
-
389
- If PUBLISH.md is **present but invalid** (missing required field, bad enum, no surface blocks): abort deploy with `BLOCKED: PUBLISH.md parse error for <slug> — <reason>`.
390
-
391
- ### Step 7 — Derive watch_paths from PUBLISH.md surfaces
392
-
393
- Union all `watch_paths` arrays across every surface section in PUBLISH.md. Update `app_deployments.watch_paths` for the app slug to this derived union before triggering the Coolify deploy.
394
-
395
- ```sql
396
- UPDATE app_deployments
397
- SET watch_paths = '<union-of-surface-watch_paths>'
398
- WHERE app_slug = '<slug>';
399
- ```
400
-
401
- Also PATCH the Coolify application's `watch_paths` field:
402
-
403
- ```bash
404
- _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
405
- WATCH_PATHS_JSON=$(derive_watch_paths_union "$PUBLISH_MD")
406
- curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
407
- -H "Content-Type: application/json" \
408
- -d "{\"watch_paths\":\"$WATCH_PATHS_JSON\"}" \
409
- "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
410
- ```
411
-
412
- ### Step 15 — storeArtifact per surface (after successful deploy)
413
-
414
- After the deployment reaches "finished" state, INSERT one row into Studio `artifact_registry` for each surface declared in PUBLISH.md:
415
-
416
- | Column | Value |
417
- |--------|-------|
418
- | `entity_slug` | from PUBLISH.md frontmatter `entity_slug` |
419
- | `artifact_type` | from PUBLISH.md frontmatter `artifact_type` |
420
- | `canonical_url` | `https://<app_deployments.url><surface.path>` |
421
- | `surface_id` | surface name from `<!-- SURFACE:<id> -->` marker |
422
- | `commit_sha` | HEAD SHA of the deploy |
423
- | `published_at` | `now()` |
424
-
425
- Use the Supabase MCP (`mcp__claude_ai_Supabase__execute_sql`) from the supervisor session:
426
-
427
- ```sql
428
- INSERT INTO artifact_registry (entity_slug, artifact_type, canonical_url, surface_id, commit_sha, published_at)
429
- VALUES ('<entity_slug>', '<artifact_type>', 'https://<url><path>', '<surface_id>', '<commit_sha>', now())
430
- ON CONFLICT (entity_slug, surface_id) DO UPDATE SET
431
- canonical_url = EXCLUDED.canonical_url,
432
- commit_sha = EXCLUDED.commit_sha,
433
- published_at = EXCLUDED.published_at;
434
- ```
435
-
436
- If the INSERT fails, surface the failure in the deploy output but **do NOT roll back the deploy**. The artifact registry is a post-deploy record, not a deploy gate.
437
-
438
- ## Coolify Access — clauth + REST API
439
-
440
- All Coolify operations use the clauth daemon and the Coolify REST API directly.
441
- There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
442
-
443
- ```bash
444
- # Get token (plain text — no JSON parsing needed)
445
- _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
446
-
447
- # List applications
448
- curl -s -H "Authorization: Bearer $_COOLIFY" \
449
- "$DEPLOY_API_BASE/api/v1/applications"
450
-
451
- # Get application details
452
- curl -s -H "Authorization: Bearer $_COOLIFY" \
453
- "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
454
-
455
- # Deploy (trigger) — correct endpoint is GET /api/v1/deploy?uuid=
456
- # (POST /applications/<uuid>/deploy returns {"message":"Not found."})
457
- curl -s -H "Authorization: Bearer $_COOLIFY" \
458
- "$DEPLOY_API_BASE/api/v1/deploy?uuid=<uuid>&force=true"
459
-
460
- # Get deployment logs
461
- curl -s -H "Authorization: Bearer $_COOLIFY" \
462
- "$DEPLOY_API_BASE/api/v1/deployments/<deployment-id>"
463
-
464
- # Set env var
465
- curl -s -X POST -H "Authorization: Bearer $_COOLIFY" \
466
- -H "Content-Type: application/json" \
467
- -d '{"key":"<KEY>","value":"<VALUE>"}' \
468
- "$DEPLOY_API_BASE/api/v1/applications/<uuid>/envs"
469
-
470
- # Set watch_paths
471
- curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
472
- -H "Content-Type: application/json" \
473
- -d '{"watch_paths":"apps/<name>/**\npackages/**"}' \
474
- "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
475
-
476
- # Change the app's domain — the writable field is "domains", NOT "fqdn"
477
- # (Coolify v4 PATCH rejects {"fqdn":...} with "This field is not allowed"; fqdn is read-only/derived)
478
- curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
479
- -H "Content-Type: application/json" \
480
- -d '{"domains":"https://<host>"}' \
481
- "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
482
- ```
483
-
484
- **Domain change / namespace migration** (lesson 2026-06-13-deploy-media-manager-namespace-migration):
485
- PATCH `applications/<uuid>` with `{"domains":"https://<host>"}` — never `fqdn`.
486
- For an app NEW to main (first prod deploy) also bring the app + a lockfile importer
487
- to main (clean worktree off origin/main, `pnpm install --lockfile-only`, verify the
488
- diff = additions for `apps/<name>:` only); resolve the lockfile-guard vs scope-guard
489
- collision by committing the app+lockfile as a SINGLE `chore(infra):`-subject commit
490
- (the scope-guard's documented escape hatch) so both pre-commit guards pass.
491
-
492
- **Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
493
-
494
- If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
495
- ```
496
- BLOCKED: credential provider is not responding.
497
- Fix: start the project's credential provider or configure deployment credentials through env vars, then retry.
498
- I cannot proceed until this is resolved.
499
- ```
500
-
501
- ## Deployment Event Log — `coolify_events`
502
-
503
- Every Coolify deploy emits a webhook → `coolify_events` row. Use this for last-N-deploys queries, debugging failed deploys, and reconciling local state with Coolify state.
504
-
505
- Query the last 5 events for an app:
506
- ```sql
507
- SELECT created_at, event_type, status, branch, commit_hash, duration_seconds
508
- FROM coolify_events
509
- WHERE app_uuid = '<uuid>' OR app_name = '<slug>'
510
- ORDER BY created_at DESC LIMIT 5;
511
- ```
512
-
513
- Fields:
514
- - `app_uuid` — Coolify application UUID (matches `app_deployments.coolify_uuid`)
515
- - `event_type` — `started | succeeded | failed | cancelled` (canonical values; consult webhook receiver for full enum)
516
- - `status` — overall deploy status
517
- - `branch`, `commit_hash`, `commit_message` — git context
518
- - `duration_seconds` — total deploy time
519
- - `payload` — full webhook payload (jsonb) for forensic debugging
520
-
521
- When diagnosing a broken deploy in Mode 3: query `coolify_events` FIRST before re-running the deploy — the most recent event row tells you whether the previous deploy actually triggered, whether it failed, and how long it ran. Faster than checking the Coolify UI.
522
-
523
- ## Capture lessons (exit step)
524
-
525
- Before the final verdict line, follow `.rdc/guides/lessons-learned-spec.md` § Capture procedure. If this run taught something non-obvious — a first root-cause theory that turned out wrong, the documented/standard path not working, a missing gate or check that cost a round, or a surprising tool/infra behavior — write one `.rdc/lessons/<YYYY-MM-DD>-deploy-<short-slug>.md` per lesson using the schema in that spec. Set `scope` (`simple` | `architectural`) and `status` (`open`, or `applied` if you shipped the fix in this same run, with the commit linked). Commit the lesson file(s) on `develop` alongside the run's other commits, and note "N lessons captured" in your verdict/summary. A run that taught nothing writes nothing — absence is the default.
526
-
527
- ## References
528
-
529
- - Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
530
- - Rules / registry RPCs / hard limits: `.claude/context/coolify-deployment.md`
531
- - Infrastructure constants:
532
- ```
533
- Server UUID: ih386anenvvvn6fy1umtyow0
534
- Server IP: 64.237.54.189
535
- Dashboard: <deployment-dashboard-url>
536
- GitHub App UUID: xdmcy60putp5h9j7k4kwg9c3
537
- ```
538
-
539
- ## Supersedes
540
-
541
- `coolify-deploy` standalone skill (kept for back-compat; new work uses `rdc:deploy`).
1
+ ---
2
+ name: rdc:deploy
3
+ description: "Usage `rdc:deploy <slug> [new|diagnose|audit|promote|convert] [--fix|--hotfix <sha>]` — Deploy an app to Coolify (production) or PM2 (staging), promote a verified dev change to production (one-command cherry-pick → PR → admin-merge → explicit Coolify trigger → verify), convert a prod app's runtime in place static→Next, add a new Coolify app, diagnose a failed deploy, or audit watch paths. Handles DNS, branch protection, health checks, and post-deploy verification."
4
+ ---
5
+
6
+ > **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
7
+ > Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
8
+ > One checklist upfront, updated in place, shown again at end with a 1-line verdict.
9
+
10
+
11
+ # rdc:deploy — Coolify Operations
12
+
13
+ **READ FIRST:** `guides/output-contract.md`. Checklist-only output. No narration.
14
+ No raw MCP dumps. No UUIDs unless asked.
15
+
16
+ > **Sandbox contract:** This skill honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
17
+ >
18
+ > *Under `$RDC_TEST=1`:* Modes 1 (deploy), 2 (new), and 5 (promote) are **entirely skipped** — echo `[RDC_TEST] skipping Coolify deploy/create/promote` and mark every `[ ]` line in those checklists as `[~]`. Modes 3 (diagnose) and 4 (audit without `--fix`) are **read-only and run normally**. Mode 4 with `--fix` skips all remediation — echo `[RDC_TEST] skipping audit --fix remediation` and report findings only. Registry SELECTs, Coolify status reads, HTTP gate probes, TLS checks, and DNS lookups are NOT destructive and run normally. Anything that writes (create app, set watch_paths, deploy trigger, **PR/admin-merge to main**, env var write, DNS write, CF cache purge, registry UPDATE/INSERT) is gated.
19
+
20
+ ## When to Use
21
+ - Project lead says "deploy", "ship it", "push to production", "update the server"
22
+ - Project lead says "promote", "patch prod", "hotfix production", "push this fix live" — use `rdc:deploy <slug> promote`
23
+ - Dev is a Next.js app but prod is still a static site for the same slug ("dev is XJS, prod is static", "make prod Next") — use `rdc:deploy <slug> convert` (Mode 6)
24
+ - A new app needs to be registered and deployed for the first time (`rdc:deploy new`)
25
+ - A deployed app is behaving unexpectedly and needs diagnosis (`rdc:deploy diagnose`)
26
+ - Running a compliance/health audit of all deployed apps (`rdc:deploy audit`)
27
+
28
+ ## Arguments
29
+
30
+ - `rdc:deploy <slug>` — deploy existing app (latest commit on its watched branch)
31
+ - `rdc:deploy <slug> <build-id>` — deploy specific commit/tag
32
+ - `rdc:deploy <slug> promote` — promote the verified `develop` change for this app to production (Mode 5)
33
+ - `rdc:deploy <slug> promote --hotfix <sha>` — promote a specific commit (cherry-pick just that sha to `main`)
34
+ - `rdc:deploy <slug> convert` — convert a prod app's runtime in place from static→Next (Mode 6); use when dev is Next but prod is still a static Coolify build
35
+ - `rdc:deploy new <slug>` — create a new Coolify app from registry
36
+ - `rdc:deploy diagnose <slug>` — debug why an app is broken
37
+ - `rdc:deploy audit` — fleet-wide scan for missed failures
38
+ - `rdc:deploy audit --fix` — fleet scan + auto-remediate safe issues
39
+ - `rdc:deploy` (no args) — print mode menu, ask which
40
+
41
+ ## Modes
42
+
43
+ ### Mode 1 — deploy <slug> [build-id]
44
+
45
+ ```
46
+ rdc:deploy: <slug> → <domain>
47
+ [ ] Registry lookup (slug, uuid, branch, type, env_vars_needed)
48
+ [ ] Runtime-source guard: if the source being deployed is static (`sites/<name>`, nixpacks/static) but the registry `runtime` for <slug> is `next`, BLOCK — never deploy a flat/static source over a Next app. A static prototype deploys only under its OWN slug (never an existing Next app's slug), dev only. Changing a slug's runtime is architectural. (`.claude/rules/production-stack-nextjs.md`)
49
+ [ ] Git state verified (branch matches Coolify, commit pushed)
50
+ [ ] Build-id resolved (default: HEAD of watched branch)
51
+ [ ] Env vars present in Coolify (compare to registry)
52
+ [ ] Type-specific preflight (see docs/runbooks/coolify-deploy-checklist.md)
53
+ [ ] Mandatory pre-deploy code-review (pr-review-toolkit:code-reviewer on `git diff <last-deployed-sha>..HEAD` for this app's paths). Block deploy on `critical`/`high` findings; record `medium`/`low` and proceed.
54
+ [ ] PUBLISH.md read from app root (warn if absent; fail if present but invalid)
55
+ [ ] watch_paths derived from PUBLISH.md surfaces (union of all surface watch_paths arrays) and updated in app_deployments
56
+ [ ] Deploy triggered
57
+ [ ] Deployment reached "finished" state
58
+ [ ] Gate: HTTP 200
59
+ [ ] Gate: TLS valid (no SSL cipher mismatch)
60
+ [ ] Gate: cache headers correct on HTML
61
+ [ ] Gate: container running on declared port
62
+ [ ] Gate: metadata audit (see § Metadata Audit below) — warn on gaps, do not block deploy
63
+ [ ] Cloudflare cache purged (if proxied)
64
+ [ ] artifact_registry INSERT per PUBLISH.md surface (if PUBLISH.md present)
65
+ [ ] deployment_registry updated (last_deploy_at, status)
66
+ ✅ rdc:deploy: <slug> deployed in Nm Ns
67
+ ```
68
+
69
+ #### Static PM2 dev sites — do NOT trust the push webhook (SSH-reset + served-hash gate)
70
+
71
+ For a **static** PM2 dev site, `git push` succeeding does NOT mean the live site
72
+ updated: the push webhook skips/instruments static apps unreliably, so the host
73
+ working tree (and the committed `dist/` it serves) can stay STUCK across several
74
+ pushes while HTTP stays 200 and `origin/develop` looks shipped (lesson
75
+ 2026-06-11-deploy-static-host-stuck: issho served the v1.10.0 bundle across three
76
+ pushes because `/srv/regen/regen-root` never pulled). The only signal is the
77
+ SERVED bundle hash vs the local `dist/` hash.
78
+
79
+ After pushing committed `dist/`, SSH-reset the host explicitly, then verify the
80
+ served hash:
81
+ ```bash
82
+ _K=$(mktemp); curl -s http://127.0.0.1:52437/v/vultr-dev-ssh > "$_K"; printf '\n' >> "$_K"; chmod 600 "$_K"
83
+ ssh -i "$_K" root@64.237.54.189 'cd /srv/regen/regen-root && git fetch -q origin develop && git reset -q --hard origin/develop && pm2 restart <app> --update-env'
84
+ rm -f "$_K"
85
+ # Then verify SERVED hash == local build hash (HTTP 200 is NOT proof):
86
+ curl -s https://<app>.dev.place.fund/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' # served
87
+ grep -oE 'index-[A-Za-z0-9_-]+\.js' sites/<app>/dist/index.html # local
88
+ ```
89
+ Only when the two hashes match do the content/screenshot gates mean anything.
90
+
91
+ ### Mode 2 — new <slug>
92
+
93
+ **MANDATORY:** All new apps are created from `docs/runbooks/coolify-app-templates.json`. Read that file first — pick the right template, substitute the required vars, POST the exact payload. No manual field configuration. No improvisation. The template encodes all learned lessons (base_directory, build_pack, watch_paths, health_check, ports). Deviating from it breaks things.
94
+
95
+ Template selection:
96
+ - `nextjs-app` — for `apps/<name>/` (Dockerfile, turbo filter, port 3000)
97
+ - `static-site` — for `sites/<name>/` (nixpacks, publish_directory=out, no packages)
98
+ - `mcp-server` — for `mcp-servers/<name>/` (Dockerfile, health check enabled, custom port)
99
+
100
+ ```
101
+ rdc:deploy new: <slug>
102
+ [ ] .dockerignore present at project root (`ls {PROJECT_ROOT}/.dockerignore` — STOP if missing)
103
+ [ ] Template loaded from docs/runbooks/coolify-app-templates.json (pick nextjs-app / static-site / mcp-server)
104
+ [ ] Required vars substituted: NAME, APP_PATH, DOMAIN, BRANCH, PROJECT_UUID, ENVIRONMENT_UUID [+ TURBO_FILTER / PORT]
105
+ [ ] DNS path chosen (A: staging wildcard B: apex C: other zone)
106
+ [ ] DNS record verified or wildcard confirmed
107
+ [ ] Cloudflare proxy setting correct for DNS path
108
+ [ ] Application created via POST /applications/private-github-app (template payload)
109
+ [ ] UUID recorded from response
110
+ [ ] watch_paths verified via GET /api/v1/applications/<uuid> — must match template
111
+ [ ] Env vars set in Coolify (from deployment_registry.env_vars_needed)
112
+ [ ] First deploy triggered
113
+ [ ] Deployment reached "finished" state
114
+ [ ] Gate: HTTP 200 on <domain>
115
+ [ ] Gate: TLS valid
116
+ [ ] deployment_registry row inserted
117
+ ✅ rdc:deploy new: <slug> live at <domain>
118
+ ```
119
+
120
+ ### Mode 3 — diagnose <slug>
121
+
122
+ ```
123
+ rdc:deploy diagnose: <slug>
124
+ [ ] App located (uuid, domain, last deploy)
125
+ [ ] Container state (running / restarting / stopped)
126
+ [ ] Last 100 log lines scanned for known error patterns
127
+ [ ] Port mismatch check (declared vs actual)
128
+ [ ] Env var drift check (registry vs Coolify)
129
+ [ ] watch_paths sanity check
130
+ [ ] HTTP / TLS reachability
131
+ [ ] Cloudflare proxy state check
132
+ [ ] Disk space on server
133
+ [ ] Branch mismatch check (Coolify git_branch vs expected)
134
+ ⚠️ rdc:deploy diagnose: <root cause in one sentence> — fix: <one command>
135
+ ```
136
+
137
+ #### `next start` crash-loop — check `.next/BUILD_ID` FIRST
138
+
139
+ When a PM2 `next start` app is crash-looping (`pm2 jlist` shows high `restart_time`
140
+ / "waiting restart"), check **`.next/BUILD_ID`** before chasing env/port theories —
141
+ it is the single gate `next start` enforces (`Could not find a production build in
142
+ the '.next' directory` repeats for every restart when it is missing). A `.next`
143
+ tree can exist as a PARTIAL build (manifests + server artifacts but no `BUILD_ID`)
144
+ — the signature of a `next build` that started but was interrupted/OOM-killed; the
145
+ crash loop then churns CPU and can re-trigger the OOM (lesson
146
+ 2026-06-13-deploy-nextstart-missing-buildid: ~7300 restarts, all `BUILD_ID`-missing,
147
+ while the documented prior lead was the NEXT_PUBLIC env issue — which was NOT the
148
+ cause here). Fix sequence:
149
+ ```bash
150
+ pm2 stop <app> # halt the loop so it stops competing for resources
151
+ rm -rf apps/<name>/.next # clear the partial tree
152
+ pnpm --filter @regen/<name> build # ONE clean scoped build (LOCAL BUILD SAFETY)
153
+ pm2 restart <app> --update-env
154
+ ```
155
+ Guard: assert `apps/<name>/.next/BUILD_ID` exists before (re)starting any
156
+ `next start` process.
157
+
158
+ #### ⛔ On the 2nd IDENTICAL failed probe, STOP polling and diagnose full-state
159
+
160
+ A repeated identical failure (same 404, same 502, same non-200) is a STRUCTURAL
161
+ signal, not eventual-consistency — polling it will never clear it and reads to the
162
+ user as stalling/throttling (lesson 2026-06-10-deploy-stop-polling-diagnose: a
163
+ 12×-404 poll loop on a remote-managed tunnel that was structurally ignoring the
164
+ local config — it could never clear). On the **2nd identical failed check**, stop
165
+ polling and pull GROUND TRUTH instead: list ALL processes / the whole effective
166
+ remote config / the full event log (`debugging-protocol.md` Rule 6 — full-state, not
167
+ a narrow filter). Poll ONLY when something is genuinely in-progress with a known
168
+ finish (a deploy build, a DNS first-create) — never to hope a structural error
169
+ resolves itself.
170
+
171
+ ### Mode 4 — audit
172
+
173
+ ```
174
+ rdc:deploy audit: fleet scan
175
+ [ ] Inventory join: Coolify apps ⋈ deployment_registry
176
+ [ ] Orphans (in one but not the other)
177
+ [ ] Monorepo apps missing watch_paths
178
+ [ ] Stale deploys (>14 days since last success)
179
+ [ ] Registry rows with status='broken'
180
+ [ ] Failed deployments in last 7 days
181
+ [ ] HTTP gate sweep (non-200 per domain)
182
+ [ ] TLS cert expiry <30 days
183
+ [ ] Port mismatches (ports_exposes vs actual container port)
184
+ [ ] Env var drift (registry.env_vars_needed vs Coolify env)
185
+ [ ] Branch mismatches (Coolify git_branch ≠ expected)
186
+ [ ] Disk space on 64.237.54.189
187
+ [ ] DNS/proxy misconfigs on configured staging wildcard
188
+ [ ] Duplicate apps (same repo, multiple UUIDs)
189
+
190
+ Findings:
191
+ | Severity | App | Issue | Fix |
192
+ |----------|-----|-------|-----|
193
+ | HIGH | ... | ... | ... |
194
+ ⚠️ rdc:deploy audit: N HIGH · M MED · K LOW — run `rdc:deploy audit --fix` to auto-remediate safe issues
195
+ ```
196
+
197
+ Severity rules:
198
+ - **HIGH** — user-facing down (HTTP non-200, TLS invalid, container not running)
199
+ - **MED** — degraded or drifting (watch_paths missing, env var drift, stale deploy, branch mismatch)
200
+ - **LOW** — cleanup (orphans, duplicates, registry status stale)
201
+
202
+ `--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
203
+
204
+ ### Mode 5 — promote <slug> [--hotfix <sha>]
205
+
206
+ Promote a **verified `develop` change** for one app to production. This is the sanctioned production-patch fast path — one command instead of fighting branch protection, the main-push hook, and a flaky Coolify webhook by hand.
207
+
208
+ **Authorization:** production promote requires explicit user go-ahead ("promote", "patch prod", "push live", "go"). A dev deploy does NOT. If the user has not given it for THIS promote, stop and ask first.
209
+
210
+ ```
211
+ rdc:deploy promote: <slug> → <prod-domain>
212
+ [ ] Registry lookup: PROD row (uuid, prod branch=main, watch_paths, url)
213
+ [ ] PUBLISH.md status gate: status=active AND prod in environments (block if not)
214
+ [ ] Runtime-source guard: BLOCK promoting a static source to a prod row whose intended `runtime` is `next` — production apps are Next.js (DB-backed). (`.claude/rules/production-stack-nextjs.md`)
215
+ [ ] Scope resolved: --hotfix <sha> → that commit; else the app-path commits on develop not on main
216
+ [ ] Scope guard: promote ONLY this app's paths. NEVER merge develop→main wholesale (drags unrelated WIP to prod)
217
+ [ ] Clean worktree off origin/main (never switch the dirty working tree)
218
+ [ ] Export Supabase creds in the worktree BEFORE commit (the pre-commit `sync:docs` hook needs them, and a fresh worktree does NOT inherit the main checkout's `.env*`/shell env — without the key sync:docs silently regenerates a GUTTED `app-deployments.md` ("Registry: unavailable") and `git add`s it into the surgical promote; `--no-verify` is forbidden):
219
+ `export NEXT_PUBLIC_SUPABASE_URL=https://uvojezuorjgqzmhhgluu.supabase.co`
220
+ `export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(curl -s http://127.0.0.1:52437/v/supabase-anon)`
221
+ Then ALWAYS `git show --stat HEAD` before pushing and confirm the file set is scoped — never ship the no-key gutted app-deployments.md. (lesson 2026-06-13-deploy-worktree-syncdocs-guts-app-deployments)
222
+ [ ] Apply change: cherry-pick <sha> (or `git checkout <develop-sha> -- <app-paths>`); confirm diff = expected files only
223
+ [ ] Mandatory pre-promote code-review (pr-review-toolkit:code-reviewer on the promote diff). Block on critical/high.
224
+ [ ] Commit on promote branch; push branch (NOT main directly — the main-push hook blocks raw main pushes)
225
+ [ ] Open PR base=main; merge with admin override (`gh pr merge --squash --admin --delete-branch`) — branch protection needs --admin
226
+ [ ] EXPLICITLY trigger Coolify deploy — NEVER rely on the GitHub→Coolify webhook (it silently no-ops on some merges even with auto-deploy ON)
227
+ [ ] Deployment reached "finished" state (poll coolify_events / deployment status)
228
+ [ ] Gate: HTTP 200 on prod domain
229
+ [ ] Gate: content-level check — assert the actual changed string is live (200 alone is NOT proof; origin may serve stale)
230
+ [ ] Gate: TLS valid
231
+ [ ] Gate: metadata audit (see § Metadata Audit below) — BLOCK promote on any missing required item
232
+ [ ] Cloudflare cache purged (if proxied)
233
+ [ ] deployment_registry updated (last_deploy_at, last_deploy_commit, status)
234
+ ✅ rdc:deploy promote: <slug> live in prod — <changed-string> verified
235
+ ```
236
+
237
+ **The explicit Coolify trigger (the whole point — do not skip):**
238
+ ```bash
239
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
240
+ # Correct endpoint is GET /api/v1/deploy?uuid= — NOT POST /applications/<uuid>/deploy (that 404s)
241
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
242
+ "$DEPLOY_API_BASE/api/v1/deploy?uuid=<PROD_UUID>&force=true"
243
+ # → {"deployments":[{"deployment_uuid":"...","message":"...deployment queued."}]}
244
+ ```
245
+
246
+ **Why each guard exists (lessons from 2026-06-05 EF Hooper promote):**
247
+ - `main` branch protection rejects PR merge without `--admin`; a raw `git push …:main` is blocked by the main-push hook → must go branch → PR → admin-merge.
248
+ - Coolify auto-deploy was **ON** for the app yet the merge did **not** auto-deploy — a webhook-delivery flake. A promote must ALWAYS trigger the deploy explicitly and verify; never hope the webhook fired.
249
+ - `develop` was 87 commits ahead of `main` (unrelated apps' WIP). Promoting must be surgical (this app's paths / one sha), never a develop→main merge.
250
+ - HTTP 200 was returned by the stale origin the whole time — only a content-level assertion (`curl … | grep '<new string>'`) proves the promote landed.
251
+
252
+ ### Mode 6 — convert <slug> (prod runtime static→Next, in place)
253
+
254
+ One-time conversion of an existing **production** Coolify app from a static/nixpacks build to the Next.js (dockerfile) runtime, **in place** — same UUID, same fqdn, same DNS. After this, the slug is a normal Next app and every future ship is just `rdc:deploy <slug> promote`. This is the mode for "dev is Next, prod is still static" (the `vlas.earth` / `life.ai` class). `promote` alone CANNOT do this — it re-triggers the prod app's existing (static) build; only `convert` changes the build config.
255
+
256
+ **Authorization:** this changes a production app's runtime — **architectural** per `.claude/rules/production-stack-nextjs.md` + `.claude/rules/architectural-change-approval.md`. Requires explicit user go-ahead for THIS slug. Stop and ask if not given.
257
+
258
+ **Why in-place PATCH, not recreate:** keeps the application UUID, fqdn (apex + `www`), DNS binding, and project — zero DNS cutover, zero window where the domain is unbound. PATCH switches the build pack; the next deploy builds the Next app on the same application object.
259
+
260
+ ```
261
+ rdc:deploy convert: <slug> → <prod-domain> (static→Next, in place)
262
+ [ ] Go-ahead confirmed for THIS slug (production runtime change — architectural)
263
+ [ ] Registry lookup: PROD coolify_uuid, fqdn, project/env, current build_pack
264
+ [ ] Confirm it IS a convert: dev runtime=next AND prod build_pack ∈ {static, nixpacks}. If prod build_pack already `dockerfile` → already converted, fall through to Mode 5 promote.
265
+ [ ] Source Next-prod-readiness preflight (BLOCK on any miss):
266
+ - apps/<name>/Dockerfile present (prod build) — BLOCK "source not Next-prod-ready: add Dockerfile" if missing
267
+ - start/listen port reconciled with intended ports_exposes (no dev-only port like :3214 leaking to prod)
268
+ - PUBLISH.md present, build_type=nextjs, status=active, prod in environments
269
+ [ ] Mandatory pre-convert code-review (pr-review-toolkit:code-reviewer on the apps/<name> diff being promoted). Block on critical/high.
270
+ [ ] Promote app code to main (Mode 5 path): clean worktree off origin/main → checkout apps/<name> paths (or cherry-pick) → confirm diff = expected files only → branch → PR base=main → `gh pr merge --squash --admin --delete-branch`
271
+ [ ] LOCKFILE IMPORTER (mandatory for an app NEW to main): if `git show origin/main:pnpm-lock.yaml | grep -c "apps/<name>:"` is 0, the Docker `pnpm install --frozen-lockfile` WILL fail. In the clean main worktree run `pnpm install --lockfile-only` (adds only the apps/<name> importer; verify the diff is minimal — do NOT promote develop's whole lockfile, it carries unrelated drift) and include `pnpm-lock.yaml` in the SAME promote PR.
272
+ [ ] In-place PATCH prod Coolify app <uuid> to nextjs-app template fields:
273
+ build_pack=dockerfile · dockerfile_location=/apps/<name>/Dockerfile · base_directory=/ ·
274
+ build_command="pnpm turbo run build --filter=<pkg>" · start_command="pnpm --filter=<pkg> start" ·
275
+ install_command="pnpm install --frozen-lockfile" · ports_exposes=<port> ·
276
+ watch_paths="apps/<name>/**\npackages/**"
277
+ (clear publish_directory; static-only field)
278
+ [ ] PROXY LABELS (mandatory — the static-app `custom_labels` are read-only and do NOT auto-update on a build_pack PATCH): fetch `custom_labels`, base64-decode, rewrite `loadbalancer.server.port=80`→`=<port>` (all routers) AND `upstreams 80}}`→`upstreams <port>}}`, and STRIP the static `caddy_*.try_files=…/index.html /index.php` lines (wrong for a Next app), re-base64, PATCH `custom_labels`. Skipping this = container serves on :<port> but the proxy still routes :80 → **502** even though the build "finished" and the app logs "Ready". (Note: the API rejects `is_container_label_readonly_enabled` and a non-base64 `custom_labels` — you must hand-rewrite the base64.)
279
+ [ ] Env vars present in Coolify (compare registry.env_vars_needed); set any missing
280
+ [ ] EXPLICITLY trigger deploy: GET /api/v1/deploy?uuid=<PROD_UUID>&force=true — never rely on the webhook
281
+ [ ] Deployment reached "finished" state (poll coolify_events)
282
+ [ ] Gate: SSR proof — prod HTML now contains `/_next/static` (it is the Next app, not the old static build)
283
+ [ ] Gate: HTTP 200 + content-level assertion of a known string from the Next render
284
+ [ ] Gate: TLS valid; cache headers correct on HTML
285
+ [ ] Gate: metadata audit (see § Metadata Audit) — BLOCK on any required gap
286
+ [ ] Cloudflare cache purged (apex + www)
287
+ [ ] app_deployments updated: runtime intent → next, notes (converted static→Next <date>), last_deploy_at, last_deploy_commit, status=active
288
+ [ ] `apps` row runtime=next confirmed
289
+ ✅ rdc:deploy convert: <slug> now Next.js in prod — SSR verified at <prod-domain>
290
+ ```
291
+
292
+ **Rollback:** PATCH the app back to static (`build_pack=static`, `base_directory=/sites/<name>`, `publish_directory=/sites/<name>`, `ports_exposes=80`, `watch_paths=sites/<name>/**`) and redeploy. `sites/<name>` stays in the repo precisely so this rollback is always available.
293
+
294
+ **PATCH command (in-place build-pack switch):**
295
+ ```bash
296
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
297
+ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" -H "Content-Type: application/json" \
298
+ -d '{"build_pack":"dockerfile","dockerfile_location":"/apps/<name>/Dockerfile","base_directory":"/","build_command":"pnpm turbo run build --filter=<pkg>","start_command":"pnpm --filter=<pkg> start","install_command":"pnpm install --frozen-lockfile","ports_exposes":"<port>","watch_paths":"apps/<name>/**\npackages/**"}' \
299
+ "$DEPLOY_API_BASE/api/v1/applications/<PROD_UUID>"
300
+ ```
301
+
302
+ ## Metadata Audit
303
+
304
+ After a successful deploy or promote, audit the live site's `<head>` metadata. Run this against the deployed URL (not a local file).
305
+
306
+ **Check list** (curl the deployed URL, parse the HTML `<head>`):
307
+
308
+ | # | Item | How to check | Required | Severity |
309
+ |---|------|-------------|----------|----------|
310
+ | 1 | `<title>` | grep `<title>` — non-empty, not default/placeholder | yes | block |
311
+ | 2 | `<meta name="description">` | content attr ≥ 50 chars, ≤ 160 chars | yes | block on promote; warn on deploy |
312
+ | 3 | `<link rel="canonical">` | href present, starts with `https://`, matches the deployed domain | yes | block on promote; warn on deploy |
313
+ | 4 | `og:title` | `<meta property="og:title">` present | yes | block on promote; warn on deploy |
314
+ | 5 | `og:description` | `<meta property="og:description">` present, ≥ 50 chars | yes | block on promote; warn on deploy |
315
+ | 6 | `og:image` | `<meta property="og:image">` present, URL returns HTTP 200 | yes | block on promote; warn on deploy |
316
+ | 7 | `og:url` | present, matches canonical | recommended | warn |
317
+ | 8 | `twitter:card` | `summary_large_image` or `summary` | recommended | warn |
318
+ | 9 | `twitter:image` | present, URL returns HTTP 200 | recommended | warn |
319
+ | 10 | Favicon | `<link rel="icon">` present, href returns HTTP 200 | yes | block on promote; warn on deploy |
320
+ | 11 | `sitemap.xml` | `<domain>/sitemap.xml` returns HTTP 200 | recommended | warn |
321
+ | 12 | `robots.txt` | `<domain>/robots.txt` returns HTTP 200 | recommended | warn |
322
+ | 13 | Version | `<meta name="version">` or visible version string in page | yes (project rule) | warn |
323
+
324
+ **Behavior by mode:**
325
+ - **Mode 1 (deploy):** run the audit after HTTP/TLS gates. Print a table of results. **Warn** on gaps but do NOT block the deploy — dev deploys are iterative.
326
+ - **Mode 5 (promote):** run the audit after content-level check. **Block the promote** if any item marked "block on promote" is missing. Print the table and require the metadata to be fixed before re-attempting.
327
+ - **Mode 4 (audit):** include the metadata sweep in the fleet scan (one row per app per missing item in the findings table).
328
+
329
+ **Implementation — one-liner per check:**
330
+ ```bash
331
+ URL="https://<domain>"
332
+ HEAD=$(curl -s "$URL" | sed -n '/<head/,/<\/head>/p')
333
+ echo "$HEAD" | grep -ioE '<title>[^<]+</title>'
334
+ echo "$HEAD" | grep -ioE 'name="description"[^>]*content="[^"]*"'
335
+ echo "$HEAD" | grep -ioE 'rel="canonical"[^>]*href="[^"]*"'
336
+ echo "$HEAD" | grep -ioE 'property="og:title"[^>]*content="[^"]*"'
337
+ echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="[^"]*"'
338
+ echo "$HEAD" | grep -ioE 'name="twitter:card"[^>]*content="[^"]*"'
339
+ echo "$HEAD" | grep -ioE 'rel="icon"[^>]*href="[^"]*"'
340
+ echo "$HEAD" | grep -ioE 'name="version"[^>]*content="[^"]*"'
341
+ curl -s -o /dev/null -w "%{http_code}" "$URL/sitemap.xml"
342
+ curl -s -o /dev/null -w "%{http_code}" "$URL/robots.txt"
343
+ OG_IMG=$(echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="([^"]*)"' | grep -ioE 'https://[^"]*')
344
+ [ -n "$OG_IMG" ] && curl -s -o /dev/null -w "%{http_code}" "$OG_IMG"
345
+ ```
346
+
347
+ **Why this exists (2026-06-05):** life.ai deployed to production with zero social/SEO metadata — no description, no OG, no favicon, no sitemap. HTTP 200 + TLS + content passed; metadata was invisible to the existing gates. This audit catches that class of defect.
348
+
349
+ ## Hotlink referer allowlist — new media.place.fund consumer domains
350
+
351
+ When onboarding a NEW brand domain that loads assets from `media.place.fund`, add
352
+ that domain to the hotlink-protection Worker's referer allowlist **before
353
+ go-live** (lesson 2026-06-14-deploy-issholiving-referer-hotlink-403: a new brand
354
+ domain served its own HTML fine but every `media.place.fund` image returned 403
355
+ because the hotlink Worker rejected the unknown `Referer`). The image 403 is
356
+ invisible to the HTTP-200 / TLS / metadata gates — they probe the HTML document,
357
+ not the cross-origin asset.
358
+
359
+ - Add the new origin (apex + `www`, dev + prod) to the Worker's referer allowlist
360
+ and redeploy the Worker before the brand domain goes live.
361
+ - Verify with an explicit `Referer` probe (NOT a bare curl — a missing Referer can
362
+ pass while the real browser Referer fails):
363
+ ```bash
364
+ curl -s -o /dev/null -w "%{http_code}" -e "https://<new-brand-domain>/" \
365
+ "https://media.place.fund/<a-known-asset-path>" # expect 200, not 403
366
+ ```
367
+
368
+ ## PUBLISH.md Integration
369
+
370
+ Every deploy reads `PUBLISH.md` from the app's source root to derive `watch_paths` and to register surfaces in Studio `artifact_registry`.
371
+
372
+ ### Step 6 — Read PUBLISH.md from the app root
373
+
374
+ ```bash
375
+ MONOREPO_PATH=$(get_app_deployments_monorepo_path "$SLUG")
376
+ PUBLISH_MD="$MONOREPO_PATH/PUBLISH.md"
377
+
378
+ if [ ! -f "$PUBLISH_MD" ]; then
379
+ echo "WARN: PUBLISH.md missing for $SLUG — using app_deployments.watch_paths only"
380
+ # Deploy continues; watch_paths derivation and artifact_registry INSERT are skipped
381
+ fi
382
+ ```
383
+
384
+ PUBLISH.md format: see `C:/Dev/rdc-skills/guides/publish-md-spec.md` (authoritative).
385
+
386
+ Required frontmatter fields: `schema_version`, `entity_slug`, `artifact_type`, `environments`, `status`.
387
+ One or more `<!-- SURFACE:<id> -->` … `<!-- /SURFACE:<id> -->` blocks per surface (each with `path`, `source_dir`, `build_type`, `visibility`, `cache`, `watch_paths`).
388
+
389
+ If PUBLISH.md is **present but invalid** (missing required field, bad enum, no surface blocks): abort deploy with `BLOCKED: PUBLISH.md parse error for <slug> — <reason>`.
390
+
391
+ ### Step 7 — Derive watch_paths from PUBLISH.md surfaces
392
+
393
+ Union all `watch_paths` arrays across every surface section in PUBLISH.md. Update `app_deployments.watch_paths` for the app slug to this derived union before triggering the Coolify deploy.
394
+
395
+ ```sql
396
+ UPDATE app_deployments
397
+ SET watch_paths = '<union-of-surface-watch_paths>'
398
+ WHERE app_slug = '<slug>';
399
+ ```
400
+
401
+ Also PATCH the Coolify application's `watch_paths` field:
402
+
403
+ ```bash
404
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
405
+ WATCH_PATHS_JSON=$(derive_watch_paths_union "$PUBLISH_MD")
406
+ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
407
+ -H "Content-Type: application/json" \
408
+ -d "{\"watch_paths\":\"$WATCH_PATHS_JSON\"}" \
409
+ "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
410
+ ```
411
+
412
+ ### Step 15 — storeArtifact per surface (after successful deploy)
413
+
414
+ After the deployment reaches "finished" state, INSERT one row into Studio `artifact_registry` for each surface declared in PUBLISH.md:
415
+
416
+ | Column | Value |
417
+ |--------|-------|
418
+ | `entity_slug` | from PUBLISH.md frontmatter `entity_slug` |
419
+ | `artifact_type` | from PUBLISH.md frontmatter `artifact_type` |
420
+ | `canonical_url` | `https://<app_deployments.url><surface.path>` |
421
+ | `surface_id` | surface name from `<!-- SURFACE:<id> -->` marker |
422
+ | `commit_sha` | HEAD SHA of the deploy |
423
+ | `published_at` | `now()` |
424
+
425
+ Use the Supabase MCP (`mcp__claude_ai_Supabase__execute_sql`) from the supervisor session:
426
+
427
+ ```sql
428
+ INSERT INTO artifact_registry (entity_slug, artifact_type, canonical_url, surface_id, commit_sha, published_at)
429
+ VALUES ('<entity_slug>', '<artifact_type>', 'https://<url><path>', '<surface_id>', '<commit_sha>', now())
430
+ ON CONFLICT (entity_slug, surface_id) DO UPDATE SET
431
+ canonical_url = EXCLUDED.canonical_url,
432
+ commit_sha = EXCLUDED.commit_sha,
433
+ published_at = EXCLUDED.published_at;
434
+ ```
435
+
436
+ If the INSERT fails, surface the failure in the deploy output but **do NOT roll back the deploy**. The artifact registry is a post-deploy record, not a deploy gate.
437
+
438
+ ## Coolify Access — clauth + REST API
439
+
440
+ All Coolify operations use the clauth daemon and the Coolify REST API directly.
441
+ There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
442
+
443
+ ```bash
444
+ # Get token (plain text — no JSON parsing needed)
445
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
446
+
447
+ # List applications
448
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
449
+ "$DEPLOY_API_BASE/api/v1/applications"
450
+
451
+ # Get application details
452
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
453
+ "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
454
+
455
+ # Deploy (trigger) — correct endpoint is GET /api/v1/deploy?uuid=
456
+ # (POST /applications/<uuid>/deploy returns {"message":"Not found."})
457
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
458
+ "$DEPLOY_API_BASE/api/v1/deploy?uuid=<uuid>&force=true"
459
+
460
+ # Get deployment logs
461
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
462
+ "$DEPLOY_API_BASE/api/v1/deployments/<deployment-id>"
463
+
464
+ # Set env var
465
+ curl -s -X POST -H "Authorization: Bearer $_COOLIFY" \
466
+ -H "Content-Type: application/json" \
467
+ -d '{"key":"<KEY>","value":"<VALUE>"}' \
468
+ "$DEPLOY_API_BASE/api/v1/applications/<uuid>/envs"
469
+
470
+ # Set watch_paths
471
+ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
472
+ -H "Content-Type: application/json" \
473
+ -d '{"watch_paths":"apps/<name>/**\npackages/**"}' \
474
+ "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
475
+
476
+ # Change the app's domain — the writable field is "domains", NOT "fqdn"
477
+ # (Coolify v4 PATCH rejects {"fqdn":...} with "This field is not allowed"; fqdn is read-only/derived)
478
+ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
479
+ -H "Content-Type: application/json" \
480
+ -d '{"domains":"https://<host>"}' \
481
+ "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
482
+ ```
483
+
484
+ **Domain change / namespace migration** (lesson 2026-06-13-deploy-media-manager-namespace-migration):
485
+ PATCH `applications/<uuid>` with `{"domains":"https://<host>"}` — never `fqdn`.
486
+ For an app NEW to main (first prod deploy) also bring the app + a lockfile importer
487
+ to main (clean worktree off origin/main, `pnpm install --lockfile-only`, verify the
488
+ diff = additions for `apps/<name>:` only); resolve the lockfile-guard vs scope-guard
489
+ collision by committing the app+lockfile as a SINGLE `chore(infra):`-subject commit
490
+ (the scope-guard's documented escape hatch) so both pre-commit guards pass.
491
+
492
+ **Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
493
+
494
+ If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
495
+ ```
496
+ BLOCKED: credential provider is not responding.
497
+ Fix: start the project's credential provider or configure deployment credentials through env vars, then retry.
498
+ I cannot proceed until this is resolved.
499
+ ```
500
+
501
+ ## Deployment Event Log — `coolify_events`
502
+
503
+ Every Coolify deploy emits a webhook → `coolify_events` row. Use this for last-N-deploys queries, debugging failed deploys, and reconciling local state with Coolify state.
504
+
505
+ Query the last 5 events for an app:
506
+ ```sql
507
+ SELECT created_at, event_type, status, branch, commit_hash, duration_seconds
508
+ FROM coolify_events
509
+ WHERE app_uuid = '<uuid>' OR app_name = '<slug>'
510
+ ORDER BY created_at DESC LIMIT 5;
511
+ ```
512
+
513
+ Fields:
514
+ - `app_uuid` — Coolify application UUID (matches `app_deployments.coolify_uuid`)
515
+ - `event_type` — `started | succeeded | failed | cancelled` (canonical values; consult webhook receiver for full enum)
516
+ - `status` — overall deploy status
517
+ - `branch`, `commit_hash`, `commit_message` — git context
518
+ - `duration_seconds` — total deploy time
519
+ - `payload` — full webhook payload (jsonb) for forensic debugging
520
+
521
+ When diagnosing a broken deploy in Mode 3: query `coolify_events` FIRST before re-running the deploy — the most recent event row tells you whether the previous deploy actually triggered, whether it failed, and how long it ran. Faster than checking the Coolify UI.
522
+
523
+ ## Capture lessons (exit step)
524
+
525
+ Before the final verdict line, follow `.rdc/guides/lessons-learned-spec.md` § Capture procedure. If this run taught something non-obvious — a first root-cause theory that turned out wrong, the documented/standard path not working, a missing gate or check that cost a round, or a surprising tool/infra behavior — write one `.rdc/lessons/<YYYY-MM-DD>-deploy-<short-slug>.md` per lesson using the schema in that spec. Set `scope` (`simple` | `architectural`) and `status` (`open`, or `applied` if you shipped the fix in this same run, with the commit linked). Commit the lesson file(s) on `develop` alongside the run's other commits, and note "N lessons captured" in your verdict/summary. A run that taught nothing writes nothing — absence is the default.
526
+
527
+ ## References
528
+
529
+ - Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
530
+ - Rules / registry RPCs / hard limits: `.claude/context/coolify-deployment.md`
531
+ - Infrastructure constants:
532
+ ```
533
+ Server UUID: ih386anenvvvn6fy1umtyow0
534
+ Server IP: 64.237.54.189
535
+ Dashboard: <deployment-dashboard-url>
536
+ GitHub App UUID: xdmcy60putp5h9j7k4kwg9c3
537
+ ```
538
+
539
+ ## Supersedes
540
+
541
+ `coolify-deploy` standalone skill (kept for back-compat; new work uses `rdc:deploy`).