@lifeaitools/rdc-skills 0.20.3 → 0.20.5

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,6 +1,6 @@
1
1
  {
2
2
  "name": "rdc",
3
- "version": "0.20.3",
3
+ "version": "0.20.5",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight unattended builds with work-item tracking and TDD enforcement.",
5
5
  "author": {
6
6
  "name": "LIFEAI",
package/git-sha.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "sha": "9116bba71645a39735855cd9d8de292023f16496"
2
+ "sha": "564ace3c41ccf10d58d8c0cc8c0709d7574b653d"
3
3
  }
@@ -37,6 +37,16 @@ thing they need: **is this working or not, and what step are we on?**
37
37
  - `⚠️ <skill>: <N findings> — <next action>`
38
38
  - `❌ <skill>: <one-sentence reason>`
39
39
 
40
+ ⛔ **The verdict emoji (✅ / ⚠️ / ❌) MUST be the FIRST character of the final
41
+ line — never prefix it with `**Verdict:**` or any other text.** The enforcing
42
+ Stop-hook checks that a line *begins* with the emoji; `**Verdict:** ✅ PASS …`
43
+ reads as a verdict to a human but FAILS the machine check because the line
44
+ starts with `**Verdict:`, not `✅` (lesson 2026-06-08-collab-verdict-line-must-start-with-emoji).
45
+
46
+ > This guide is the source under `guides/output-contract.md`; it is mirrored to
47
+ > `regen-root/.rdc/guides/output-contract.md` by the installer. Edit only the
48
+ > rdc-skills source here — never the mirror.
49
+
40
50
  7. **Interactive checklists only when human input is required.** If the skill
41
51
  needs a decision (pick an epic, confirm a destructive op), ask ONE question,
42
52
  then resume.
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart hook — hard gate for the RDC skills runtime.
4
+ *
5
+ * This hook repairs the approved install path when it can do so safely:
6
+ * npm install -g @lifeaitools/rdc-skills@latest
7
+ * rdc-skills-install --profile lifeai --project-root <repo> --write-startup-blocks
8
+ *
9
+ * It then verifies that the local MCP server answers /health and sees a real
10
+ * skills catalog. If repair fails, startup is blocked before agents trust stale
11
+ * copied skill files.
12
+ */
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const { execFileSync, execSync } = require('child_process');
19
+ const hookLog = require('./hook-logger');
20
+
21
+ const MIN_SKILLS = 20;
22
+ const MCP_HEALTH = 'http://127.0.0.1:3110/health';
23
+ const PACKAGE = '@lifeaitools/rdc-skills';
24
+ const stampPath = path.join(os.tmpdir(), 'rdc-skills-environment-last-repair.json');
25
+
26
+ function q(value) {
27
+ return `"${String(value).replace(/"/g, '\\"')}"`;
28
+ }
29
+
30
+ function run(command, args, opts = {}) {
31
+ return execFileSync(command, args, {
32
+ encoding: 'utf8',
33
+ stdio: ['ignore', 'pipe', 'pipe'],
34
+ timeout: opts.timeout || 30000,
35
+ cwd: opts.cwd || process.cwd(),
36
+ env: { ...process.env, ...(opts.env || {}) },
37
+ }).trim();
38
+ }
39
+
40
+ function shell(command, opts = {}) {
41
+ return execSync(command, {
42
+ encoding: 'utf8',
43
+ stdio: ['ignore', 'pipe', 'pipe'],
44
+ timeout: opts.timeout || 30000,
45
+ cwd: opts.cwd || process.cwd(),
46
+ env: { ...process.env, ...(opts.env || {}) },
47
+ }).trim();
48
+ }
49
+
50
+ function commandExists(name) {
51
+ try {
52
+ if (process.platform === 'win32') run('where.exe', [name], { timeout: 5000 });
53
+ else run('which', [name], { timeout: 5000 });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function projectRoot() {
61
+ try {
62
+ return shell('git rev-parse --show-toplevel', { timeout: 5000 });
63
+ } catch {
64
+ return process.cwd();
65
+ }
66
+ }
67
+
68
+ function globalPackageJson() {
69
+ try {
70
+ const root = shell('npm root -g', { timeout: 10000 });
71
+ const pkg = path.join(root, '@lifeaitools', 'rdc-skills', 'package.json');
72
+ if (!fs.existsSync(pkg)) return null;
73
+ return JSON.parse(fs.readFileSync(pkg, 'utf8'));
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ async function health() {
80
+ try {
81
+ const res = await fetch(MCP_HEALTH, { signal: AbortSignal.timeout(4000) });
82
+ if (!res.ok) return null;
83
+ return await res.json();
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function recentlyRepaired() {
90
+ try {
91
+ const data = JSON.parse(fs.readFileSync(stampPath, 'utf8'));
92
+ return Date.now() - Date.parse(data.ts) < 10 * 60 * 1000;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function markRepaired(reason) {
99
+ try {
100
+ fs.writeFileSync(stampPath, JSON.stringify({ ts: new Date().toISOString(), reason }, null, 2));
101
+ } catch {
102
+ /* best effort */
103
+ }
104
+ }
105
+
106
+ function repair(reason) {
107
+ hookLog('check-rdc-environment', 'SessionStart', 'repair', { reason });
108
+ shell(`npm install -g ${q(`${PACKAGE}@latest`)}`, { timeout: 120000 });
109
+ shell(`rdc-skills-install --profile lifeai --project-root ${q(projectRoot())} --write-startup-blocks`, { timeout: 180000 });
110
+ markRepaired(reason);
111
+ }
112
+
113
+ function block(message, details = {}) {
114
+ hookLog('check-rdc-environment', 'SessionStart', 'block', { message, ...details });
115
+ process.stdout.write(JSON.stringify({
116
+ systemMessage:
117
+ `HARD BLOCK — RDC skills environment is not healthy.\n\n` +
118
+ `${message}\n\n` +
119
+ `Do not proceed with RDC work until the approved install path is repaired:\n` +
120
+ `npm install -g @lifeaitools/rdc-skills@latest\n` +
121
+ `rdc-skills-install --profile lifeai --project-root ${projectRoot()} --write-startup-blocks`
122
+ }));
123
+ process.exit(1);
124
+ }
125
+
126
+ async function main() {
127
+ const initialPkg = globalPackageJson();
128
+ const initialHealth = await health();
129
+ const reasons = [];
130
+
131
+ if (!initialPkg) reasons.push('global package missing');
132
+ if (!commandExists('rdc-skills-install')) reasons.push('installer command missing');
133
+ if (!initialHealth || initialHealth.status !== 'ok' || Number(initialHealth.skills || 0) < MIN_SKILLS) {
134
+ reasons.push('local MCP health/catalog invalid');
135
+ }
136
+
137
+ if (reasons.length) {
138
+ if (recentlyRepaired()) {
139
+ block(`RDC skills still unhealthy after a recent repair attempt: ${reasons.join(', ')}`);
140
+ }
141
+ try {
142
+ repair(reasons.join(', '));
143
+ } catch (err) {
144
+ block(`Automatic RDC skills repair failed: ${err.message}`, { reasons });
145
+ }
146
+ }
147
+
148
+ const finalPkg = globalPackageJson();
149
+ const finalHealth = await health();
150
+ if (!finalPkg) block('Global @lifeaitools/rdc-skills package is missing after repair.');
151
+ if (!commandExists('rdc-skills-install')) block('rdc-skills-install is missing after repair.');
152
+ if (!finalHealth || finalHealth.status !== 'ok' || Number(finalHealth.skills || 0) < MIN_SKILLS) {
153
+ block(`rdc-skills MCP is unhealthy after repair. Health: ${JSON.stringify(finalHealth)}`);
154
+ }
155
+
156
+ hookLog('check-rdc-environment', 'SessionStart', 'pass', {
157
+ version: finalPkg.version,
158
+ mcpVersion: finalHealth.version,
159
+ skills: finalHealth.skills,
160
+ });
161
+ process.exit(0);
162
+ }
163
+
164
+ main().catch((err) => block(`RDC skills environment check crashed: ${err.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.20.3",
3
+ "version": "0.20.5",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -38,7 +38,7 @@
38
38
  "test:mcp:remote": "node tests/mcp.test.mjs --remote",
39
39
  "mcp": "node bin/rdc-skills-mcp.mjs",
40
40
  "rebuild-mcp": "node scripts/rebuild-mcp.mjs",
41
- "prepack": "node scripts/validate-publish-manifests.js --mode warn || true && node scripts/validate-place-histories.js --mode warn || true && node scripts/stamp-git-sha.mjs"
41
+ "prepack": "node scripts/prepack.mjs"
42
42
  },
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -707,6 +707,7 @@ function buildHooksConfig(hooksDir, profile = 'core') {
707
707
 
708
708
  if (profile === 'lifeai') {
709
709
  config.SessionStart = [{ hooks: [
710
+ cmd('check-rdc-environment.js', 'Checking RDC skills runtime...'),
710
711
  cmd('check-cwd.js'),
711
712
  cmd('check-stale-work-items.js', 'Checking for stale work items...'),
712
713
  ]}];
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cross-platform prepack steps.
4
+ *
5
+ * The validators are advisory in package builds; they report drift in the
6
+ * LIFEAI publishing corpus, but they must not make `npm pack` fail on Windows
7
+ * because cmd.exe does not provide a POSIX `true` command (the old
8
+ * `... || true && ...` chain silently short-circuited under cmd.exe).
9
+ *
10
+ * stamp-git-sha bakes the current HEAD into git-sha.json so the MCP server can
11
+ * report a PROVABLE running commit on /health even when it runs from the npm
12
+ * install (no .git at runtime). It runs LAST and is required, not advisory.
13
+ */
14
+
15
+ import { spawnSync } from 'node:child_process';
16
+
17
+ const advisory = [
18
+ ['scripts/validate-publish-manifests.js', '--mode', 'warn'],
19
+ ['scripts/validate-place-histories.js', '--mode', 'warn'],
20
+ ];
21
+
22
+ for (const args of advisory) {
23
+ const result = spawnSync(process.execPath, args, { stdio: 'inherit' });
24
+ if (result.error) {
25
+ console.warn(`[prepack] advisory check failed to launch: ${args[0]}: ${result.error.message}`);
26
+ } else if (result.status !== 0) {
27
+ console.warn(`[prepack] advisory check exited ${result.status}: ${args[0]}`);
28
+ }
29
+ }
30
+
31
+ // Required: stamp the git SHA (never fails the build — writes 'unknown' if git is unavailable).
32
+ spawnSync(process.execPath, ['scripts/stamp-git-sha.mjs'], { stdio: 'inherit' });
@@ -331,6 +331,14 @@ function expectedSkillName(dirOrFile) {
331
331
  return "rdc:" + base;
332
332
  }
333
333
 
334
+ function expectedFrontmatterName(dirOrFile, frontmatterName) {
335
+ const base = basename(dirOrFile, ".md");
336
+ if (frontmatterName && frontmatterName.startsWith("rdc:")) {
337
+ return expectedSkillName(dirOrFile);
338
+ }
339
+ return base;
340
+ }
341
+
334
342
  function findReferencedFiles(body) {
335
343
  const refs = [];
336
344
  const guideRe = /(?:^|[\s(`'"/])(?:\.rdc\/)?guides\/([\w/-]+\.md)/g;
@@ -473,7 +481,7 @@ function auditSkill(filepath) {
473
481
  }
474
482
  }
475
483
 
476
- const expected = expectedSkillName(skillDirOrFile);
484
+ const expected = expectedFrontmatterName(skillDirOrFile, fm.name);
477
485
  if (expected && fm.name !== expected) {
478
486
  addFinding(
479
487
  result,
@@ -199,6 +199,18 @@ Read the task title and description, then:
199
199
  isolation: "worktree"
200
200
  ```
201
201
 
202
+ ### ⛔ Concurrent committers ⇒ worktree isolation, no exceptions (docs included)
203
+ For ANY parallel agent dispatch where the agents COMMIT — **including a pure
204
+ docs wave** — force `isolation: "worktree"` so each agent commits in its own
205
+ tree and the orchestrator merges. NEVER fan out concurrent committers onto one
206
+ shared working tree (lesson 2026-06-13-build-concurrent-agents-same-branch-git-race:
207
+ three parallel doc agents on one `develop` tree raced on the git index/stash —
208
+ one commit swept in another agent's staged-but-uncommitted files under the wrong
209
+ message, two agents reported the SAME SHA, and a pre-commit `sync:docs` ref-lock
210
+ race misattributed authorship). "It's just docs" is NOT an exemption. The only
211
+ alternative to worktree isolation is running the committing agents **sequentially**
212
+ on the shared tree (one commits and pushes before the next starts).
213
+
202
214
  **Agent model routing — pick per task, not per wave.** The supervisor session model does NOT cascade to agents; you must set `model` explicitly on every dispatch.
203
215
 
204
216
  | Task character | Model | When to pick it |
@@ -219,6 +231,31 @@ Read the task title and description, then:
219
231
  Without `max_turns: 70`, agents hit the default turn cap mid-task and stop.
220
232
  `isolation: "worktree"` gives each agent its own git worktree and branch — eliminates push race conditions and index lock contention when multiple agents commit in parallel. The supervisor merges worktree branches after each wave (Step 9).
221
233
 
234
+ ### ⛔ Worktree base must equal develop HEAD — assert BEFORE every isolated wave
235
+ The worktree-isolation harness has shipped worktrees pinned to a STALE base
236
+ commit (lessons 2026-06-10-build-worktree-stale-base, 2026-06-11-build-worktree-stale-base:
237
+ agents branched 464 commits / 4 days behind develop HEAD, on a tree where the
238
+ target app did not yet exist — their diffs would have silently reverted merged
239
+ work or operated on a deleted structure). **Before dispatching ANY
240
+ `isolation:"worktree"` wave**, assert each worktree's base equals current
241
+ develop HEAD:
242
+ ```bash
243
+ DEV_HEAD=$(git rev-parse develop)
244
+ # After worktrees are created, for each agent worktree:
245
+ git worktree list # compare each agent worktree's SHA to $DEV_HEAD
246
+ # If a worktree base != $DEV_HEAD (it is behind), the wave is UNSAFE.
247
+ ```
248
+ - If any worktree base is behind `$DEV_HEAD`: **ABORT isolation for this wave.**
249
+ Do NOT merge stale worktree output. Pivot to **sequential, non-isolated
250
+ dispatch on a real `develop` checkout** (one disjoint WP at a time to avoid
251
+ `.git/index` races; the supervisor pushes) — the same reason the validator
252
+ runs non-isolated (it must see merged develop).
253
+ - Also at every merge: `git show <branch>:<key-file> | grep -c <symbol-a-prior-wave-introduced>`
254
+ — a 0 where there should be ≥1 means the branch is stale or deleted a shared
255
+ export; resolve to `--ours` and re-apply that wave's real delta on current HEAD.
256
+ esbuild/tsc PASS is necessary, not sufficient — pair it with a grep gate on
257
+ the symbols a refactor must preserve.
258
+
222
259
  ### Forked agents vs. standalone agents
223
260
 
224
261
  **When the supervisor has already read the plan** (via a prior `Read` tool call in the same session),
@@ -237,6 +274,21 @@ Read the task title and description, then:
237
274
  **When the supervisor has NOT read the plan** (e.g. dispatching from a fresh `rdc:build` call with
238
275
  only an epic ID), the agent has no plan context — write a full briefing prompt with all specs.
239
276
 
277
+ ### ⛔ Reuse contract — when a WP builds on an existing subsystem
278
+ When a work package extends an existing subsystem, "compose adapter + X"
279
+ under-specifies reuse and lets an agent legitimately re-author markup/logic the
280
+ subsystem already exposes (lesson 2026-06-11-build-reuse-existing-engine-prompt:
281
+ an agent set up to reinvent a grid when the card engine already shipped four
282
+ `CardLayout` display types + a full `parseCommand → CardSpec → adapter → CardModel[]`
283
+ calling sequence). The dispatch prompt MUST:
284
+ 1. **Enumerate the existing public API the agent must reuse, BY FILE** — types/enums
285
+ (`packages/.../types.ts:NN`), calling-sequence functions, AND the existing
286
+ display/render components — not just the data adapter.
287
+ 2. **Mark which seams are extend/delegate-only.** State explicitly: "thread a
288
+ pass-through prop (e.g. `layout`) to the existing components; do NOT
289
+ reimplement layout/render." Verify at WP review that the agent reused the
290
+ named parser/adapter/components and did not hand-roll a parallel implementation.
291
+
240
292
  ---
241
293
 
242
294
  ### Required agent prompt contents
@@ -270,6 +322,25 @@ Read the task title and description, then:
270
322
  - Use `run_in_background: true` for parallel execution
271
323
  - NEVER let agents overlap on the same files
272
324
 
325
+ ### ⛔ Agent Definition-of-Done additions (include in every prompt)
326
+ - **Declare every import in the package's OWN package.json.** Every import an
327
+ agent adds MUST be present in that package's own `package.json`
328
+ dependencies/devDependencies — never rely on monorepo-hoisted deps. A pnpm
329
+ worktree can import a root-hoisted dep (e.g. `vitest`,
330
+ `@supabase/supabase-js`) so `tsc`/`vitest` PASS in the worktree, then `tsc`
331
+ FAILS on a clean develop checkout after merge (lesson
332
+ 2026-06-10-build-card-engine-agent-deps). Agent prompt line: *"Verify that
333
+ every import you add is declared in this package's own package.json — do not
334
+ rely on hoisted monorepo deps."*
335
+ - **A server/MCP/API task is NOT done without a committed automated test that
336
+ ships in the SAME commit and exercises EVERY surface.** Manual curl / a single
337
+ `/health` 200 is a proxy, not coverage (lesson 2026-06-10-build-weak-dod-no-tests).
338
+ For a collection (MCP skills, API routes, CLI commands): loop over ALL items
339
+ and assert `output == source` — never a single spot check. Wire an npm script
340
+ and run it green before the item leaves `review`. Agent prompt line: *"This
341
+ task is not done until a committed test in the same commit exercises every
342
+ surface; for collections, loop all items and assert output == source."*
343
+
273
344
  8. **Post-wave test gate (mandatory):**
274
345
  After all agents in a wave complete, before proceeding:
275
346
  ```bash
@@ -98,8 +98,17 @@ Print `[rdc:collab] Still listening...` and retry SSE immediately. After 10
98
98
  consecutive 30s timeouts (5 min idle), print a longer heartbeat but keep
99
99
  looping.
100
100
 
101
- **If curl fails (daemon restart, connection refused, non-200):** fall back to
102
- polling path below.
101
+ **⛔ curl exit 28 (`--max-time`) is SUCCESS, not failure, on an SSE read.**
102
+ `curl --max-time 30` ALWAYS exits 28 at the timeout boundary — that is normal for
103
+ a long-lived SSE stream and says nothing about delivery. If a `data:` event was
104
+ received in the output, process it and proceed to Step 4 — do NOT treat exit 28 as
105
+ a curl failure (lesson 2026-06-08-collab-sse-exit-28-is-success: exit 28 arrived
106
+ together with a full `event: message` / `data: {...}` payload, and reading it as a
107
+ failure misclassified a zero-latency delivery). Only **connection-refused or a
108
+ non-200** is a real curl failure that triggers the polling fallback.
109
+
110
+ **If curl fails (daemon restart, connection refused, non-200 — NOT a bare exit 28
111
+ with a delivered `data:` event):** fall back to polling path below.
103
112
 
104
113
  ### Fallback path — polling (2s interval)
105
114
 
@@ -215,6 +224,15 @@ If Dave types in this terminal during a turn:
215
224
  - Treat it as an override injected into the current task
216
225
  - Acknowledge it in your `chitchat_reply` response
217
226
  - If it changes direction mid-task, note what you stopped and why
227
+ - ⛔ **When an interjection appears to CONTRADICT the task premise, restate your
228
+ understanding in ONE sentence and confirm before branching into a wide
229
+ `AskUserQuestion` menu.** A tight "I read this as X — correct?" reconciles faster
230
+ than a multiple-choice and avoids acting on a misread premise (lesson
231
+ 2026-06-08-collab-premise-contradicting-interjection: "there is no pm2 this
232
+ replaces it" was read as "PM2 is abolished as the transport" and triggered a
233
+ 3-option transport menu, when it meant "there was no dev *site* yet — push to
234
+ the unchanged PM2 path"; the wide menu over-committed to one interpretation and
235
+ cost a round).
218
236
 
219
237
  ## Capture lessons (exit step)
220
238
 
@@ -66,6 +66,28 @@ rdc:deploy: <slug> → <domain>
66
66
  ✅ rdc:deploy: <slug> deployed in Nm Ns
67
67
  ```
68
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
+
69
91
  ### Mode 2 — new <slug>
70
92
 
71
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.
@@ -112,6 +134,40 @@ rdc:deploy diagnose: <slug>
112
134
  ⚠️ rdc:deploy diagnose: <root cause in one sentence> — fix: <one command>
113
135
  ```
114
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
+
115
171
  ### Mode 4 — audit
116
172
 
117
173
  ```
@@ -159,6 +215,10 @@ rdc:deploy promote: <slug> → <prod-domain>
159
215
  [ ] Scope resolved: --hotfix <sha> → that commit; else the app-path commits on develop not on main
160
216
  [ ] Scope guard: promote ONLY this app's paths. NEVER merge develop→main wholesale (drags unrelated WIP to prod)
161
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)
162
222
  [ ] Apply change: cherry-pick <sha> (or `git checkout <develop-sha> -- <app-paths>`); confirm diff = expected files only
163
223
  [ ] Mandatory pre-promote code-review (pr-review-toolkit:code-reviewer on the promote diff). Block on critical/high.
164
224
  [ ] Commit on promote branch; push branch (NOT main directly — the main-push hook blocks raw main pushes)
@@ -393,8 +453,23 @@ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
393
453
  -H "Content-Type: application/json" \
394
454
  -d '{"watch_paths":"apps/<name>/**\npackages/**"}' \
395
455
  "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
456
+
457
+ # Change the app's domain — the writable field is "domains", NOT "fqdn"
458
+ # (Coolify v4 PATCH rejects {"fqdn":...} with "This field is not allowed"; fqdn is read-only/derived)
459
+ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
460
+ -H "Content-Type: application/json" \
461
+ -d '{"domains":"https://<host>"}' \
462
+ "$DEPLOY_API_BASE/api/v1/applications/<uuid>"
396
463
  ```
397
464
 
465
+ **Domain change / namespace migration** (lesson 2026-06-13-deploy-media-manager-namespace-migration):
466
+ PATCH `applications/<uuid>` with `{"domains":"https://<host>"}` — never `fqdn`.
467
+ For an app NEW to main (first prod deploy) also bring the app + a lockfile importer
468
+ to main (clean worktree off origin/main, `pnpm install --lockfile-only`, verify the
469
+ diff = additions for `apps/<name>:` only); resolve the lockfile-guard vs scope-guard
470
+ collision by committing the app+lockfile as a SINGLE `chore(infra):`-subject commit
471
+ (the scope-guard's documented escape hatch) so both pre-commit guards pass.
472
+
398
473
  **Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
399
474
 
400
475
  If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
@@ -251,6 +251,24 @@ Q5. What SSL path?
251
251
 
252
252
  Embed all five answers into the infra task description verbatim before handing to the agent.
253
253
 
254
+ ## MCP / infra plans — mirror the closest sibling deployment FIRST
255
+
256
+ Before writing any plan that stands up an MCP server or infra service, read how the
257
+ **closest existing sibling** is actually deployed (`.mcp.json` + its PM2/tunnel or
258
+ Coolify/Docker config) and **cite it in the plan**. Two MCP topologies coexist and
259
+ applying the wrong reference over-engineers the design (lesson
260
+ 2026-06-10-plan-mirror-sibling-mcp-pattern: a first MCP plan proposed
261
+ Docker + Coolify + a Cloudflare-proxied origin + runtime GitHub-pull when the real
262
+ pattern was a local Node process + clauth-managed tunnel, exactly like codeflow-mcp):
263
+
264
+ - **LOCAL-MCP reference = `codeflow-mcp`** — local Node process (`:3109`) + clauth-managed
265
+ tunnel ingress. Mirror this for MCPs that live on Dave's box.
266
+ - **REMOTE-MCP reference = `web-research` / `regen-media`** — Coolify/Docker, Cloudflare-proxied
267
+ origin (`.claude/rules/mcp-endpoint-design.md` covers the REMOTE class only).
268
+
269
+ Decide which class applies and name the sibling in the plan's Design Decisions before
270
+ designing topology. Do not reach for the REMOTE rule by default.
271
+
254
272
  ## Capture lessons (exit step)
255
273
 
256
274
  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>-plan-<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.
@@ -99,6 +99,31 @@ npm view <package-name> version
99
99
 
100
100
  Never use `--force` or bypass hooks. If a hook fails, fix the cause.
101
101
 
102
+ ## ⛔ Standalone-repo staging guard (no `git add -A`)
103
+
104
+ Standalone repos (`rdc-skills`, `clauth`, `build-corpus`) have NO pre-commit
105
+ doc-sync/scope guard to catch contamination. In them:
106
+
107
+ - **REFUSE `git add -A` / `git add .`.** Stage explicit declared paths only
108
+ (`git add skills/<name>/SKILL.md package.json ...`).
109
+ - Run `git status --porcelain` FIRST. If untracked files exist that are not part
110
+ of the declared change, STOP — list or stash them; do not sweep them in.
111
+ - **Pre-tag guard: refuse to tag if `git diff --cached --name-only` includes any
112
+ path outside the declared change set.** A broad add swept 4 pre-existing
113
+ untracked skill files into a tagged release that CI published before anyone
114
+ noticed (lesson 2026-06-08-release-git-add-all-swept-untracked-wip). Same
115
+ dirty-tree contamination class as a lockfile generated against a dirty tree.
116
+
117
+ ## ⛔ Cross-platform prepack + verify the PUBLISHED tarball
118
+
119
+ - **Prepack must be OS-agnostic.** A bash-style `prepack` chain (`node A || true && node B || true && node stamp`) short-circuits under Windows **cmd.exe** (npm runs lifecycle scripts via cmd, not bash; `true` is not a cmd builtin and `||`/`&&` evaluate differently), so an appended step silently never runs (lesson 2026-06-13-release-windows-cmd-prepack-shortcircuit). When a prepack step must run cross-platform, use a node wrapper / `shx` / `cross-env` — never rely on `|| true` shell semantics that differ between cmd and bash.
120
+ - **Validate the PUBLISHED artifact, not a local Windows `npm pack`.** A local Windows `npm pack` is NOT a faithful rehearsal of the CI (ubuntu/bash) publish. After publish, verify the real tarball:
121
+ ```bash
122
+ npm pack <pkg>@<version> # downloads the PUBLISHED tarball
123
+ tar -xzOf <pkg>-<version>.tgz package/<file> # inspect the shipped file
124
+ ```
125
+ Confirm the published artifact carries what CI's prepack was supposed to stamp (e.g. `git_sha == tag commit`).
126
+
102
127
  ## RDC Skills Package
103
128
 
104
129
  For this package, prefer the npm installer binary after publish: