@lifeaitools/rdc-skills 0.35.23 → 0.35.25
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/self-test.yml +34 -34
- package/MANIFEST.md +224 -224
- package/RELEASE.md +53 -11
- package/deploy/install-systemd.sh +29 -6
- package/deploy/systemd/rdc-skills-mcp-update.service +15 -0
- package/deploy/systemd/rdc-skills-mcp-update.timer +9 -0
- package/deploy/update-from-tag.sh +382 -0
- package/guides/agents/backend.md +102 -102
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +86 -86
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +91 -91
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +272 -272
- package/guides/agents/verify.md +119 -119
- package/guides/agents/viz.md +106 -106
- package/guides/orchestration-epic.md +17 -17
- package/guides/output-contract.md +8 -0
- package/guides/work-contract.md +194 -0
- package/package.json +3 -2
- package/scripts/lib/guide-content-rules.mjs +49 -0
- package/scripts/self-test.mjs +1439 -1459
- package/scripts/test-guide-validator.mjs +25 -24
- package/skills/behavior-audit/agents/openai.yaml +4 -4
- package/skills/build/SKILL.md +81 -29
- package/skills/fixit/SKILL.md +16 -0
- package/skills/open/SKILL.md +35 -5
- package/skills/overnight/SKILL.md +3 -1
- package/skills/plan/SKILL.md +21 -23
- package/skills/tests/onramp.test.json +101 -101
- package/skills/tests/rdc-env.test.json +12 -12
- package/skills/tests/rdc-new-model.test.json +12 -12
- package/skills/tests/rdc-refactor.test.json +29 -29
- package/skills/tests/rdc-regen-media.test.json +29 -29
- package/tests/deploy-updater.test.mjs +608 -0
- package/.rdc/evidence/orchestrator-rework-strikes/a9fad8b4716ee35e5f5d0047.json +0 -1
- package/.rdc/last_seen.json +0 -8
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Keep the public RDC Skills MCP on the newest release tag
|
|
3
|
+
After=network-online.target
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=oneshot
|
|
8
|
+
User=root
|
|
9
|
+
WorkingDirectory=/srv/regen/rdc-skills
|
|
10
|
+
# A copy outside the checkout: a rollback to a release that predates the
|
|
11
|
+
# updater must not be able to delete the program doing the rollback.
|
|
12
|
+
ExecStart=/usr/local/libexec/rdc-skills/update-from-tag.sh
|
|
13
|
+
# Proved and failed commits, and install hashes — outside the checkout it judges.
|
|
14
|
+
StateDirectory=rdc-skills-update
|
|
15
|
+
TimeoutStartSec=20min
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Keep the public MCP on the newest release tag, and prove it is serving it.
|
|
3
|
+
#
|
|
4
|
+
# The tag is the promotion for rdc-skills (RELEASE.md). npm consumers get a tag
|
|
5
|
+
# when the registry does; this is the same act for the one consumer that runs
|
|
6
|
+
# from a git checkout — the public MCP at https://rdc-skills.regendevcorp.com.
|
|
7
|
+
# Before this existed the checkout moved only when someone logged in and pulled,
|
|
8
|
+
# which the fleet's hand-rolled-deploy guard refuses, so the public catalog sat
|
|
9
|
+
# on 0.35.18 through six releases while npm moved on.
|
|
10
|
+
#
|
|
11
|
+
# THE MODEL (classified per .claude/rules/stateful-inspection.md)
|
|
12
|
+
# identity a RELEASE: a plain vX.Y.Z tag merged into origin/master, or the
|
|
13
|
+
# PROVED commit — the last one this updater restarted onto and saw
|
|
14
|
+
# /health report. On a host with no history, the commit the running
|
|
15
|
+
# process reports is adopted as proved: it is demonstrably serving.
|
|
16
|
+
# state HEAD; the recorded dependency and unit installs; the FAILED set;
|
|
17
|
+
# the install-error ATTEMPTS against one release.
|
|
18
|
+
# liveness /health's git_sha (fixed at process start) and version (re-read
|
|
19
|
+
# from package.json on every request — so only git_sha proves a
|
|
20
|
+
# restart took).
|
|
21
|
+
#
|
|
22
|
+
# THE INVARIANT: the service is restarted only onto a release. HEAD is state; it
|
|
23
|
+
# is moved to a release, never served for being checked out. A HEAD that is not
|
|
24
|
+
# a release and not on origin/master (a commit made on the host) is refused and
|
|
25
|
+
# the service is left alone.
|
|
26
|
+
#
|
|
27
|
+
# EACH RUN: desired = the newest tag unless it has FAILED or is not ahead of the
|
|
28
|
+
# proved release, else the proved release. Move HEAD there if needed; if /health
|
|
29
|
+
# already reports it and the installs match, done. Otherwise converge (install
|
|
30
|
+
# what changed, restart, prove):
|
|
31
|
+
# proved record it, refresh the updater copy
|
|
32
|
+
# never answers the release is bad: record FAILED (never the proved one),
|
|
33
|
+
# return to the proved release
|
|
34
|
+
# install/restart says nothing about the release, the first two times: return
|
|
35
|
+
# error to the proved release and retry next run. The third time
|
|
36
|
+
# against the same release it is treated as bad. A registry
|
|
37
|
+
# that cannot be reached is not counted — unless it has been
|
|
38
|
+
# unreachable for that release for a whole day.
|
|
39
|
+
#
|
|
40
|
+
# Run by rdc-skills-mcp-update.timer from a copy OUTSIDE the checkout
|
|
41
|
+
# (/usr/local/libexec/rdc-skills), refreshed only after a proved move — a
|
|
42
|
+
# rollback to a release that predates this file can never delete the updater.
|
|
43
|
+
# State lives in /var/lib/rdc-skills-update, not in the checkout it judges.
|
|
44
|
+
#
|
|
45
|
+
# Exit codes: 0 serving the newest release · 2 wrong checkout · 3 tracked changes
|
|
46
|
+
# (preserved, never touched) · 4 HEAD is a commit made on the host (refused, service
|
|
47
|
+
# untouched) · 5 the newest release failed and the proved one is serving again ·
|
|
48
|
+
# 6 nothing proved serving this run (retried next run) · 7 the newest release
|
|
49
|
+
# failed before; the proved one is held · 8 serving and proved, but recording it
|
|
50
|
+
# or refreshing the updater copy failed (retried next run) · 9 the newest tag is
|
|
51
|
+
# not ahead of the proved release (a downgrade); the proved one is held.
|
|
52
|
+
#
|
|
53
|
+
# Everything runs from functions called on the last line. Bash reads a script as
|
|
54
|
+
# it executes; by the time main runs, every line it will execute has been read.
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
|
|
57
|
+
ROOT="${RDC_SKILLS_DEPLOY_ROOT:-/srv/regen/rdc-skills}"
|
|
58
|
+
EXPECTED_ROOT="${RDC_SKILLS_EXPECTED_ROOT:-/srv/regen/rdc-skills}"
|
|
59
|
+
UNIT="rdc-skills-mcp.service"
|
|
60
|
+
UNIT_DIR="${RDC_SKILLS_UNIT_DIR:-/etc/systemd/system}"
|
|
61
|
+
LIBEXEC_DIR="${RDC_SKILLS_LIBEXEC_DIR:-/usr/local/libexec/rdc-skills}"
|
|
62
|
+
STATE_DIR="${RDC_SKILLS_STATE_DIR:-/var/lib/rdc-skills-update}"
|
|
63
|
+
LOCK_FILE="${RDC_SKILLS_LOCK_FILE:-/run/lock/rdc-skills-update.lock}"
|
|
64
|
+
HEALTH_URL="${RDC_SKILLS_HEALTH_URL:-http://127.0.0.1:3110/health}"
|
|
65
|
+
HEALTH_DEADLINE="${RDC_SKILLS_HEALTH_DEADLINE:-120}"
|
|
66
|
+
HEALTH_INTERVAL="${RDC_SKILLS_HEALTH_INTERVAL:-2}"
|
|
67
|
+
HEALTH_SETTLE_ATTEMPTS="${RDC_SKILLS_HEALTH_SETTLE_ATTEMPTS:-3}"
|
|
68
|
+
INSTALL_ERROR_LIMIT="${RDC_SKILLS_INSTALL_ERROR_LIMIT:-3}"
|
|
69
|
+
# How long one release may keep failing to reach the registry before it is judged
|
|
70
|
+
# bad after all (a lockfile `resolved` URL on a host that no longer exists).
|
|
71
|
+
NETWORK_ERROR_WINDOW="${RDC_SKILLS_NETWORK_ERROR_WINDOW:-86400}"
|
|
72
|
+
SYSTEMCTL="${SYSTEMCTL:-systemctl}"
|
|
73
|
+
# Release tags are fetched with force into a namespace this updater owns, so a
|
|
74
|
+
# tag moved or re-created on origin updates here instead of failing the fetch.
|
|
75
|
+
RELEASE_REFS="refs/rdc-release-tags"
|
|
76
|
+
|
|
77
|
+
log() { printf 'rdc-skills-update: %s\n' "$*"; }
|
|
78
|
+
|
|
79
|
+
state_get() { cat "$STATE_DIR/$1" 2>/dev/null || true; }
|
|
80
|
+
state_put() { printf '%s\n' "$2" > "$STATE_DIR/$1.tmp" && mv -f "$STATE_DIR/$1.tmp" "$STATE_DIR/$1"; }
|
|
81
|
+
|
|
82
|
+
# Never fails, and never reads stdin: no arguments hash as empty input.
|
|
83
|
+
hash_files() { { if (($#)); then cat -- "$@" 2>/dev/null || true; fi; } | sha256sum | cut -d' ' -f1; }
|
|
84
|
+
unit_files() { (shopt -s nullglob; printf '%s\n' deploy/systemd/*.service deploy/systemd/*.timer); }
|
|
85
|
+
|
|
86
|
+
version_at() {
|
|
87
|
+
git show "$1:package.json" | node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(0, "utf8")).version)'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
health_field() {
|
|
91
|
+
HEALTH="$1" FIELD="$2" node -e '
|
|
92
|
+
try { process.stdout.write(String(JSON.parse(process.env.HEALTH)[process.env.FIELD] ?? "")); } catch {}
|
|
93
|
+
' 2>/dev/null || true
|
|
94
|
+
}
|
|
95
|
+
health_body() { curl -fsS -m 5 "$HEALTH_URL" 2>/dev/null || true; }
|
|
96
|
+
answers_as() { # <body> <sha> <version>
|
|
97
|
+
[[ -n "$1" && "$(health_field "$1" git_sha)" == "$2" && "$(health_field "$1" version)" == "$3" ]]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# The first ANSWER decides; only silence is retried — one slow response never
|
|
101
|
+
# restarts a healthy service. Prints the answer (empty when there was none).
|
|
102
|
+
settled_body() {
|
|
103
|
+
local i body=""
|
|
104
|
+
for ((i = 1; i <= HEALTH_SETTLE_ATTEMPTS; i++)); do
|
|
105
|
+
body="$(health_body)"
|
|
106
|
+
[[ -n "$body" ]] && break
|
|
107
|
+
((i < HEALTH_SETTLE_ATTEMPTS)) && sleep "$HEALTH_INTERVAL"
|
|
108
|
+
done
|
|
109
|
+
printf '%s' "$body"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
wait_serving() { # <sha> <version>: after a restart, until the deadline
|
|
113
|
+
local end=$((SECONDS + HEALTH_DEADLINE)) body=""
|
|
114
|
+
while :; do
|
|
115
|
+
body="$(health_body)"
|
|
116
|
+
answers_as "$body" "$1" "$2" && return 0
|
|
117
|
+
((SECONDS >= end)) && break
|
|
118
|
+
sleep "$HEALTH_INTERVAL"
|
|
119
|
+
done
|
|
120
|
+
log "health at $HEALTH_URL did not report $2 @ ${1:0:7} within ${HEALTH_DEADLINE}s; last: ${body:-<no answer>}"
|
|
121
|
+
return 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
installs_match() {
|
|
125
|
+
[[ -d node_modules ]] \
|
|
126
|
+
&& [[ "$(hash_files package-lock.json)" == "$(state_get lock-sha256)" ]] \
|
|
127
|
+
&& [[ "$(hash_files $(unit_files))" == "$(state_get units-sha256)" ]]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
held() { # <sha> <version>: /health reports the release and the installs match (serve moved HEAD to it)
|
|
131
|
+
answers_as "$(settled_body)" "$1" "$2" && installs_match
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# converge <sha> <version>: with HEAD at <sha>, install what changed since the
|
|
135
|
+
# last install, restart, and prove it.
|
|
136
|
+
# 0 proved · 1 install or restart error · 2 restarted, but /health never reported it
|
|
137
|
+
# 3 the dependency install could not reach the registry (never counted against a release)
|
|
138
|
+
# Each recorded hash is cleared BEFORE its install and written after, so a run
|
|
139
|
+
# killed halfway is redone rather than trusted. errexit is suspended inside a
|
|
140
|
+
# function called from a condition, so every step checks its own status.
|
|
141
|
+
converge() {
|
|
142
|
+
local sha="$1" version="$2" lock_hash units_hash units npm_out npm_rc
|
|
143
|
+
if [[ -e git-sha.json ]]; then
|
|
144
|
+
# A pack-time stamp outranks `git rev-parse` in the MCP and would pin
|
|
145
|
+
# /health to whatever commit was packed here, failing every proof.
|
|
146
|
+
log "removing a stale git-sha.json stamp from the checkout"
|
|
147
|
+
rm -f git-sha.json || return 1
|
|
148
|
+
fi
|
|
149
|
+
lock_hash="$(hash_files package-lock.json)"
|
|
150
|
+
if [[ ! -d node_modules || "$lock_hash" != "$(state_get lock-sha256)" ]]; then
|
|
151
|
+
log "installing the locked production dependency graph"
|
|
152
|
+
state_put lock-sha256 "" || return 1
|
|
153
|
+
# --ignore-scripts: nothing the server imports needs an install script, and
|
|
154
|
+
# this runs as root, unattended, within minutes of a tag.
|
|
155
|
+
npm_rc=0
|
|
156
|
+
npm_out="$(npm ci --omit=dev --no-audit --no-fund --ignore-scripts 2>&1)" || npm_rc=$?
|
|
157
|
+
if ((npm_rc != 0)); then
|
|
158
|
+
printf '%s\n' "$npm_out" | tail -n 5 | sed 's/^/rdc-skills-update: /'
|
|
159
|
+
# A registry that cannot be reached says nothing about the release; a
|
|
160
|
+
# lockfile that cannot install does.
|
|
161
|
+
if grep -qE '^npm (ERR!|error) (code (E5[0-9]{2}|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|ENOTFOUND|ENETUNREACH|EHOSTUNREACH)|network )' <<<"$npm_out"; then
|
|
162
|
+
log "npm ci could not reach the registry"
|
|
163
|
+
return 3
|
|
164
|
+
fi
|
|
165
|
+
log "npm ci failed"
|
|
166
|
+
return 1
|
|
167
|
+
fi
|
|
168
|
+
state_put lock-sha256 "$lock_hash" || return 1
|
|
169
|
+
fi
|
|
170
|
+
units="$(unit_files)"
|
|
171
|
+
# shellcheck disable=SC2086 # unit file names carry no spaces
|
|
172
|
+
units_hash="$(hash_files $units)"
|
|
173
|
+
if [[ "$units_hash" != "$(state_get units-sha256)" ]]; then
|
|
174
|
+
log "installing unit files"
|
|
175
|
+
state_put units-sha256 "" || return 1
|
|
176
|
+
if [[ -n "$units" ]]; then
|
|
177
|
+
# shellcheck disable=SC2086
|
|
178
|
+
install -m 0644 $units "$UNIT_DIR/" || return 1
|
|
179
|
+
fi
|
|
180
|
+
"$SYSTEMCTL" daemon-reload || return 1
|
|
181
|
+
state_put units-sha256 "$units_hash" || return 1
|
|
182
|
+
fi
|
|
183
|
+
"$SYSTEMCTL" restart "$UNIT" || { log "restart failed"; return 1; }
|
|
184
|
+
wait_serving "$sha" "$version" || return 2
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
is_failed() { [[ -f "$STATE_DIR/failed" ]] && grep -qx "$1" "$STATE_DIR/failed"; }
|
|
188
|
+
mark_failed() { is_failed "$1" || printf '%s\n' "$1" >> "$STATE_DIR/failed"; }
|
|
189
|
+
|
|
190
|
+
# Install/restart errors counted against one release; any other release resets it.
|
|
191
|
+
bump_attempts() {
|
|
192
|
+
local sha="$1" recorded count=0
|
|
193
|
+
recorded="$(state_get attempts)"
|
|
194
|
+
[[ "${recorded%% *}" == "$sha" ]] && count="${recorded##* }"
|
|
195
|
+
count=$((count + 1))
|
|
196
|
+
state_put attempts "$sha $count"
|
|
197
|
+
printf '%s' "$count"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Seconds this release has been failing to reach the registry (0 on the first failure).
|
|
201
|
+
network_failing_for() {
|
|
202
|
+
local sha="$1" recorded now since
|
|
203
|
+
now="$(date +%s)"
|
|
204
|
+
recorded="$(state_get network-since)"
|
|
205
|
+
if [[ "${recorded%% *}" == "$sha" ]]; then
|
|
206
|
+
since="${recorded##* }"
|
|
207
|
+
else
|
|
208
|
+
since="$now"
|
|
209
|
+
state_put network-since "$sha $now"
|
|
210
|
+
fi
|
|
211
|
+
printf '%s' "$((now - since))"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# serve <sha> <version>: move HEAD to a release and make /health prove it.
|
|
215
|
+
# Returns converge's code (0 also when it was already held).
|
|
216
|
+
serve() {
|
|
217
|
+
local sha="$1" version="$2" rc=0
|
|
218
|
+
if [[ "$(git rev-parse HEAD)" != "$sha" ]]; then
|
|
219
|
+
git reset --quiet --hard "$sha" || return 1
|
|
220
|
+
fi
|
|
221
|
+
held "$sha" "$version" && return 0
|
|
222
|
+
log "converging ${sha:0:7} ($version)"
|
|
223
|
+
converge "$sha" "$version" || rc=$?
|
|
224
|
+
return "$rc"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
main() {
|
|
228
|
+
if [[ "$ROOT" != "$EXPECTED_ROOT" ]]; then
|
|
229
|
+
log "refused: checkout is $ROOT, expected $EXPECTED_ROOT"
|
|
230
|
+
return 2
|
|
231
|
+
fi
|
|
232
|
+
mkdir -p "$STATE_DIR"
|
|
233
|
+
# systemd never overlaps two timer runs; this also covers a run started by hand.
|
|
234
|
+
if command -v flock >/dev/null 2>&1; then
|
|
235
|
+
exec 9>"$LOCK_FILE"
|
|
236
|
+
if ! flock -n 9; then
|
|
237
|
+
log "another update run holds $LOCK_FILE"
|
|
238
|
+
return 0
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
cd "$ROOT"
|
|
242
|
+
|
|
243
|
+
local tracked
|
|
244
|
+
tracked="$(git status --porcelain --untracked-files=no)"
|
|
245
|
+
if [[ -n "$tracked" ]]; then
|
|
246
|
+
log "refused: tracked changes in $ROOT — preserved, not updated"
|
|
247
|
+
printf '%s\n' "$tracked" | sed 's/^/rdc-skills-update: /'
|
|
248
|
+
return 3
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# Releases are read only from the forced namespace below, so a tag moved on
|
|
252
|
+
# origin updates there instead of being refused as a clobber (which, with
|
|
253
|
+
# `--tags` into refs/tags, fails the whole fetch every run). --no-tags keeps
|
|
254
|
+
# refs/tags out of it entirely.
|
|
255
|
+
git fetch --quiet --no-tags --prune origin \
|
|
256
|
+
'+refs/heads/master:refs/remotes/origin/master' \
|
|
257
|
+
"+refs/tags/v*:$RELEASE_REFS/v*"
|
|
258
|
+
# Plain vX.Y.Z only: a pre-release or a stray `v*` name is never a release.
|
|
259
|
+
local tag target=""
|
|
260
|
+
tag="$(git for-each-ref --merged refs/remotes/origin/master --sort=-v:refname \
|
|
261
|
+
--format='%(refname:lstrip=2)' "$RELEASE_REFS" \
|
|
262
|
+
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)"
|
|
263
|
+
[[ -n "$tag" ]] && target="$(git rev-parse "$RELEASE_REFS/$tag^{commit}")"
|
|
264
|
+
|
|
265
|
+
local proved
|
|
266
|
+
proved="$(state_get proved)"
|
|
267
|
+
if [[ -z "$proved" ]]; then
|
|
268
|
+
# No history: adopt what is demonstrably serving, so a bad first release has
|
|
269
|
+
# somewhere to go back to — recorded BEFORE anything is restarted.
|
|
270
|
+
local running
|
|
271
|
+
running="$(health_field "$(settled_body)" git_sha)"
|
|
272
|
+
# Only a commit on origin/master: adopting a host-made commit would let the
|
|
273
|
+
# move below reset it off every branch.
|
|
274
|
+
if [[ -n "$running" ]] && git cat-file -e "$running^{commit}" 2>/dev/null \
|
|
275
|
+
&& git merge-base --is-ancestor "$running" refs/remotes/origin/master; then
|
|
276
|
+
state_put proved "$running"
|
|
277
|
+
proved="$running"
|
|
278
|
+
log "adopted the serving commit ${running:0:7} as the proved release"
|
|
279
|
+
fi
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# A release only moves forward: a tag that is not the proved commit or a
|
|
283
|
+
# descendant of it (a higher number put on an older commit) would downgrade the
|
|
284
|
+
# public MCP. Rolling back is a new, higher tag — the tag is the promotion.
|
|
285
|
+
local desired="" holding=""
|
|
286
|
+
if [[ -n "$target" && -n "$proved" && "$target" != "$proved" ]] \
|
|
287
|
+
&& ! git merge-base --is-ancestor "$proved" "$target"; then
|
|
288
|
+
desired="$proved"
|
|
289
|
+
holding="behind"
|
|
290
|
+
elif [[ -n "$target" ]] && ! is_failed "$target"; then
|
|
291
|
+
desired="$target"
|
|
292
|
+
elif [[ -n "$proved" ]]; then
|
|
293
|
+
desired="$proved"
|
|
294
|
+
[[ -n "$target" ]] && holding="failed"
|
|
295
|
+
fi
|
|
296
|
+
if [[ -z "$desired" ]]; then
|
|
297
|
+
if [[ -n "$target" ]]; then
|
|
298
|
+
log "$tag failed its health proof and there is no proved release to serve"
|
|
299
|
+
else
|
|
300
|
+
log "no release tag reachable from origin/master, and no proved release"
|
|
301
|
+
fi
|
|
302
|
+
return 6
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
# HEAD is state, but a commit made ON the host is work that exists nowhere else:
|
|
306
|
+
# never discard it, and never serve it.
|
|
307
|
+
local head
|
|
308
|
+
head="$(git rev-parse HEAD)"
|
|
309
|
+
if [[ "$head" != "$desired" && "$head" != "$proved" && "$head" != "$target" ]] \
|
|
310
|
+
&& ! git merge-base --is-ancestor "$head" refs/remotes/origin/master; then
|
|
311
|
+
log "refused: HEAD ${head:0:7} is a commit made on this host (not on origin/master) — preserved; the service is left as it is"
|
|
312
|
+
return 4
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
local version rc=0
|
|
316
|
+
version="$(version_at "$desired")"
|
|
317
|
+
serve "$desired" "$version" || rc=$?
|
|
318
|
+
local name="${tag:-release}"
|
|
319
|
+
[[ "$desired" != "$target" ]] && name="${desired:0:7}"
|
|
320
|
+
|
|
321
|
+
if ((rc == 0)); then
|
|
322
|
+
state_put attempts "" || true
|
|
323
|
+
state_put network-since "" || true
|
|
324
|
+
if [[ "$(state_get proved)" != "$desired" ]] && ! state_put proved "$desired"; then
|
|
325
|
+
log "serving $name (${desired:0:7}), but recording it as proved failed"
|
|
326
|
+
return 8
|
|
327
|
+
fi
|
|
328
|
+
# The updater copy follows the newest release only — a held older release may
|
|
329
|
+
# predate this file. Compared by content, so a failed refresh is retried.
|
|
330
|
+
if [[ "$desired" == "$target" ]] && ! cmp -s deploy/update-from-tag.sh "$LIBEXEC_DIR/update-from-tag.sh"; then
|
|
331
|
+
if ! install -D -m 0755 deploy/update-from-tag.sh "$LIBEXEC_DIR/update-from-tag.sh"; then
|
|
332
|
+
log "serving $name (${desired:0:7}), but refreshing the updater copy in $LIBEXEC_DIR failed"
|
|
333
|
+
return 8
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
if [[ "$holding" == failed ]]; then
|
|
337
|
+
log "holding ${desired:0:7}: $tag (${target:0:7}) failed its health proof before; waiting for a newer tag"
|
|
338
|
+
return 7
|
|
339
|
+
fi
|
|
340
|
+
if [[ "$holding" == behind ]]; then
|
|
341
|
+
log "holding ${desired:0:7}: the newest tag $tag (${target:0:7}) is not ahead of the proved release; waiting for a newer tag"
|
|
342
|
+
return 9
|
|
343
|
+
fi
|
|
344
|
+
log "current at $name (${desired:0:7})"
|
|
345
|
+
return 0
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# Not proved. The proved release itself is never marked failed: it served.
|
|
349
|
+
local bad=0
|
|
350
|
+
if [[ "$desired" != "$proved" ]]; then
|
|
351
|
+
if ((rc == 2)); then
|
|
352
|
+
bad=1
|
|
353
|
+
elif ((rc == 3)); then
|
|
354
|
+
if (( $(network_failing_for "$desired") >= NETWORK_ERROR_WINDOW )); then
|
|
355
|
+
log "installing $name has failed to reach the registry for over ${NETWORK_ERROR_WINDOW}s; treating it as a bad release"
|
|
356
|
+
bad=1
|
|
357
|
+
else
|
|
358
|
+
log "the registry could not be reached installing $name; retrying next run (not counted)"
|
|
359
|
+
fi
|
|
360
|
+
elif (( $(bump_attempts "$desired") >= INSTALL_ERROR_LIMIT )); then
|
|
361
|
+
log "install or restart error on $name for the ${INSTALL_ERROR_LIMIT}th run; treating it as a bad release"
|
|
362
|
+
bad=1
|
|
363
|
+
else
|
|
364
|
+
log "install or restart error on $name; retrying next run"
|
|
365
|
+
fi
|
|
366
|
+
((bad)) && mark_failed "$desired"
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
if [[ -n "$proved" && "$desired" != "$proved" ]]; then
|
|
370
|
+
log "returning to the proved release ${proved:0:7}"
|
|
371
|
+
local back_rc=0
|
|
372
|
+
serve "$proved" "$(version_at "$proved")" || back_rc=$?
|
|
373
|
+
if ((back_rc == 0)); then
|
|
374
|
+
((bad)) && { log "$name failed; the proved release is serving again"; return 5; }
|
|
375
|
+
return 6
|
|
376
|
+
fi
|
|
377
|
+
log "the proved release ${proved:0:7} could not be proved serving either"
|
|
378
|
+
fi
|
|
379
|
+
return 6
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
main "$@"; exit $?
|
package/guides/agents/backend.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
2
|
-
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
3
|
-
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
4
|
-
|
|
5
|
-
> If dispatching subagents or running as a subagent: read `{PROJECT_ROOT}/.rdc/guides/agent-bootstrap.md` first (fallback: `{PROJECT_ROOT}/.rdc/guides/agent-bootstrap.md`).
|
|
6
|
-
|
|
7
|
-
> **Sandbox contract:** This guide honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# rdc:backend — Backend Agent
|
|
11
|
-
|
|
12
|
-
## Mandatory First Step
|
|
13
|
-
|
|
14
|
-
Read the guide before ANY code:
|
|
15
|
-
```
|
|
16
|
-
{PROJECT_ROOT}/.rdc/guides/backend.md
|
|
17
|
-
(fallback: {PROJECT_ROOT}/.rdc/guides/backend.md)
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Database Client — One Pattern Per Context
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
// Server component / API route
|
|
24
|
-
import { createServerClient } from "@regen/supabase";
|
|
25
|
-
const supabase = await createServerClient();
|
|
26
|
-
|
|
27
|
-
// Client component
|
|
28
|
-
import { createBrowserClient } from "@regen/supabase";
|
|
29
|
-
const supabase = createBrowserClient();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Non-public schemas:
|
|
33
|
-
```ts
|
|
34
|
-
const { data } = await supabase.schema("custom").from("table_name").select("*");
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Credentials — Daemon First
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
curl -s http://127.0.0.1:52437/get/<service>
|
|
41
|
-
```
|
|
42
|
-
- Never hardcode credentials
|
|
43
|
-
- Never print keys to stdout
|
|
44
|
-
- If daemon is down: report BLOCKED — do not work around it
|
|
45
|
-
|
|
46
|
-
## Work Items — RPC Only
|
|
47
|
-
|
|
48
|
-
```sql
|
|
49
|
-
-- Read epics
|
|
50
|
-
SELECT get_open_epics();
|
|
51
|
-
|
|
52
|
-
-- Create
|
|
53
|
-
SELECT insert_work_item(
|
|
54
|
-
p_title := 'Task title',
|
|
55
|
-
p_priority := 'high',
|
|
56
|
-
p_parent_id := '<epic-uuid>'::uuid,
|
|
57
|
-
p_source := 'agent'
|
|
58
|
-
);
|
|
59
|
-
|
|
1
|
+
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
2
|
+
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
3
|
+
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
4
|
+
|
|
5
|
+
> If dispatching subagents or running as a subagent: read `{PROJECT_ROOT}/.rdc/guides/agent-bootstrap.md` first (fallback: `{PROJECT_ROOT}/.rdc/guides/agent-bootstrap.md`).
|
|
6
|
+
|
|
7
|
+
> **Sandbox contract:** This guide honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# rdc:backend — Backend Agent
|
|
11
|
+
|
|
12
|
+
## Mandatory First Step
|
|
13
|
+
|
|
14
|
+
Read the guide before ANY code:
|
|
15
|
+
```
|
|
16
|
+
{PROJECT_ROOT}/.rdc/guides/backend.md
|
|
17
|
+
(fallback: {PROJECT_ROOT}/.rdc/guides/backend.md)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Database Client — One Pattern Per Context
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// Server component / API route
|
|
24
|
+
import { createServerClient } from "@regen/supabase";
|
|
25
|
+
const supabase = await createServerClient();
|
|
26
|
+
|
|
27
|
+
// Client component
|
|
28
|
+
import { createBrowserClient } from "@regen/supabase";
|
|
29
|
+
const supabase = createBrowserClient();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Non-public schemas:
|
|
33
|
+
```ts
|
|
34
|
+
const { data } = await supabase.schema("custom").from("table_name").select("*");
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Credentials — Daemon First
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -s http://127.0.0.1:52437/get/<service>
|
|
41
|
+
```
|
|
42
|
+
- Never hardcode credentials
|
|
43
|
+
- Never print keys to stdout
|
|
44
|
+
- If daemon is down: report BLOCKED — do not work around it
|
|
45
|
+
|
|
46
|
+
## Work Items — RPC Only
|
|
47
|
+
|
|
48
|
+
```sql
|
|
49
|
+
-- Read epics
|
|
50
|
+
SELECT get_open_epics();
|
|
51
|
+
|
|
52
|
+
-- Create
|
|
53
|
+
SELECT insert_work_item(
|
|
54
|
+
p_title := 'Task title',
|
|
55
|
+
p_priority := 'high',
|
|
56
|
+
p_parent_id := '<epic-uuid>'::uuid,
|
|
57
|
+
p_source := 'agent'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
60
|
-- Implementation agents submit report + CodeFlow post, then move to review
|
|
61
61
|
SELECT update_work_item_status('<uuid>'::uuid, 'review', '["Implementation complete; ready for validator"]'::jsonb, '<agent-session-id>', 'agent');
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**NEVER write raw INSERT/UPDATE against work items.**
|
|
65
|
-
|
|
66
|
-
## API Route Pattern
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
import { createServerClient } from "@regen/supabase";
|
|
70
|
-
import { NextResponse } from "next/server";
|
|
71
|
-
|
|
72
|
-
export async function GET() {
|
|
73
|
-
const supabase = await createServerClient();
|
|
74
|
-
const { data, error } = await supabase.from("table").select("*");
|
|
75
|
-
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
|
76
|
-
return NextResponse.json(data);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function POST(request: Request) {
|
|
80
|
-
const body = await request.json();
|
|
81
|
-
const supabase = await createServerClient();
|
|
82
|
-
const { data, error } = await supabase.from("table").insert(body).select().single();
|
|
83
|
-
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
|
|
84
|
-
return NextResponse.json(data, { status: 201 });
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Auth
|
|
89
|
-
|
|
90
|
-
Use the auth helpers from your project's auth package for protected apps.
|
|
91
|
-
|
|
92
|
-
## Schema-Driven Forms
|
|
93
|
-
|
|
94
|
-
When working with schema-driven forms (common in dynamic CRUD), never hardcode columns.
|
|
95
|
-
Use the schema table to drive form rendering instead.
|
|
96
|
-
|
|
97
|
-
## Safety Rules
|
|
98
|
-
|
|
99
|
-
- Branch: development branch — auto-commit after logical blocks
|
|
100
|
-
- NEVER run `pnpm build`
|
|
101
|
-
- NEVER overlap with other agents on the same files
|
|
102
|
-
- Update work items in real time — not batch at end
|
|
103
|
-
- Push after each logical block *(skip if `$RDC_TEST=1` — echo `[RDC_TEST] skipping git push` instead)*
|
|
104
|
-
- Write tests FIRST — red → implement → green
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**NEVER write raw INSERT/UPDATE against work items.**
|
|
65
|
+
|
|
66
|
+
## API Route Pattern
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createServerClient } from "@regen/supabase";
|
|
70
|
+
import { NextResponse } from "next/server";
|
|
71
|
+
|
|
72
|
+
export async function GET() {
|
|
73
|
+
const supabase = await createServerClient();
|
|
74
|
+
const { data, error } = await supabase.from("table").select("*");
|
|
75
|
+
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
|
76
|
+
return NextResponse.json(data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function POST(request: Request) {
|
|
80
|
+
const body = await request.json();
|
|
81
|
+
const supabase = await createServerClient();
|
|
82
|
+
const { data, error } = await supabase.from("table").insert(body).select().single();
|
|
83
|
+
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
|
|
84
|
+
return NextResponse.json(data, { status: 201 });
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Auth
|
|
89
|
+
|
|
90
|
+
Use the auth helpers from your project's auth package for protected apps.
|
|
91
|
+
|
|
92
|
+
## Schema-Driven Forms
|
|
93
|
+
|
|
94
|
+
When working with schema-driven forms (common in dynamic CRUD), never hardcode columns.
|
|
95
|
+
Use the schema table to drive form rendering instead.
|
|
96
|
+
|
|
97
|
+
## Safety Rules
|
|
98
|
+
|
|
99
|
+
- Branch: development branch — auto-commit after logical blocks
|
|
100
|
+
- NEVER run `pnpm build`
|
|
101
|
+
- NEVER overlap with other agents on the same files
|
|
102
|
+
- Update work items in real time — not batch at end
|
|
103
|
+
- Push after each logical block *(skip if `$RDC_TEST=1` — echo `[RDC_TEST] skipping git push` instead)*
|
|
104
|
+
- Write tests FIRST — red → implement → green
|