Warden plan
tier: destructive requires: [needs-postgres, needs-server]
A composite wiki (one that weaves other wikis via a curated, deliberate WIKI_CITES_WIKI reference set — the "contents page" render, D-05/D-06) stops silently drifting out of date when a child it cites changes body, WITHOUT reintroducing the background auto-rewriter the evolution model deleted (#281/#292, D-08). The V1.1 design landed by this issue is narrow and deliberate:
WIKI_CITES_WIKI, composite -> child) gets dirty_since stamped -- the SAME plain-facts marker the atomic signal-triggered path already uses (modules/signals/routes.ts), surfaced in regen_status / GET /wikis/:id exactly like any other dirty wiki. Nothing is enqueued and no content is rewritten by the stamp itself -- the V1 model is explicit-only synthesis (Seed at birth/brief-change, Repair on-demand), and this cascade must not be a side door back into automatic regen.WIKI_CITES_WIKI edge) changing body must NOT dirty that composite -- matching the existing D-05 deliberate-only weave rule.regen_now against a non-empty composite body produces a pending wiki_suggestions row (the same Repair persistence path atomic wikis already use, D-09) built from the child wikis' CURRENT content, not a stale snapshot. Nothing writes wikis.content directly; the D38 auto-accept gate must not touch a composite Repair row either. Only an explicit accept (POST /inbox/suggestions/:id/accept) folds it in.greenlight-pg :5433 with vector, greenlight-redis :6380) plus the app ($APP_URL, default http://localhost:8080) and server ($SERVER_URL, default http://localhost:3000) started via bash ~/.studio/master.withrobin.ai/scripts/dev-server.sh.PROJECT_ROOT = a tree carrying the #224 composite-reweave fix. Invoke via that tree's .warden symlink.curl, jq, grep, psql on PATH. The env file is provisioned automatically by .warden/run.sh from .warden/env/ci-env.template.sh plus ~/.config/robin/warden-secrets.sh.andrew@robin.ai / robin2026, via bash ~/.studio/master.withrobin.ai/scripts/seed-andrew.sh) with a workspace to fixture against.set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# CR-1: something in the synthesis/wikis code stamps dirty_since on the
# WIKI_CITES_WIKI *source* (the composite) keyed off the *destination*
# (the child) -- i.e. a query shaped around dstId/srcId over edgeType
# 'WIKI_CITES_WIKI' feeding a dirtySince update, distinct from the existing
# child-of-signal cascade in modules/signals/routes.ts. Loose (source-shape)
# guard only -- step 3 is the load-bearing live assertion.
if grep -rl "WIKI_CITES_WIKI" server/src/core/synthesis server/src/modules/wikis 2>/dev/null \
| xargs grep -l "dirtySince" 2>/dev/null | grep -q .; then
warden_pass "CR-1: a WIKI_CITES_WIKI-keyed dirtySince stamp exists in synthesis/wikis code"
else
warden_fail "CR-1: no code ties WIKI_CITES_WIKI to a dirtySince stamp -- the upward cascade looks unwired"
fi
# CR-2: no producer enqueues a regen job as a SIDE EFFECT of the cascade
# itself (the stamp must stay a marker, not a trigger) -- the only two
# legitimate enqueue call sites remain the explicit ones (manual regenerate
# route, regen_now).
if grep -rn "enqueueWikiRegen(" server/src/core/synthesis server/src/modules/wikis server/src/modules/signals --include='*.ts' 2>/dev/null \
| grep -v '\.test\.ts' | grep -v "routes.ts\|handlers.ts" | grep -vE ':[0-9]+:[[:space:]]*(//|\*)' | grep -q .; then
warden_fail "CR-2: enqueueWikiRegen is called from somewhere other than the known explicit-trigger routes/handlers -- the dirty cascade may have grown a silent auto-regen side door"
else
warden_pass "CR-2: no unexpected enqueueWikiRegen call site -- the cascade stays marker-only"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
source "$WARDEN_LIB/db.sh"
source "$WARDEN_LIB/auth.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set -- run this plan via .warden/run.sh}"
APP_BASE="${APP_URL:-http://localhost:8080}"
JAR="$(mktemp /tmp/warden-reweave-cookies-XXXXXX.txt)"
curl -s -o /dev/null -c "$JAR" -X POST -H 'Content-Type: application/json' -H "Origin: $APP_BASE" \
-d '{"email":"andrew@robin.ai","password":"robin2026"}' \
"$APP_BASE/api/auth/sign-in/email" >/dev/null
export WARDEN_AUTH_STRATEGY=cookie-session
export WARDEN_AUTH_COOKIE_JAR="$JAR"
WS_ID=$(warden_authed_curl "$APP_BASE/api/workspaces" | jq -r '.workspaces[0].id')
[ -n "$WS_ID" ] && [ "$WS_ID" != "null" ] \
&& warden_pass "resolved a workspace to fixture against ($WS_ID)" \
|| warden_fail "could not resolve any workspace for the warden identity"
# A CHILD wiki. Wiki creation requires a mandatory scope (#169) and a type
# from the workspace's wiki_types registry ('note' is not one); content is
# not writable at create -- authored bodies are bootstrapped via SQL below,
# deliberately NOT via the Seed/accept events this plan is testing.
CHILD_ID=$(warden_authed_curl -X POST -H 'Content-Type: application/json' \
-d "$(jq -cn --arg n "Warden Reweave Child $(date +%s)" '{name:$n, type:"log", scope:{kind:"workspace"}}')" \
"$APP_BASE/api/wikis" | jq -r '.id')
CHILD_SLUG=$(warden_authed_curl "$APP_BASE/api/wikis/$CHILD_ID" | jq -r '.slug')
COMPOSITE_ID=$(warden_authed_curl -X POST -H 'Content-Type: application/json' \
-d "$(jq -cn --arg n "Warden Reweave Composite $(date +%s)" '{name:$n, type:"log", scope:{kind:"workspace"}, composite:true}')" \
"$APP_BASE/api/wikis" | jq -r '.id')
# An UNRELATED wiki that will only ever co-occur (shares no deliberate edge)
# -- the negative fixture for step 4.
UNRELATED_ID=$(warden_authed_curl -X POST -H 'Content-Type: application/json' \
-d "$(jq -cn --arg n "Warden Reweave Unrelated $(date +%s)" '{name:$n, type:"log", scope:{kind:"workspace"}}')" \
"$APP_BASE/api/wikis" | jq -r '.id')
[ -n "$CHILD_ID" ] && [ "$CHILD_ID" != "null" ] && [ -n "$COMPOSITE_ID" ] && [ "$COMPOSITE_ID" != "null" ] \
&& [ -n "$UNRELATED_ID" ] && [ "$UNRELATED_ID" != "null" ] \
&& warden_pass "fixture child ($CHILD_ID), composite ($COMPOSITE_ID), unrelated ($UNRELATED_ID) wikis created" \
|| warden_fail "could not create the fixture wikis over the API"
# Authored-body bootstrap (SQL): there is no direct authored-edit HTTP
# endpoint -- wikis.content is only written by the Seed persist and the
# accepted-suggestion apply, which are exactly the events under test.
warden_psql_exec "UPDATE wikis SET content = 'Original child body: the widget ships in blue.' WHERE lookup_key = '$CHILD_ID'"
warden_psql_exec "UPDATE wikis SET content = 'Unrelated content, never referenced.' WHERE lookup_key = '$UNRELATED_ID'"
# The deliberate reference: composite cites child via the real references API
# (this is what creates the live WIKI_CITES_WIKI edge).
warden_authed_curl -o /dev/null -X POST -H 'Content-Type: application/json' \
-d "$(jq -cn --arg w "$CHILD_ID" '{wikiId:$w}')" \
"$APP_BASE/api/wikis/$COMPOSITE_ID/references"
EDGE_COUNT=$(warden_psql_one "SELECT count(*) FROM edges WHERE src_id = '$COMPOSITE_ID' AND edge_type = 'WIKI_CITES_WIKI' AND dst_id = '$CHILD_ID' AND deleted_at IS NULL")
[ "${EDGE_COUNT:-0}" != "0" ] \
&& warden_pass "live WIKI_CITES_WIKI edge from composite to child (references API)" \
|| warden_fail "no live WIKI_CITES_WIKI edge from composite to child -- cannot exercise the cascade without one"
# Seed the composite once (bakes the contents page) so step 5's regenerate
# lands in Repair mode against a non-empty body. Async job -- poll.
warden_authed_curl -o /dev/null -X POST "$APP_BASE/api/wikis/$COMPOSITE_ID/regenerate"
COMPOSITE_BODY=""
for _i in $(seq 1 45); do
COMPOSITE_BODY=$(warden_psql_one "SELECT COALESCE(content,'') FROM wikis WHERE lookup_key = '$COMPOSITE_ID'")
[ -n "$COMPOSITE_BODY" ] && break
sleep 2
done
[ -n "$COMPOSITE_BODY" ] \
&& warden_pass "composite Seed baked a non-empty contents page (Repair-eligible)" \
|| warden_fail "composite Seed did not produce a body within 90s -- step 5 cannot exercise Repair"
# Clear dirtySince so the fixture starts CLEAN before step 3 changes the child.
warden_psql_exec "UPDATE wikis SET dirty_since = NULL WHERE lookup_key IN ('$COMPOSITE_ID','$UNRELATED_ID')"
echo "$CHILD_ID" > /tmp/warden-reweave-child-id
echo "$CHILD_SLUG" > /tmp/warden-reweave-child-slug
echo "$COMPOSITE_ID" > /tmp/warden-reweave-composite-id
echo "$UNRELATED_ID" > /tmp/warden-reweave-unrelated-id
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
source "$WARDEN_LIB/db.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
APP_BASE="${APP_URL:-http://localhost:8080}"
CHILD_ID="$(cat /tmp/warden-reweave-child-id 2>/dev/null)"
COMPOSITE_ID="$(cat /tmp/warden-reweave-composite-id 2>/dev/null)"
if [ -z "$CHILD_ID" ] || [ -z "$COMPOSITE_ID" ]; then
warden_skip "cascade check" "step 2 did not produce the fixture wikis"
else
BEFORE=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.dirtySince')
[ "$BEFORE" = "null" ] \
&& warden_pass "composite starts clean (dirtySince null) before the child edit" \
|| warden_fail "composite fixture did not start clean -- dirtySince was '$BEFORE' before the child edit; step 2's setup is unreliable"
# Change the child's body through a REAL content-change event: an accepted
# suggestion (there is no direct authored-edit HTTP endpoint -- the two
# wikis.content writers are the Seed persist and the accepted-suggestion
# apply). The pending row is fixture (SQL, deterministic text + correct
# base_hash); the ACCEPT is the live HTTP event whose transaction must
# stamp the citing composite.
CHILD_WS=$(warden_psql_one "SELECT workspace_id FROM wikis WHERE lookup_key = '$CHILD_ID'")
ANDREW_ID=$(warden_psql_one "SELECT id FROM users WHERE email = 'andrew@robin.ai'")
BASE_HASH=$(printf '%s' 'Original child body: the widget ships in blue.' | sha256sum | cut -d' ' -f1)
CHILD_SUG="wardensug$(date +%s)"
warden_psql_exec "INSERT INTO wiki_suggestions (id, workspace_id, wiki_id, proposed_by_user_id, status, rationale, patch, citations, base_hash) VALUES ('$CHILD_SUG', '$CHILD_WS', '$CHILD_ID', '$ANDREW_ID', 'pending', 'warden fixture: child body revision', 'Changed child body: the widget now ships in red.', '[]', '$BASE_HASH')"
ACCEPT_CODE=$(warden_authed_curl -o /dev/null -w '%{http_code}' -X POST "$APP_BASE/api/inbox/suggestions/$CHILD_SUG/accept")
[ "$ACCEPT_CODE" = "200" ] \
&& warden_pass "child body changed via a real accepted suggestion (200)" \
|| warden_fail "accepting the child's suggestion returned $ACCEPT_CODE -- the child body-change event never happened"
sleep 1
AFTER=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.dirtySince')
if [ "$AFTER" != "null" ] && [ -n "$AFTER" ]; then
warden_pass "CR-3: the citing composite's dirtySince got stamped after its deliberately-referenced child's body changed"
else
warden_fail "CR-3: the composite's dirtySince is STILL null after a deliberately-cited child's body changed -- the upward cascade did not fire"
fi
# NEGATIVE: the composite's own content must NOT have been rewritten by
# the dirty stamp -- the cascade is a marker, never an automatic re-weave.
COMPOSITE_BODY=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.wikiContent // .content')
if echo "$COMPOSITE_BODY" | grep -qi "red"; then
warden_fail "CR-4 (negative): the composite's body already mentions the child's NEW content -- something auto-rewrote it off the dirty stamp; the cascade must stay marker-only, no silent re-weave"
else
warden_pass "CR-4 (negative): the composite's body is unchanged by the dirty stamp alone -- no silent auto-rewrite"
fi
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
APP_BASE="${APP_URL:-http://localhost:8080}"
UNRELATED_ID="$(cat /tmp/warden-reweave-unrelated-id 2>/dev/null)"
COMPOSITE_ID="$(cat /tmp/warden-reweave-composite-id 2>/dev/null)"
if [ -z "$UNRELATED_ID" ] || [ -z "$COMPOSITE_ID" ]; then
warden_skip "deliberate-only negative check" "step 2 did not produce the fixture wikis"
else
# Step 3 legitimately dirtied the composite (CR-3); reset to clean so this
# negative check isolates the unrelated edit. SQL bootstrap, same as step 2.
warden_psql_exec "UPDATE wikis SET dirty_since = NULL WHERE lookup_key = '$COMPOSITE_ID'"
# Same real content-change event as step 3 (accepted suggestion), on the
# wiki the composite does NOT cite.
UNREL_WS=$(warden_psql_one "SELECT workspace_id FROM wikis WHERE lookup_key = '$UNRELATED_ID'")
ANDREW_ID=$(warden_psql_one "SELECT id FROM users WHERE email = 'andrew@robin.ai'")
UNREL_HASH=$(printf '%s' 'Unrelated content, never referenced.' | sha256sum | cut -d' ' -f1)
UNREL_SUG="wardenusg$(date +%s)"
warden_psql_exec "INSERT INTO wiki_suggestions (id, workspace_id, wiki_id, proposed_by_user_id, status, rationale, patch, citations, base_hash) VALUES ('$UNREL_SUG', '$UNREL_WS', '$UNRELATED_ID', '$ANDREW_ID', 'pending', 'warden fixture: unrelated body revision', 'Unrelated content, edited, still never referenced.', '[]', '$UNREL_HASH')"
warden_authed_curl -o /dev/null -X POST "$APP_BASE/api/inbox/suggestions/$UNREL_SUG/accept"
sleep 1
DIRTY=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.dirtySince')
[ "$DIRTY" = "null" ] \
&& warden_pass "CR-5 (negative): editing a merely-co-occurring, non-referenced wiki did NOT dirty the composite" \
|| warden_fail "CR-5 (negative): the composite got dirtied by a wiki it never deliberately cites -- the cascade is over-firing past the deliberate-only rule"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
source "$WARDEN_LIB/db.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set -- run this plan via .warden/run.sh}"
APP_BASE="${APP_URL:-http://localhost:8080}"
COMPOSITE_ID="$(cat /tmp/warden-reweave-composite-id 2>/dev/null)"
if [ -z "$COMPOSITE_ID" ]; then
warden_skip "Repair re-weave check" "step 2/3 did not produce a dirtied composite fixture"
else
BODY_BEFORE=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.wikiContent // .content')
# Re-stamp dirty (step 4's negative check cleared it) so CR-11's
# accept-time lifecycle close has real dirt to clear. SQL bootstrap: the
# stamping EVENT itself is already proven live by CR-3.
warden_psql_exec "UPDATE wikis SET dirty_since = now() WHERE lookup_key = '$COMPOSITE_ID'"
warden_authed_curl -o /dev/null -X POST "$APP_BASE/api/wikis/$COMPOSITE_ID/regenerate"
# Repair is an async job (real model call on this stack) -- poll for the
# pending suggestion rather than hoping 3 seconds is enough.
for _i in $(seq 1 60); do
SUGGESTION=$(warden_psql_one "SELECT id FROM wiki_suggestions WHERE wiki_id = '$COMPOSITE_ID' AND status = 'pending' ORDER BY created_at DESC LIMIT 1")
[ -n "$SUGGESTION" ] && break
sleep 2
done
BODY_AFTER=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.wikiContent // .content')
[ "$BODY_BEFORE" = "$BODY_AFTER" ] \
&& warden_pass "CR-6: Repair did NOT write wikis.content directly -- the composite's stored body is unchanged" \
|| warden_fail "CR-6: the composite's stored body changed from a bare regenerate call -- Repair must land as a pending suggestion, never a direct write"
if [ -n "$SUGGESTION" ]; then
warden_pass "CR-7: a pending wiki_suggestions row exists for the composite's Repair ($SUGGESTION)"
PROPOSED=$(warden_psql_one "SELECT patch FROM wiki_suggestions WHERE id = '$SUGGESTION'")
if echo "$PROPOSED" | grep -qi "red"; then
warden_pass "CR-8: the pending Repair suggestion reflects the child's CURRENT (post-change) content, not a stale snapshot"
else
echo "--- CR-8 proposed patch (for judgment) ---"
echo "$PROPOSED"
echo "--- child body at repair time ---"
warden_psql_one "SELECT content FROM wikis WHERE lookup_key = '$CHILD_ID'"
warden_fail "CR-8: the pending Repair suggestion does not mention the child's updated content -- the re-weave is reading a stale child body"
fi
else
warden_fail "CR-7: no pending wiki_suggestions row for the composite after regenerate -- Repair may have applied directly or done nothing"
fi
# NEGATIVE: nothing auto-accepted it (D38 gate must not touch a composite Repair row).
STILL_PENDING=$(warden_psql_one "SELECT status FROM wiki_suggestions WHERE id = '$SUGGESTION'")
[ "$STILL_PENDING" = "pending" ] \
&& warden_pass "CR-9 (negative): the composite Repair suggestion is still pending -- nothing auto-accepted it" \
|| warden_fail "CR-9 (negative): the composite Repair suggestion status is '$STILL_PENDING', not pending -- something auto-applied it"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
source "$WARDEN_LIB/db.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set -- run this plan via .warden/run.sh}"
APP_BASE="${APP_URL:-http://localhost:8080}"
COMPOSITE_ID="$(cat /tmp/warden-reweave-composite-id 2>/dev/null)"
if [ -z "$COMPOSITE_ID" ]; then
warden_skip "accept-folds-in check" "no fixture composite carried through"
else
SUGGESTION=$(warden_psql_one "SELECT id FROM wiki_suggestions WHERE wiki_id = '$COMPOSITE_ID' AND status = 'pending' ORDER BY created_at DESC LIMIT 1")
if [ -z "$SUGGESTION" ]; then
warden_skip "accept-folds-in check" "no pending suggestion carried through from step 5"
else
ACCEPT_CODE=$(warden_authed_curl -o /dev/null -w '%{http_code}' -X POST "$APP_BASE/api/inbox/suggestions/$SUGGESTION/accept")
[ "$ACCEPT_CODE" = "200" ] \
&& warden_pass "explicit accept on the composite Repair suggestion succeeded (200)" \
|| warden_fail "explicit accept on the composite Repair suggestion returned $ACCEPT_CODE, expected 200"
BODY=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.wikiContent // .content')
if echo "$BODY" | grep -qi "red"; then
warden_pass "CR-10: accepting the Repair suggestion folded the re-woven (post-child-change) content into the composite's body"
else
echo "--- CR-10 composite body after accept (for judgment) ---"
echo "$BODY"
warden_fail "CR-10: the composite's body after accept still doesn't reflect the child's changed content"
fi
DIRTY=$(warden_authed_curl "$APP_BASE/api/wikis/$COMPOSITE_ID" | jq -r '.dirtySince')
[ "$DIRTY" = "null" ] \
&& warden_pass "CR-11: dirtySince cleared once the Repair fold-in landed" \
|| warden_fail "CR-11: dirtySince is still '$DIRTY' after the Repair was accepted -- the lifecycle didn't close"
fi
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set -- run this plan via .warden/run.sh}"
if pnpm --filter @robin/server exec vitest run \
src/core/synthesis/regen.composite.dbtest.test.ts \
src/core/synthesis/composite-contents.dbtest.test.ts \
src/core/synthesis/synthesis-modes.dbtest.test.ts \
src/modules/signals/signals.detach.dbtest.test.ts \
>/tmp/warden-224-suite.log 2>&1; then
warden_pass "composite/synthesis regression suite passes (Seed-once, contents-page validation, deliberate-only weave, atomic detach cascade all still hold)"
else
tail -30 /tmp/warden-224-suite.log
warden_fail "composite/synthesis regression suite FAILED -- see /tmp/warden-224-suite.log"
fi