@lifeaitools/rdc-skills 0.35.18 → 0.35.21

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.
@@ -1,566 +1,115 @@
1
1
  ---
2
2
  name: deploy
3
- description: rdc:deploy (slug, [action]) - [--fix, --hotfix] ship to PM2 dev; promote to Coolify production
3
+ description: rdc:deploy (slug, [action]) — run one command; exit 0 means shipped
4
4
  ---
5
5
 
6
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.
7
+ > One command, then one line: the URL on exit 0, or the `DEPLOY-FAILED` block on
8
+ > non-zero. No tool-call narration, no raw JSON dumps, no progress commentary.
9
9
 
10
+ # rdc:deploy — run the program, read the exit code
10
11
 
11
- # rdc:deploy Bearer Deployment Control Plane
12
+ **This skill is not a procedure. It is one command.**
12
13
 
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 (PM2 dev deploy), 2 (new), and 5 (Coolify promote) are **entirely skipped** — echo `[RDC_TEST] skipping PM2 deploy/create/Coolify 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
- - A platform service needs to run only on the Coolify network, with no public domain or host port (`rdc:deploy maintenance <service>`)
28
-
29
- ## Arguments
30
-
31
- - `rdc:deploy <slug>` — submit the registered `develop` deployment to the PM2 dev target
32
- - `rdc:deploy <slug> <ref>` — submit one registered manifest ref (never arbitrary shell or repository input)
33
- - `rdc:deploy <slug> promote` — promote the verified `develop` change for this app to production (Mode 5)
34
- - `rdc:deploy <slug> promote --hotfix <sha>` — promote a specific commit (cherry-pick just that sha to `main`)
35
- - `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
36
- - `rdc:deploy new <slug>` — create a new Coolify app from registry
37
- - `rdc:deploy diagnose <slug>` — debug why an app is broken
38
- - `rdc:deploy audit` — fleet-wide scan for missed failures
39
- - `rdc:deploy audit --fix` — fleet scan + auto-remediate safe issues
40
- - `rdc:deploy maintenance <service>` — create, update, or verify one allowlisted private infrastructure service (Mode 7)
41
- - `rdc:deploy dev <slug>` — explicit alias for the first form above. Plain
42
- `rdc:deploy <slug>` already targets PM2 dev, so this only makes the intent
43
- unmissable when a reader expects Coolify.
44
- - `rdc:deploy` (no args) — print mode menu, ask which
45
-
46
- > `<ref>` is what `commands/deploy.md` called `<build-id>` before that file was
47
- > removed (2026-08-29). Same thing — a registered manifest ref, a commit, or a
48
- > tag. With no ref, the deployment is the latest commit on the app's WATCHED
49
- > branch, not on whatever branch happens to be checked out locally.
50
-
51
- ## Modes
52
-
53
- ### Mode 1 — deploy <slug> [ref]
54
-
55
- ```
56
- rdc:deploy: <slug> → PM2 dev target
57
- [ ] Registry lookup (slug, uuid, branch, type, env_vars_needed)
58
- [ ] 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`)
59
- [ ] Git state verified (registered ref is pushed to origin)
60
- [ ] Dev deployment manifest resolved (application, repo path, fixed build argv, PM2 name, allowed ref, health URL)
61
- [ ] Local clauth retrieves `vultr-ops-api-token` internally; bearer is never printed or passed by an agent
62
- [ ] 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.
63
- [ ] PUBLISH.md read from app root (warn if absent; fail if present but invalid)
64
- [ ] watch_paths derived from PUBLISH.md surfaces (union of all surface watch_paths arrays) and updated in app_deployments
65
- [ ] `clauth ops deploy --endpoint <control-plane> --application <slug> --ref <registered-ref>` accepted (202)
66
- [ ] Job polled at `GET /v1/ops/jobs/<id>` to terminal state; receipt proves checkout SHA, build, PM2 reload, and health probe
67
- [ ] Gate: HTTP 200
68
- [ ] Gate: TLS valid (no SSL cipher mismatch)
69
- [ ] Gate: cache headers correct on HTML
70
- [ ] Gate: named PM2 process is online at its declared port
71
- [ ] Gate: metadata audit (see § Metadata Audit below) — warn on gaps, do not block deploy
72
- [ ] Cloudflare cache purged (if proxied)
73
- [ ] artifact_registry INSERT per PUBLISH.md surface (if PUBLISH.md present)
74
- [ ] deployment_registry updated (last_deploy_at, status)
75
- ✅ rdc:deploy: <slug> deployed in Nm Ns
76
- ```
77
-
78
- #### Static PM2 dev sites — bearer job + served-content gate
79
-
80
- For a **static** PM2 dev site, a successful `git push` is not deployment proof:
81
- the formerly unreliable webhook could leave the host working tree and committed
82
- `dist/` stale while HTTP still returned 200. The agent MUST submit the same
83
- manifest-scoped `clauth ops deploy` job used by every other dev runtime, then
84
- record its checkout SHA, declared build, PM2 reload, and health receipt.
85
-
86
- Agents MUST NOT retrieve `vultr-dev-ssh`, stage private keys, run raw SSH, reset
87
- the host checkout, or invoke PM2 directly. If a served-asset hash or content
88
- marker does not match the declared build after the job reaches `succeeded`, mark
89
- the deployment verification failed and use `rdc:deploy <slug> diagnose`; only
90
- the server-operations recovery procedure may perform host-level repair.
91
-
92
- ### Mode 2 — new <slug>
93
-
94
- **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.
95
-
96
- Template selection:
97
- - `nextjs-app` — for `apps/<name>/` (Dockerfile, turbo filter, port 3000)
98
- - `static-site` — for `sites/<name>/` (nixpacks, publish_directory=out, no packages)
99
- - `mcp-server` — for `mcp-servers/<name>/` (Dockerfile, health check enabled, custom port)
100
-
101
- ```
102
- rdc:deploy new: <slug>
103
- [ ] .dockerignore present at project root (`ls {PROJECT_ROOT}/.dockerignore` — STOP if missing)
104
- [ ] Template loaded from docs/runbooks/coolify-app-templates.json (pick nextjs-app / static-site / mcp-server)
105
- [ ] Required vars substituted: NAME, APP_PATH, DOMAIN, BRANCH, PROJECT_UUID, ENVIRONMENT_UUID [+ TURBO_FILTER / PORT]
106
- [ ] DNS path chosen (A: staging wildcard B: apex C: other zone)
107
- [ ] DNS record verified or wildcard confirmed
108
- [ ] Cloudflare proxy setting correct for DNS path
109
- [ ] Application created via POST /applications/private-github-app (template payload)
110
- [ ] UUID recorded from response
111
- [ ] watch_paths verified via GET /api/v1/applications/<uuid> — must match template
112
- [ ] Env vars set in Coolify (from deployment_registry.env_vars_needed)
113
- [ ] First deploy triggered
114
- [ ] Deployment reached "finished" state
115
- [ ] Gate: HTTP 200 on <domain>
116
- [ ] Gate: TLS valid
117
- [ ] deployment_registry row inserted
118
- ✅ rdc:deploy new: <slug> live at <domain>
119
- ```
120
-
121
- ### Mode 3 — diagnose <slug>
122
-
123
- ```
124
- rdc:deploy diagnose: <slug>
125
- [ ] App located (uuid, domain, last deploy)
126
- [ ] Container state (running / restarting / stopped)
127
- [ ] Last 100 log lines scanned for known error patterns
128
- [ ] Port mismatch check (declared vs actual)
129
- [ ] Env var drift check (registry vs Coolify)
130
- [ ] watch_paths sanity check
131
- [ ] HTTP / TLS reachability
132
- [ ] Cloudflare proxy state check
133
- [ ] Disk space on server
134
- [ ] Branch mismatch check (Coolify git_branch vs expected)
135
- ⚠️ rdc:deploy diagnose: <root cause in one sentence> — fix: <one command>
136
- ```
137
-
138
- #### `next start` crash-loop — check `.next/BUILD_ID` FIRST
139
-
140
- When a PM2 `next start` app is crash-looping (`pm2 jlist` shows high `restart_time`
141
- / "waiting restart"), check **`.next/BUILD_ID`** before chasing env/port theories —
142
- it is the single gate `next start` enforces (`Could not find a production build in
143
- the '.next' directory` repeats for every restart when it is missing). A `.next`
144
- tree can exist as a PARTIAL build (manifests + server artifacts but no `BUILD_ID`)
145
- — the signature of a `next build` that started but was interrupted/OOM-killed; the
146
- crash loop then churns CPU and can re-trigger the OOM (lesson
147
- 2026-06-13-deploy-nextstart-missing-buildid: ~7300 restarts, all `BUILD_ID`-missing,
148
- while the documented prior lead was the NEXT_PUBLIC env issue — which was NOT the
149
- cause here). Fix sequence:
150
- ```bash
151
- pm2 stop <app> # halt the loop so it stops competing for resources
152
- rm -rf apps/<name>/.next # clear the partial tree
153
- pnpm --filter @regen/<name> build # ONE clean scoped build (LOCAL BUILD SAFETY)
154
- pm2 restart <app> --update-env
155
- ```
156
- Guard: assert `apps/<name>/.next/BUILD_ID` exists before (re)starting any
157
- `next start` process.
158
-
159
- #### ⛔ On the 2nd IDENTICAL failed probe, STOP polling and diagnose full-state
160
-
161
- A repeated identical failure (same 404, same 502, same non-200) is a STRUCTURAL
162
- signal, not eventual-consistency — polling it will never clear it and reads to the
163
- user as stalling/throttling (lesson 2026-06-10-deploy-stop-polling-diagnose: a
164
- 12×-404 poll loop on a remote-managed tunnel that was structurally ignoring the
165
- local config — it could never clear). On the **2nd identical failed check**, stop
166
- polling and pull GROUND TRUTH instead: list ALL processes / the whole effective
167
- remote config / the full event log (`debugging-protocol.md` Rule 6 — full-state, not
168
- a narrow filter). Poll ONLY when something is genuinely in-progress with a known
169
- finish (a deploy build, a DNS first-create) — never to hope a structural error
170
- resolves itself.
171
-
172
- ### Mode 4 — audit
173
-
174
- ```
175
- rdc:deploy audit: fleet scan
176
- [ ] Inventory join: Coolify apps ⋈ deployment_registry
177
- [ ] Orphans (in one but not the other)
178
- [ ] Monorepo apps missing watch_paths
179
- [ ] Stale deploys (>14 days since last success)
180
- [ ] Registry rows with status='broken'
181
- [ ] Failed deployments in last 7 days
182
- [ ] HTTP gate sweep (non-200 per domain)
183
- [ ] TLS cert expiry <30 days
184
- [ ] Port mismatches (ports_exposes vs actual container port)
185
- [ ] Env var drift (registry.env_vars_needed vs Coolify env)
186
- [ ] Branch mismatches (Coolify git_branch ≠ expected)
187
- [ ] Disk space on 64.237.54.189
188
- [ ] DNS/proxy misconfigs on configured staging wildcard
189
- [ ] Duplicate apps (same repo, multiple UUIDs)
190
-
191
- Findings:
192
- | Severity | App | Issue | Fix |
193
- |----------|-----|-------|-----|
194
- | HIGH | ... | ... | ... |
195
- ⚠️ rdc:deploy audit: N HIGH · M MED · K LOW — run `rdc:deploy audit --fix` to auto-remediate safe issues
196
- ```
197
-
198
- Severity rules:
199
- - **HIGH** — user-facing down (HTTP non-200, TLS invalid, container not running)
200
- - **MED** — degraded or drifting (watch_paths missing, env var drift, stale deploy, branch mismatch)
201
- - **LOW** — cleanup (orphans, duplicates, registry status stale)
202
-
203
- `--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
204
-
205
- ### Mode 5 — promote <slug> [--hotfix <sha>]
206
-
207
- 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.
208
-
209
- **Authorization:** production promote requires explicit user go-ahead, in either form:
210
- - **Per-promote:** a direct go word for THIS call ("promote", "patch prod", "push live", "go").
211
- - **Standing opt-in:** the user names one slug and grants it for the current session only ("auto-promote <slug> whenever dev is green", "standing go-ahead for <slug> this session"). Scope rules:
212
- - One named slug per opt-in — never blanket ("auto-promote everything").
213
- - Expires with the session. A new session/conversation needs a fresh opt-in; never infer one from a prior session's transcript or memory.
214
- - Does not skip any other gate below — metadata-audit block, content-level verify, mandatory code review block on critical/high, scope guard, and back-merge all still run in full. The opt-in only removes the "stop and ask" step.
215
- - Before EACH promote under a standing opt-in, re-verify dev is green with a **fresh** check (the content-level check further down this checklist) — "dev is green" means checked now for this call, not remembered from earlier in the session.
216
-
217
- A dev deploy requires neither form. If the user has not given either form of go-ahead for THIS promote, stop and ask first.
218
-
219
- Approved: option-2 — standing per-slug/session opt-in. Interview: 2026-07-31 (Dave), regen-root session.
220
-
221
- ```
222
- rdc:deploy promote: <slug> → <prod-domain>
223
- [ ] Registry lookup: PROD row (uuid, prod branch=main, watch_paths, url)
224
- [ ] PUBLISH.md status gate: status=active AND prod in environments (block if not)
225
- [ ] 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`)
226
- [ ] Scope resolved: --hotfix <sha> → that commit; else the app-path commits on develop not on main
227
- [ ] Scope guard: promote ONLY this app's paths. NEVER merge develop→main wholesale (drags unrelated WIP to prod)
228
- [ ] Clean worktree off origin/main (never switch the dirty working tree)
229
- [ ] 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):
230
- `export NEXT_PUBLIC_SUPABASE_URL=https://uvojezuorjgqzmhhgluu.supabase.co`
231
- `export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(curl -s http://127.0.0.1:52437/v/supabase-anon)`
232
- 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)
233
- [ ] Apply change: cherry-pick <sha> (or `git checkout <develop-sha> -- <app-paths>`); confirm diff = expected files only
234
- [ ] Mandatory pre-promote code-review (pr-review-toolkit:code-reviewer on the promote diff). Block on critical/high.
235
- [ ] Commit on promote branch; push branch (NOT main directly — the main-push hook blocks raw main pushes)
236
- [ ] Open PR base=main; merge with admin override (`gh pr merge --squash --admin --delete-branch`) — branch protection needs --admin
237
- [ ] EXPLICITLY trigger Coolify deploy — NEVER rely on the GitHub→Coolify webhook (it silently no-ops on some merges even with auto-deploy ON)
238
- [ ] Deployment reached "finished" state (poll coolify_events / deployment status)
239
- [ ] Gate: HTTP 200 on prod domain
240
- [ ] Gate: content-level check — assert the actual changed string is live (200 alone is NOT proof; origin may serve stale)
241
- [ ] Gate: TLS valid
242
- [ ] Gate: metadata audit (see § Metadata Audit below) — BLOCK promote on any missing required item
243
- [ ] Cloudflare cache purged (if proxied)
244
- [ ] deployment_registry updated (last_deploy_at, last_deploy_commit, status)
245
- ✅ rdc:deploy promote: <slug> live in prod — <changed-string> verified
246
- ```
247
-
248
- **The explicit Coolify trigger (the whole point — do not skip):** submit
249
- `clauth ops promote --endpoint <control-plane> --application <registered-coolify-uuid>`.
250
- The local client retrieves `vultr-ops-api-token` internally; the server-side
251
- allowlist owns the Coolify UUID and keeps `coolify-api` out of agent inputs and
252
- transcripts. Poll the returned job or subscribe to
253
- `GET /v1/ops/jobs/<id>/events` until the redacted receipt reaches a terminal
254
- state.
255
-
256
- **Why each guard exists (lessons from 2026-06-05 EF Hooper promote):**
257
- - `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.
258
- - 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.
259
- - `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.
260
- - HTTP 200 was returned by the stale origin the whole time — only a content-level assertion (`curl … | grep '<new string>'`) proves the promote landed.
261
-
262
- ### Mode 6 — convert <slug> (prod runtime static→Next, in place)
263
-
264
- 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.
265
-
266
- **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.
267
-
268
- **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.
269
-
270
- ```
271
- rdc:deploy convert: <slug> → <prod-domain> (static→Next, in place)
272
- [ ] Go-ahead confirmed for THIS slug (production runtime change — architectural)
273
- [ ] Registry lookup: PROD coolify_uuid, fqdn, project/env, current build_pack
274
- [ ] 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.
275
- [ ] Source Next-prod-readiness preflight (BLOCK on any miss):
276
- - apps/<name>/Dockerfile present (prod build) — BLOCK "source not Next-prod-ready: add Dockerfile" if missing
277
- - start/listen port reconciled with intended ports_exposes (no dev-only port like :3214 leaking to prod)
278
- - PUBLISH.md present, build_type=nextjs, status=active, prod in environments
279
- [ ] Mandatory pre-convert code-review (pr-review-toolkit:code-reviewer on the apps/<name> diff being promoted). Block on critical/high.
280
- [ ] 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`
281
- [ ] 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.
282
- [ ] In-place PATCH prod Coolify app <uuid> to nextjs-app template fields:
283
- build_pack=dockerfile · dockerfile_location=/apps/<name>/Dockerfile · base_directory=/ ·
284
- build_command="pnpm turbo run build --filter=<pkg>" · start_command="pnpm --filter=<pkg> start" ·
285
- install_command="pnpm install --frozen-lockfile" · ports_exposes=<port> ·
286
- watch_paths="apps/<name>/**\npackages/**"
287
- (clear publish_directory; static-only field)
288
- [ ] 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.)
289
- [ ] Env vars present in Coolify (compare registry.env_vars_needed); set any missing
290
- [ ] EXPLICITLY trigger deploy: GET /api/v1/deploy?uuid=<PROD_UUID>&force=true — never rely on the webhook
291
- [ ] Deployment reached "finished" state (poll coolify_events)
292
- [ ] Gate: SSR proof — prod HTML now contains `/_next/static` (it is the Next app, not the old static build)
293
- [ ] Gate: HTTP 200 + content-level assertion of a known string from the Next render
294
- [ ] Gate: TLS valid; cache headers correct on HTML
295
- [ ] Gate: metadata audit (see § Metadata Audit) — BLOCK on any required gap
296
- [ ] Cloudflare cache purged (apex + www)
297
- [ ] app_deployments updated: runtime intent → next, notes (converted static→Next <date>), last_deploy_at, last_deploy_commit, status=active
298
- [ ] `apps` row runtime=next confirmed
299
- ✅ rdc:deploy convert: <slug> now Next.js in prod — SSR verified at <prod-domain>
300
- ```
301
-
302
- **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.
303
-
304
- ### Mode 7 — maintenance <service> (private Coolify infrastructure)
305
-
306
- Use this mode for a **private service**, not a website: its consumers run on the
307
- same Coolify Docker network and reach it by internal service alias. It is the
308
- only sanctioned path for creating or changing private infrastructure such as
309
- the Coolify clauth runtime.
310
-
311
- **Authorization and scope:** maintenance may create, deploy, or update only a
312
- service declared in the `private-service` template. It must never add a public
313
- domain, host-port publication, DNS record, Cloudflare route, or public probe.
314
- Destructive retirement remains separately approval-gated and is not part of
315
- this command. Secrets are checked by **key name only**; their values must never
316
- be retrieved, logged, or copied into source control.
317
-
318
- ```
319
- rdc:deploy maintenance: <service>
320
- [ ] Service allowlisted by the `private-service` template; source repository, branch, Dockerfile, port, and health path resolved
321
- [ ] Private-only contract checked: domains empty, no host port mapping, and service alias declared for the Coolify network
322
- [ ] Required secret keys present by name only; values neither read nor emitted
323
- [ ] Dockerfile preflight: service binds its container interface and exposes only its declared internal port
324
- [ ] Create or update performed through the private-service template; no manual Coolify field configuration
325
- [ ] Deploy explicitly triggered and reaches finished state
326
- [ ] Container health gate passes at the declared health path
327
- [ ] In-network probe from an existing Coolify workload reaches the declared service alias and health path
328
- [ ] Public exposure gate passes: no domain, DNS, TLS, or host-port endpoint exists for the service
329
- [ ] Service record and maintenance receipt updated without secret values
330
- ✅ rdc:deploy maintenance: <service> private service healthy on the Coolify network
331
- ```
332
-
333
- **Current allowlisted service — `clauth`:** source `LIFEAI/clauth` on `main`;
334
- Dockerfile `/Dockerfile`; internal port `52437`; health path `/ping`; required
335
- secret key names `CLAUTH_MACHINE_ID` and `CLAUTH_MASTER_PASSWORD`. Its Docker
336
- runtime must bind `0.0.0.0` *inside the container only*. Consumers use the
337
- resolved Coolify network alias, never `localhost` and never a published URL.
338
-
339
- **Verification boundary:** a host-side request to this service is not evidence
340
- of correctness. Verify it from a running workload on the same Coolify network;
341
- the expected result is a successful internal health response while public
342
- domain, TLS, and host-port checks remain absent.
343
-
344
- **PATCH command (in-place build-pack switch):**
345
- ```bash
346
- _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
347
- curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" -H "Content-Type: application/json" \
348
- -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/**"}' \
349
- "$DEPLOY_API_BASE/api/v1/applications/<PROD_UUID>"
350
14
  ```
351
-
352
- ## Metadata Audit
353
-
354
- After a successful deploy or promote, audit the live site's `<head>` metadata. Run this against the deployed URL (not a local file).
355
-
356
- **Check list** (curl the deployed URL, parse the HTML `<head>`):
357
-
358
- | # | Item | How to check | Required | Severity |
359
- |---|------|-------------|----------|----------|
360
- | 1 | `<title>` | grep `<title>` — non-empty, not default/placeholder | yes | block |
361
- | 2 | `<meta name="description">` | content attr ≥ 50 chars, ≤ 160 chars | yes | block on promote; warn on deploy |
362
- | 3 | `<link rel="canonical">` | href present, starts with `https://`, matches the deployed domain | yes | block on promote; warn on deploy |
363
- | 4 | `og:title` | `<meta property="og:title">` present | yes | block on promote; warn on deploy |
364
- | 5 | `og:description` | `<meta property="og:description">` present, ≥ 50 chars | yes | block on promote; warn on deploy |
365
- | 6 | `og:image` | `<meta property="og:image">` present, URL returns HTTP 200 | yes | block on promote; warn on deploy |
366
- | 7 | `og:url` | present, matches canonical | recommended | warn |
367
- | 8 | `twitter:card` | `summary_large_image` or `summary` | recommended | warn |
368
- | 9 | `twitter:image` | present, URL returns HTTP 200 | recommended | warn |
369
- | 10 | Favicon | `<link rel="icon">` present, href returns HTTP 200 | yes | block on promote; warn on deploy |
370
- | 11 | `sitemap.xml` | `<domain>/sitemap.xml` returns HTTP 200 | recommended | warn |
371
- | 12 | `robots.txt` | `<domain>/robots.txt` returns HTTP 200 | recommended | warn |
372
- | 13 | Version | `<meta name="version">` or visible version string in page | yes (project rule) | warn |
373
-
374
- **Behavior by mode:**
375
- - **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.
376
- - **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.
377
- - **Mode 4 (audit):** include the metadata sweep in the fleet scan (one row per app per missing item in the findings table).
378
-
379
- **Implementation — one-liner per check:**
380
- ```bash
381
- URL="https://<domain>"
382
- HEAD=$(curl -s "$URL" | sed -n '/<head/,/<\/head>/p')
383
- echo "$HEAD" | grep -ioE '<title>[^<]+</title>'
384
- echo "$HEAD" | grep -ioE 'name="description"[^>]*content="[^"]*"'
385
- echo "$HEAD" | grep -ioE 'rel="canonical"[^>]*href="[^"]*"'
386
- echo "$HEAD" | grep -ioE 'property="og:title"[^>]*content="[^"]*"'
387
- echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="[^"]*"'
388
- echo "$HEAD" | grep -ioE 'name="twitter:card"[^>]*content="[^"]*"'
389
- echo "$HEAD" | grep -ioE 'rel="icon"[^>]*href="[^"]*"'
390
- echo "$HEAD" | grep -ioE 'name="version"[^>]*content="[^"]*"'
391
- curl -s -o /dev/null -w "%{http_code}" "$URL/sitemap.xml"
392
- curl -s -o /dev/null -w "%{http_code}" "$URL/robots.txt"
393
- OG_IMG=$(echo "$HEAD" | grep -ioE 'property="og:image"[^>]*content="([^"]*)"' | grep -ioE 'https://[^"]*')
394
- [ -n "$OG_IMG" ] && curl -s -o /dev/null -w "%{http_code}" "$OG_IMG"
15
+ rdc:deploy <slug> → node scripts/deploy/deploy.mjs <slug>
16
+ rdc:deploy <slug> promote → node scripts/deploy/deploy.mjs <slug> promote
395
17
  ```
396
18
 
397
- **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.
19
+ Run it from the regen-root checkout you are working in. That is the whole
20
+ instruction. There is no checklist to follow, no mode to pick, and no decision
21
+ for you to make on the success path.
398
22
 
399
- ## Hotlink referer allowlist new media.place.fund consumer domains
23
+ ## Exit 0you are done
400
24
 
401
- When onboarding a NEW brand domain that loads assets from `media.place.fund`, add
402
- that domain to the hotlink-protection Worker's referer allowlist **before
403
- go-live** (lesson 2026-06-14-deploy-issholiving-referer-hotlink-403: a new brand
404
- domain served its own HTML fine but every `media.place.fund` image returned 403
405
- because the hotlink Worker rejected the unknown `Referer`). The image 403 is
406
- invisible to the HTTP-200 / TLS / metadata gates — they probe the HTML document,
407
- not the cross-origin asset.
25
+ Print the URL the program reports, and stop.
408
26
 
409
- - Add the new origin (apex + `www`, dev + prod) to the Worker's referer allowlist
410
- and redeploy the Worker before the brand domain goes live.
411
- - Verify with an explicit `Referer` probe (NOT a bare curl a missing Referer can
412
- pass while the real browser Referer fails):
413
- ```bash
414
- curl -s -o /dev/null -w "%{http_code}" -e "https://<new-brand-domain>/" \
415
- "https://media.place.fund/<a-known-asset-path>" # expect 200, not 403
416
- ```
27
+ Do not verify it again, do not audit anything, do not "while I'm here". The
28
+ program already resolved the slug from the registry, submitted the build and the
29
+ ship, polled the job to a terminal phase, health-probed the deployed URL and
30
+ wrote a receipt under `.rdc/evidence/deploys/`. If any of those had failed, the
31
+ exit code would not have been 0.
417
32
 
418
- ## PUBLISH.md Integration
33
+ ## Exit non-zero — and ONLY then, diagnose
419
34
 
420
- Every deploy reads `PUBLISH.md` from the app's source root to derive `watch_paths` and to register surfaces in Studio `artifact_registry`.
35
+ Every non-zero exit prints a `DEPLOY-FAILED` block naming four things **which
36
+ step, which command, which exit code, and what the tool actually said** —
37
+ followed by the same record as one line of JSON.
421
38
 
422
- ### Step 6 Read PUBLISH.md from the app root
39
+ | exit | meaning | first move |
40
+ |---|---|---|
41
+ | 2 | usage — bad or missing arguments | read the usage line; this program takes a slug and optionally `promote` |
42
+ | 3 | deploy manager unreachable | `pm2 restart regen-deploy-mgr` (it is a local PM2 process), then re-run |
43
+ | 4 | slug not resolvable or not routable | the block names what is missing — a registry row, or an allowlist entry |
44
+ | 5 | a submitted job failed or was rejected | the block carries the manager's own step, command, exit code and stderr tail |
45
+ | 6 | the job succeeded but the health probe did not | bytes shipped, app did not come up — read the job the block names |
46
+ | 7 | polling exceeded the deadline | the job is NOT cancelled; the block gives the URL to read it |
423
47
 
424
- ```bash
425
- MONOREPO_PATH=$(get_app_deployments_monorepo_path "$SLUG")
426
- PUBLISH_MD="$MONOREPO_PATH/PUBLISH.md"
48
+ **Fix the script, never the symptom.** When the deploy is wrong, the repair
49
+ belongs in `scripts/deploy/deploy.mjs` or in the deploy manager it calls. A
50
+ one-off workaround leaves the next agent to rediscover the same failure, which
51
+ is the exact history this command replaced.
427
52
 
428
- if [ ! -f "$PUBLISH_MD" ]; then
429
- echo "WARN: PUBLISH.md missing for $SLUG — using app_deployments.watch_paths only"
430
- # Deploy continues; watch_paths derivation and artifact_registry INSERT are skipped
431
- fi
432
- ```
433
-
434
- PUBLISH.md format: see `C:/Dev/rdc-skills/guides/publish-md-spec.md` (authoritative).
53
+ ## Hand-rolling a deploy is guard-blocked
435
54
 
436
- Required frontmatter fields: `schema_version`, `entity_slug`, `artifact_type`, `environments`, `status`.
437
- One or more `<!-- SURFACE:<id> -->` … `<!-- /SURFACE:<id> -->` blocks per surface (each with `path`, `source_dir`, `build_type`, `visibility`, `cache`, `watch_paths`).
55
+ Over a remote shell, these are refused at the tool layer by the
56
+ `hand-rolled-deploy` rule (both engines):
438
57
 
439
- 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>`.
58
+ - `pm2 restart|reload|start|stop|delete` aimed at an application
59
+ - an application build (`next build`, `pnpm build`, `turbo run build`, …)
60
+ - `git pull` on a deploy target
440
61
 
441
- ### Step 7 Derive watch_paths from PUBLISH.md surfaces
62
+ Direct Coolify deploy triggers are refused by `coolify-direct`.
442
63
 
443
- 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.
64
+ Not blocked, because they are how you diagnose the failure above: remote `pm2
65
+ list` / `pm2 logs` / `pm2 describe`, `docker ps`, anything local, and PM2
66
+ lifecycle operations on infrastructure daemons (codeflow-mcp, fs-mcp, rtp,
67
+ regen-deploy-mgr, pollers).
444
68
 
445
- ```sql
446
- UPDATE app_deployments
447
- SET watch_paths = '<union-of-surface-watch_paths>'
448
- WHERE app_slug = '<slug>';
449
- ```
69
+ Genuine host-level recovery goes through `flow=sv-override`. Reaching for it to
70
+ avoid a failing deploy is not recovery.
450
71
 
451
- Also PATCH the Coolify application's `watch_paths` field:
452
-
453
- ```bash
454
- _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
455
- WATCH_PATHS_JSON=$(derive_watch_paths_union "$PUBLISH_MD")
456
- curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
457
- -H "Content-Type: application/json" \
458
- -d "{\"watch_paths\":\"$WATCH_PATHS_JSON\"}" \
459
- "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
460
- ```
461
-
462
- ### Step 15 — storeArtifact per surface (after successful deploy)
463
-
464
- After the deployment reaches "finished" state, INSERT one row into Studio `artifact_registry` for each surface declared in PUBLISH.md:
465
-
466
- | Column | Value |
467
- |--------|-------|
468
- | `entity_slug` | from PUBLISH.md frontmatter `entity_slug` |
469
- | `artifact_type` | from PUBLISH.md frontmatter `artifact_type` |
470
- | `canonical_url` | `https://<app_deployments.url><surface.path>` |
471
- | `surface_id` | surface name from `<!-- SURFACE:<id> -->` marker |
472
- | `commit_sha` | HEAD SHA of the deploy |
473
- | `published_at` | `now()` |
474
-
475
- Use the Supabase MCP (`mcp__claude_ai_Supabase__execute_sql`) from the supervisor session:
476
-
477
- ```sql
478
- INSERT INTO artifact_registry (entity_slug, artifact_type, canonical_url, surface_id, commit_sha, published_at)
479
- VALUES ('<entity_slug>', '<artifact_type>', 'https://<url><path>', '<surface_id>', '<commit_sha>', now())
480
- ON CONFLICT (entity_slug, surface_id) DO UPDATE SET
481
- canonical_url = EXCLUDED.canonical_url,
482
- commit_sha = EXCLUDED.commit_sha,
483
- published_at = EXCLUDED.published_at;
484
- ```
485
-
486
- 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.
487
-
488
- ## Control-plane Access — clauth bearer jobs
489
-
490
- There is no Coolify MCP server. Agent-facing deploy and promotion operations use
491
- the clauth control plane, not raw PM2, SSH, or the Coolify deploy-trigger API.
492
- The local fixed client obtains `vultr-ops-api-token` from local clauth and sends
493
- it as `Authorization: Bearer`; it never prints the token.
494
-
495
- ```bash
496
- clauth ops catalog --endpoint <control-plane>
497
- clauth ops list --endpoint <control-plane>
498
- clauth ops describe --endpoint <control-plane> --target <pm2-name>
499
- clauth ops deploy --endpoint <control-plane> --application <slug> --ref <registered-ref>
500
- clauth ops promote --endpoint <control-plane> --application <registered-coolify-uuid>
501
- clauth ops job --endpoint <control-plane> --job <job-id>
502
- ```
503
-
504
- Returns are accepted jobs (`202`) and the server persists redacted `queued`,
505
- `running`, `building`, `waiting`, and terminal events. Generic PM2 catalog
506
- operations are policy-disabled unless the server owner explicitly allowlists the
507
- operation and target.
508
-
509
- **Domain change / namespace migration** (lesson 2026-06-13-deploy-media-manager-namespace-migration):
510
- PATCH `applications/<uuid>` with `{"domains":"https://<host>"}` — never `fqdn`.
511
- For an app NEW to main (first prod deploy) also bring the app + a lockfile importer
512
- to main (clean worktree off origin/main, `pnpm install --lockfile-only`, verify the
513
- diff = additions for `apps/<name>:` only); resolve the lockfile-guard vs scope-guard
514
- collision by committing the app+lockfile as a SINGLE `chore(infra):`-subject commit
515
- (the scope-guard's documented escape hatch) so both pre-commit guards pass.
516
-
517
- **Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
518
-
519
- If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
520
- ```
521
- BLOCKED: credential provider is not responding.
522
- Fix: start the project's credential provider or configure deployment credentials through env vars, then retry.
523
- I cannot proceed until this is resolved.
524
- ```
525
-
526
- ## Deployment Event Log — `coolify_events`
527
-
528
- 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.
529
-
530
- Query the last 5 events for an app:
531
- ```sql
532
- SELECT created_at, event_type, status, branch, commit_hash, duration_seconds
533
- FROM coolify_events
534
- WHERE app_uuid = '<uuid>' OR app_name = '<slug>'
535
- ORDER BY created_at DESC LIMIT 5;
536
- ```
72
+ ## What this command deliberately does NOT do
537
73
 
538
- Fields:
539
- - `app_uuid` Coolify application UUID (matches `app_deployments.coolify_uuid`)
540
- - `event_type` — `started | succeeded | failed | cancelled` (canonical values; consult webhook receiver for full enum)
541
- - `status` — overall deploy status
542
- - `branch`, `commit_hash`, `commit_message` — git context
543
- - `duration_seconds` — total deploy time
544
- - `payload` — full webhook payload (jsonb) for forensic debugging
74
+ `deploy` and `promote` are the only two actions. The program refuses anything
75
+ else by name rather than guessing.
545
76
 
546
- 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.
77
+ The previous version of this file carried seven modes`new`, `diagnose`,
78
+ `audit`, `convert`, `maintenance` alongside deploy and promote — across 566
79
+ lines of checklists and prose. Every one of them was an instruction an agent had
80
+ to read and interpret, and interpretation is why deployments broke differently
81
+ each time. **Those modes are not implemented here and are not silently available
82
+ elsewhere** — that is a real reduction in what this skill offers, recorded rather
83
+ than glossed:
547
84
 
548
- ## Capture lessons (exit step)
85
+ | dropped mode | where the capability stands now |
86
+ |---|---|
87
+ | `new <slug>` | not automated — register the app in `app_deployments` first; this program ships registered apps, it does not create them |
88
+ | `diagnose <slug>` | the failure block plus the manager's job record (`GET :52438/v1/local/jobs/<id>`) and its two-box dashboard |
89
+ | `audit` | the manager's dashboard (`GET :52438/v1/local/dashboard?box=local\|vultr`) — PM2 state joined against the registry, with URL health |
90
+ | `convert` | an architectural change; goes through `.claude/rules/architectural-change-approval.md` |
91
+ | `maintenance <service>` | private Coolify infrastructure; unchanged and not part of this command |
549
92
 
550
- 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.
93
+ The removed prose also carried real, hard-won operational knowledge — the
94
+ `next start` `BUILD_ID` crash-loop, the `media.place.fund` hotlink referer
95
+ allowlist, the `<head>` metadata audit, the lockfile-importer trap on a first
96
+ promote to `main`. That knowledge is in git history for this file and in the
97
+ named `.rdc/lessons/` entries it cited. **It was removed from here because prose
98
+ that duplicates behaviour drifts from it** — not because it stopped being true.
99
+ Where such a check belongs in the pipeline, it belongs in the program or in the
100
+ manager, where it runs, rather than in a document that hopes to be read.
551
101
 
552
- ## References
102
+ ## Authorization
553
103
 
554
- - Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
555
- - Rules / registry RPCs / hard limits: `.claude/context/coolify-deployment.md`
556
- - Infrastructure constants:
557
- ```
558
- Server UUID: ih386anenvvvn6fy1umtyow0
559
- Server IP: 64.237.54.189
560
- Dashboard: <deployment-dashboard-url>
561
- GitHub App UUID: xdmcy60putp5h9j7k4kwg9c3
562
- ```
104
+ A dev deploy needs no approval. `promote` puts bytes on production and needs
105
+ explicit go-ahead for that call. The program itself refuses to promote anything
106
+ that is not an image-model app, because promote retags the digest dev already
107
+ runs — **a promote never builds**, and one that fell back to building would ship
108
+ bytes production has never seen under a name claiming it had.
563
109
 
564
- ## Supersedes
110
+ ## Verification
565
111
 
566
- `coolify-deploy` standalone skill (kept for back-compat; new work uses `rdc:deploy`).
112
+ `node scripts/deploy/__tests__/deploy.test.mjs` (node --test) covers the exit
113
+ contract; `scripts/deploy/atf.ladder.mjs` holds golden-capture records of every
114
+ enumerated failure; `scripts/deploy/atf.probe.mjs` is the live probe against the
115
+ running manager. Full climb: `node C:/Dev/atf/bin/ladder-run.mjs scripts/deploy`.