Skip to content

Commit 793b079

Browse files
committed
fix(update): self-heal a present-but-stale manifest (pre-#76 vintage)
closes the stuck-vintage gap for hosts instantiated between #60 and #76: their manifest is present but does not list itself, so every sync run assembles the old file list and can never deliver the manifest's own replacement — the host stays behind forever. First field case: ND-DAC-DOME/naval-sensor-fusion (instantiated 2026-06-30): three files permanently undeliverable (template-manifest.sh, ask.md, wiki-reciprocity.py) and Edge-Types.md.template un-updatable (on disk from instantiation, but outside the stale sync list). The existing #74 self-heal only fired on a MISSING manifest. Extend the trigger: also heal when the local manifest lacks the quoted entry "scripts/lib/template-manifest.sh" — every post-#76 manifest carries its own path as an array entry, so the probe is an unambiguous vintage signature (verified: zero occurrences of the string anywhere in the c19eec8-era file, entry present in every manifest since). Red-first via the new integration fixture stale-manifest-self-heal: against the unfixed script, 5 assertions fail — no heal note, manifest unreplaced, post-#76 files undelivered. With the fix: 11/11, including convergence (run 2 sources the healed on-disk manifest, does not re-trigger, reports Changed (0)). Reverting the fix brings back the same 5 failures. Field-validated on a clone of naval-sensor-fusion itself: run 1 (their 2026-06-30 updater) delivers 8 files off the old list; run 2 (the new updater) prints the heal note and delivers the final 3; run 3 reports Changed (0).
1 parent 1f21a38 commit 793b079

3 files changed

Lines changed: 154 additions & 10 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
# Assertions: update-from-template.sh must detect a PRESENT-but-STALE local
3+
# manifest (pre-#76: does not list itself), assemble this run's file list
4+
# from the template ref's manifest instead, and thereby deliver both the
5+
# manifest replacement and the files only the new manifest lists. Without
6+
# the stale trigger the run uses the old list and the host is stuck forever.
7+
8+
STAGE="$SANDBOX/stale-manifest-self-heal"
9+
H="$STAGE/host"
10+
11+
RUN1_LOG="$STAGE/run1.log"
12+
( cd "$H" && bash scripts/update-from-template.sh --template-url="$STAGE/template-src" ) \
13+
> "$RUN1_LOG" 2>&1
14+
RC1=$?
15+
16+
assert "run 1 exits 0" "[ $RC1 -eq 0 ]"
17+
assert "run 1 announces the stale-manifest self-heal" \
18+
"grep -qF 'predates #76' '$RUN1_LOG'"
19+
20+
# The core of the fix: files only the CURRENT manifest lists arrive.
21+
assert "manifest on disk replaced (now lists itself)" \
22+
"grep -qF '\"scripts/lib/template-manifest.sh\"' '$H/scripts/lib/template-manifest.sh'"
23+
assert "wiki/Edge-Types.md.template delivered" \
24+
"[ -f '$H/wiki/Edge-Types.md.template' ]"
25+
assert ".claude/commands/ask.md delivered" \
26+
"[ -f '$H/.claude/commands/ask.md' ]"
27+
assert "scripts/wiki-reciprocity.py delivered" \
28+
"[ -f '$H/scripts/wiki-reciprocity.py' ]"
29+
30+
# Control: the normal sync path is unaffected (old-list file synced with
31+
# {{REPO_NAME}} substituted from the on-disk wiki name).
32+
CTRL="$H/.claude/commands/wiki-experiment.md"
33+
assert "control: old-list file synced" "grep -q 'command for stalehost' '$CTRL'"
34+
assert "control: no {{REPO_NAME}} left" "! grep -q '{{REPO_NAME}}' '$CTRL'"
35+
36+
# Convergence: a second run sources the now-healthy on-disk manifest, does
37+
# not re-trigger the self-heal, and reports nothing to change.
38+
RUN2_LOG="$STAGE/run2.log"
39+
( cd "$H" && bash scripts/update-from-template.sh --template-url="$STAGE/template-src" ) \
40+
> "$RUN2_LOG" 2>&1
41+
RC2=$?
42+
43+
assert "run 2 exits 0" "[ $RC2 -eq 0 ]"
44+
assert "run 2 does NOT re-trigger the self-heal" \
45+
"! grep -qF 'predates #76' '$RUN2_LOG'"
46+
assert "run 2 reports zero changed files" \
47+
"grep -qF 'Changed (0):' '$RUN2_LOG'"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
# Patch: fixture for the stale-manifest self-heal in update-from-template.sh.
3+
#
4+
# Field case: hosts instantiated between the manifest consolidation (#60,
5+
# 2026-06-28) and the manifest-ships-itself fix (#76, 2026-07-10) carry a
6+
# manifest that is PRESENT but does not list itself. Their update runs
7+
# assemble the file list from that stale manifest, which can never deliver
8+
# its own replacement — the host is stuck on the old list forever (first
9+
# observed on ND-DAC-DOME/naval-sensor-fusion, instantiated 2026-06-30).
10+
#
11+
# Effects: creates $SANDBOX/stale-manifest-self-heal/ with:
12+
# template-src/ stand-in template repo (branch main) holding the CURRENT
13+
# manifest plus three files only the current manifest lists
14+
# (wiki/Edge-Types.md.template, .claude/commands/ask.md,
15+
# scripts/wiki-reciprocity.py) and one old-list file with a
16+
# {{REPO_NAME}} token (control: normal sync still works)
17+
# host/ project with the CURRENT sync tooling under scripts/ but a
18+
# STALE manifest: derived from the current one by deleting
19+
# the self-listing and the three post-#76 entries — the
20+
# exact shape of the stuck vintage (functional, sources
21+
# cleanly, omits itself)
22+
#
23+
# Hermetic: the "remote" is a local path.
24+
25+
set -uo pipefail
26+
27+
HERE_PATCH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28+
TEMPLATE_ROOT="$(cd "$HERE_PATCH/../../../../.." && pwd)"
29+
30+
STAGE="$SANDBOX/stale-manifest-self-heal"
31+
mkdir -p "$STAGE"
32+
33+
g() { git "$@"; }
34+
35+
# --- template-src: minimal current-shape template repo ---
36+
T="$STAGE/template-src"
37+
g init -q "$T"
38+
g -C "$T" symbolic-ref HEAD refs/heads/main
39+
mkdir -p "$T/scripts/lib" "$T/.claude/commands" "$T/wiki"
40+
cp "$TEMPLATE_ROOT/scripts/lib/template-manifest.sh" "$T/scripts/lib/"
41+
printf '# Edge-Types for {{REPO_NAME}} (rendered by init-wiki)\n' > "$T/wiki/Edge-Types.md.template"
42+
printf 'ask command body\n' > "$T/.claude/commands/ask.md"
43+
printf '#!/usr/bin/env python3\n' > "$T/scripts/wiki-reciprocity.py"
44+
printf 'command for {{REPO_NAME}}\n' > "$T/.claude/commands/wiki-experiment.md"
45+
g -C "$T" add -A
46+
g -C "$T" commit -q -m "template content"
47+
48+
# --- host: current tooling + genuinely stale manifest ---
49+
H="$STAGE/host"
50+
g init -q "$H"
51+
mkdir -p "$H/wiki/stalehost.wiki" "$H/.claude/commands" "$H/scripts/lib"
52+
: > "$H/wiki/stalehost.wiki/SCHEMA_stalehost.md"
53+
printf 'stale local content\n' > "$H/.claude/commands/wiki-experiment.md"
54+
cp "$TEMPLATE_ROOT/scripts/update-from-template.sh" "$H/scripts/"
55+
cp "$TEMPLATE_ROOT/scripts/check-template-version.sh" "$H/scripts/"
56+
cp "$TEMPLATE_ROOT"/scripts/lib/*.sh "$H/scripts/lib/"
57+
58+
# Stale manifest: current one minus the self-listing and the post-#76
59+
# entries. Still valid bash, still assembles — just the stuck-vintage list.
60+
sed -e '/"scripts\/lib\/template-manifest.sh"/d' \
61+
-e '/"wiki\/Edge-Types.md.template"/d' \
62+
-e '/".claude\/commands\/ask.md"/d' \
63+
-e '/"scripts\/wiki-reciprocity.py"/d' \
64+
"$TEMPLATE_ROOT/scripts/lib/template-manifest.sh" \
65+
> "$H/scripts/lib/template-manifest.sh"
66+
67+
# Sanity: the stale manifest must source cleanly and must NOT list itself —
68+
# otherwise the fixture is not exercising the stuck vintage at all.
69+
bash -n "$H/scripts/lib/template-manifest.sh"
70+
if grep -qF '"scripts/lib/template-manifest.sh"' "$H/scripts/lib/template-manifest.sh"; then
71+
echo " ERROR: fixture failed to produce a stale (non-self-listing) manifest" >&2
72+
exit 1
73+
fi
74+
75+
echo " stale-manifest-self-heal patch applied: fixtures at $STAGE"

scripts/update-from-template.sh

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,45 @@ TEMPLATE_REF="template/$TEMPLATE_BRANCH"
8989
TEMPLATE_SHA=$(git rev-parse --short "$TEMPLATE_REF")
9090
echo "Template HEAD: $TEMPLATE_SHA"
9191

92-
# --- Load the template manifest (bootstrap when the host lacks it, #74) ---
93-
# Hosts adopted before the manifest shipped itself have an updater whose
94-
# file list lives in a file they never received; sourcing it directly would
95-
# die before the fetch that could deliver the fix. Materialize the manifest
96-
# from the just-fetched template ref into a TEMP file and source that — the
97-
# sync loop below then installs scripts/lib/template-manifest.sh on disk
98-
# like any other missing manifest-listed file (or reports it, on --dry-run).
92+
# --- Load the template manifest (self-heal when the host's copy can't) ---
93+
# Two vintages need healing. Hosts adopted before the manifest shipped
94+
# itself (#74) have an updater whose file list lives in a file they never
95+
# received; sourcing it directly would die before the fetch that could
96+
# deliver the fix. Hosts instantiated between #60 and #76 have a manifest
97+
# that is PRESENT but does not list itself, so a sync assembled from it can
98+
# never deliver its own replacement. In both cases, materialize the
99+
# manifest from the just-fetched template ref into a TEMP file and source
100+
# that — the sync loop below then installs/updates the on-disk copy like
101+
# any other manifest-listed file (or reports it, on --dry-run).
99102
MANIFEST_PATH="$HERE/lib/template-manifest.sh"
103+
_manifest_heal_reason=""
100104
if [[ ! -f "$MANIFEST_PATH" ]]; then
105+
_manifest_heal_reason="missing (host adopted before #74)"
106+
elif ! grep -qF '"scripts/lib/template-manifest.sh"' "$MANIFEST_PATH"; then
107+
# Present but stale: a pre-#76 manifest does not list itself, so a sync
108+
# assembled from it can never deliver its own replacement — the host
109+
# stays on the old file list forever (hosts instantiated between #60
110+
# and #76; first field case: naval-sensor-fusion, 2026-06-30 vintage).
111+
# Every post-#76 manifest carries its own path as an array entry, so
112+
# the quoted-string probe is an unambiguous vintage signature.
113+
_manifest_heal_reason="present but stale (predates #76: does not list itself)"
114+
fi
115+
if [[ -n "$_manifest_heal_reason" ]]; then
101116
MANIFEST_PATH=$(mktemp)
102117
if ! git show "$TEMPLATE_REF:scripts/lib/template-manifest.sh" > "$MANIFEST_PATH" 2>/dev/null; then
103118
rm -f "$MANIFEST_PATH"
104-
echo "error: scripts/lib/template-manifest.sh is missing locally AND absent from $TEMPLATE_REF; cannot assemble the sync file list" >&2
119+
echo "error: scripts/lib/template-manifest.sh is unusable locally AND absent from $TEMPLATE_REF; cannot assemble the sync file list" >&2
105120
exit 1
106121
fi
107-
echo "note: local scripts/lib/template-manifest.sh missing (host adopted before #74);"
108-
echo " bootstrapped this run's copy from $TEMPLATE_REF. The sync below installs it."
122+
if [[ "$_manifest_heal_reason" == missing* ]]; then
123+
# Wording kept verbatim from the #74 fix (the adopted-host-sync
124+
# fixture pins it as the legacy host's observable signal).
125+
echo "note: local scripts/lib/template-manifest.sh missing (host adopted before #74);"
126+
echo " bootstrapped this run's copy from $TEMPLATE_REF. The sync below installs it."
127+
else
128+
echo "note: local scripts/lib/template-manifest.sh $_manifest_heal_reason;"
129+
echo " using $TEMPLATE_REF's copy for this run. The sync below updates it on disk."
130+
fi
109131
fi
110132
# shellcheck source=lib/template-manifest.sh
111133
source "$MANIFEST_PATH"

0 commit comments

Comments
 (0)