* test(#4135): regression rows for pristine regen coverage collapse RED skeleton: src/pristine-baseline.cts exports findPristineInGit as a null-returning stub (wired into verifyFile after the #4145 orphan tier, behavior-neutral) so the git-history rows fail behaviorally, not at require time. Failing-first rows: baseline_covered aggregate on a 1-of-13 multi-version fixture, coverageHeadline typed renderer, the opt-in --min-baseline-coverage gate (exit 3, >= threshold semantics, vacuous-pass and malformed-value boundaries), git-history baseline recovery (dropped-line catch + surviving-line verify + older-commit hop), findPristineInGit unit, Step 5a workflow headline contract, and the installer-side describeBaselineCoverage honest N-of-M summary with the collapse disk-state pinned. Negative-space rows pin today: non-git ok_no_baseline posture, no-match-no-adoption, #3657 drift never rescued, canonical precedence, and no git tier without --pristine-dir. * fix(#4135): headline baseline coverage, opt-in strict gate, git-history widening The #3407 promotion rule regenerates gsd-pristine/ baselines from the INCOMING release source and keeps only candidates byte-identical with the OUTGOING recorded hash — correct in isolation, but on a multi-version jump the surviving set is precisely the files upstream did NOT change. The verifier then reports ok_no_baseline (advisory, exit 0) for everything else, and no surface distinguishes a 12-of-13-unverified green run from a fully-verified one: the human summary printed Checked/Failures only, the JSON had no coverage aggregate, and the installer's update output gave per-bucket counts without N-of-M framing. All three issue directions, none exclusive: - Report coverage prominently: --json gains an additive baseline_covered aggregate; the human summary leads with 'Baseline coverage: N of M file(s)...' on every run plus an advisory section naming each skipped file and reason; the installer prints an honest covered-of-modified line via the exported describeBaselineCoverage helper (typed return, exact contract); workflow Step 5a computes and prints the headline before any pass/fail framing. - Fail louder on low coverage: opt-in --min-baseline-coverage <0..1> exits with new documented code 3 when coverage falls below the threshold (>= semantics; empty run vacuously passes; content failure exit 1 outranks it; malformed values are usage errors, exit 2). Default posture unchanged — no_baseline stays advisory per #934. - Widen the promotion rule (its only trustworthy form): when no baseline resolves under gsd-pristine/ and a hash is recorded, the verifier now recovers the baseline from the config dir's own git history — the workflow's documented Option A — anchored by the same authority every tier trusts, exact pristine_hashes sha-256 equality. Read-only (git log/git show, windowsHide per #685), bounded (100 commits/file, 10s/subprocess), null-on-any-failure so ok_no_baseline remains the universal fallback. Tier order: canonical join -> #4145 orphan scan -> git history -> OK_NO_BASELINE; #3657 drift and canonical precedence untouched. Hash validation in saveLocalPatches is NOT relaxed — the collapse is legitimate conservatism; hiding it was the bug. Measured on the issue's shape (13 files, 12 changed upstream, 1.10->1.12): non-git installs report baseline_covered 1/13 with the headline and can gate at exit 3; a git-managed config dir with the outgoing bytes in history verifies 13/13. Review fixes folded in: workflow headline derives the unverified count from checked - baseline_covered (not the drift+no_baseline sum), and the new site-scoped allow-test-rule annotation carries its ADR-456 see-ref on the marker line. Emitted-Drift-Ack-Growth: reapply-patches.md — #4135 — +20 lines / ~1.5 KB, prose and bash only: two additive parse lines (BASELINE_COVERED, CHECKED_COUNT), a Step 5a coverage-headline block printed BEFORE any pass/fail statement (documents the opt-in --min-baseline-coverage exit-3 gate), and one Option B sentence noting the verifier's read-only git-history fallback. No step ordering, gate, tool-invocation, or dispatch shape changed; 5a's fail/drift/advisory handling is unchanged, the headline only precedes it. * chore(#4135): backfill PR number into changeset fragment --------- Co-authored-by: agent-4135 <agent-4135@gsd.local>
25 KiB
@~/.claude/gsd-core/references/response-language-directive.md
Reapply Local Patches Workflow
Invoked by /gsd:update --reapply (commands/gsd/update.md).
After a GSD update wipes and reinstalls files, this workflow merges user's previously saved local modifications back into the new version. Uses three-way comparison (pristine baseline, user-modified backup, newly installed version) to reliably distinguish user customizations from version drift.
Critical invariant: Every file in gsd-local-patches/ was backed up because the installer's hash comparison detected it was modified. The workflow must NEVER conclude "no custom content" for any backed-up file — that is a logical contradiction. When in doubt, classify as CONFLICT requiring user review, not SKIP.
Step 1: Detect backed-up patches
Check for local patches directory:
expand_home() {
case "$1" in
"~/"*) printf '%s/%s\n' "$HOME" "${1#~/}" ;;
*) printf '%s\n' "$1" ;;
esac
}
PATCHES_DIR=""
# Env overrides first — covers custom config directories used with --config-dir
if [ -n "$KILO_CONFIG_DIR" ]; then
candidate="$(expand_home "$KILO_CONFIG_DIR")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
elif [ -n "$KILO_CONFIG" ]; then
candidate="$(dirname "$(expand_home "$KILO_CONFIG")")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
elif [ -n "$XDG_CONFIG_HOME" ]; then
candidate="$(expand_home "$XDG_CONFIG_HOME")/kilo/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
fi
if [ -z "$PATCHES_DIR" ] && [ -n "$OPENCODE_CONFIG_DIR" ]; then
candidate="$(expand_home "$OPENCODE_CONFIG_DIR")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
elif [ -z "$PATCHES_DIR" ] && [ -n "$OPENCODE_CONFIG" ]; then
candidate="$(dirname "$(expand_home "$OPENCODE_CONFIG")")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
elif [ -z "$PATCHES_DIR" ] && [ -n "$XDG_CONFIG_HOME" ]; then
candidate="$(expand_home "$XDG_CONFIG_HOME")/opencode/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
fi
if [ -z "$PATCHES_DIR" ] && [ -n "$GEMINI_CONFIG_DIR" ]; then
candidate="$(expand_home "$GEMINI_CONFIG_DIR")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
fi
if [ -z "$PATCHES_DIR" ] && [ -n "$CODEX_HOME" ]; then
candidate="$(expand_home "$CODEX_HOME")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
fi
if [ -z "$PATCHES_DIR" ] && [ -n "$CLAUDE_CONFIG_DIR" ]; then
candidate="$(expand_home "$CLAUDE_CONFIG_DIR")/gsd-local-patches"
if [ -d "$candidate" ]; then
PATCHES_DIR="$candidate"
fi
fi
# Global install — detect runtime config directory defaults
if [ -z "$PATCHES_DIR" ]; then
if [ -d "$HOME/.config/kilo/gsd-local-patches" ]; then
PATCHES_DIR="$HOME/.config/kilo/gsd-local-patches"
elif [ -d "$HOME/.config/opencode/gsd-local-patches" ]; then
PATCHES_DIR="$HOME/.config/opencode/gsd-local-patches"
elif [ -d "$HOME/.opencode/gsd-local-patches" ]; then
PATCHES_DIR="$HOME/.opencode/gsd-local-patches"
elif [ -d "$HOME/.gemini/gsd-local-patches" ]; then
PATCHES_DIR="$HOME/.gemini/gsd-local-patches"
elif [ -d "$HOME/.codex/gsd-local-patches" ]; then
PATCHES_DIR="$HOME/.codex/gsd-local-patches"
else
PATCHES_DIR="$HOME/.claude/gsd-local-patches"
fi
fi
# Local install fallback — check all runtime directories
if [ ! -d "$PATCHES_DIR" ]; then
for dir in .config/kilo .kilo .config/opencode .opencode .gemini .codex .claude; do
if [ -d "./$dir/gsd-local-patches" ]; then
PATCHES_DIR="./$dir/gsd-local-patches"
break
fi
done
fi
Read backup-meta.json from the patches directory.
If no patches found:
No local patches found. Nothing to reapply.
Local patches are automatically saved when you run /gsd:update
after modifying any GSD workflow, command, or agent files.
Exit.
Step 2: Determine baseline for three-way comparison
The quality of the merge depends on having a pristine baseline — the original unmodified version of each file from the pre-update GSD release. This enables three-way comparison:
- Pristine baseline (original GSD file before any user edits)
- User's version (backed up in
gsd-local-patches/) - New version (freshly installed after update)
Check for baseline sources in priority order:
Option A: Pristine hash from backup-meta.json + git history (most reliable)
If the config directory is a git repository:
CONFIG_DIR=$(dirname "$PATCHES_DIR")
if git -C "$CONFIG_DIR" rev-parse --git-dir >/dev/null 2>&1; then
HAS_GIT=true
fi
When HAS_GIT=true, use the pristine_hashes recorded in backup-meta.json to locate the correct baseline commit. For each file, iterate commits that touched it and find the one whose blob SHA-256 matches the recorded pristine hash:
# Get the expected pristine SHA-256 from backup-meta.json
PRISTINE_HASH=$(jq -r ".pristine_hashes[\"${file_path}\"] // empty" "$PATCHES_DIR/backup-meta.json")
BASELINE_COMMIT=""
if [ -n "$PRISTINE_HASH" ]; then
# Walk commits that touched this file, pick the one matching the pristine hash
while IFS= read -r commit_hash; do
blob_hash=$(git -C "$CONFIG_DIR" show "${commit_hash}:${file_path}" 2>/dev/null | sha256sum | cut -d' ' -f1)
if [ "$blob_hash" = "$PRISTINE_HASH" ]; then
BASELINE_COMMIT="$commit_hash"
break
fi
done < <(git -C "$CONFIG_DIR" log --format="%H" -- "${file_path}")
fi
# Fallback: if no pristine hash in backup-meta (older installer), use first-add commit
if [ -z "$BASELINE_COMMIT" ]; then
BASELINE_COMMIT=$(git -C "$CONFIG_DIR" log --diff-filter=A --format="%H" -- "${file_path}" | tail -1)
fi
Extract the pristine version from the matched commit:
git -C "$CONFIG_DIR" show "${BASELINE_COMMIT}:${file_path}"
Why this matters: git log --diff-filter=A returns the commit that first added the file, which is the wrong baseline on repos that have been through multiple GSD update cycles. The pristine_hashes field in backup-meta.json records the SHA-256 of the file as it existed in the pre-update GSD release — matching against it finds the correct baseline regardless of how many updates have occurred.
Option B: Pristine snapshot directory
Check if a gsd-pristine/ directory exists alongside gsd-local-patches/:
PRISTINE_DIR="$CONFIG_DIR/gsd-pristine"
If it exists, the installer saved pristine copies at install time. Use these as the baseline. Both the deterministic verifier and the installer's preserve-check resolve each file's snapshot at its canonical path first and, when that misses, by the SHA-256 recorded in pristine_hashes — so a snapshot stored at a legacy path (for example, without the gsd-core/ prefix an earlier release dropped) is still found and, on the next update, relocated to its canonical path (#4145). When no snapshot resolves under gsd-pristine/, the deterministic verifier additionally falls back to Option A's git-history walk itself (read-only, same recorded-hash match), which is what keeps Step 5a coverage non-zero on multi-version updates where the hash-validated regeneration had nothing it could promote (#4135).
Option C: No baseline available (two-way fallback)
If neither git history nor pristine snapshots are available, fall back to two-way comparison — but with strengthened heuristics (see Step 3).
Step 3: Show patch summary
## Local Patches to Reapply
**Backed up from:** v{from_version}
**Current version:** {read VERSION file}
**Files modified:** {count}
**Merge strategy:** {three-way (git) | three-way (pristine) | two-way (enhanced)}
| # | File | Status |
|---|------|--------|
| 1 | {file_path} | Pending |
| 2 | {file_path} | Pending |
Step 4: Merge each file
Step 4 pre-flight: classify superseded customizations (#4136)
Before merging anything, run the deterministic classifier. It computes, per backed-up file,
whether the user's added lines (diff of the backup against the hash-validated pristine
baseline) are ALREADY present verbatim in the newly installed version — i.e. upstream
adopted the customization. That is the only ground on which the Incorporated status
below is valid. Never conclude Incorporated from a signature line, a heading, or general
resemblance: the classifier excludes structural/trivial lines and requires every
significant user-added line to be present, because a false Incorporated silently retires
a live customization.
PRISTINE_DIR="${CONFIG_DIR}/gsd-pristine"
# Build args as a bash array so paths with spaces survive expansion intact.
CLASSIFY_ARGS=(
--patches-dir "$PATCHES_DIR"
--config-dir "$CONFIG_DIR"
)
if [ -d "$PRISTINE_DIR" ]; then
CLASSIFY_ARGS+=(--pristine-dir "$PRISTINE_DIR")
fi
CLASSIFY_ARGS+=(--classify --json)
# Informational: exits 0. The binding gate is Step 5a's post-merge verification run.
CLASSIFY_OUTPUT="$(node "${GSD_HOME}/gsd-core/bin/verify-reapply-patches.cjs" "${CLASSIFY_ARGS[@]}")"
INCORPORATED_FILES="$(echo "$CLASSIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));(d.incorporated_files||[]).forEach(f=>process.stdout.write(f+'\n'))")"
For every file in INCORPORATED_FILES:
- Do NOT merge it. Do NOT re-apply its diff. Leave the newly installed file exactly as shipped — the user's customization is already in it. Re-grafting the diff onto a version that already contains it duplicates the content (both copies run; a stale graft can contradict a richer native upstream step that replaced it).
- Report the per-file status
Incorporated— "Already in upstream v{version}" — in the Step 3 summary and the Step 7 report. - No Hunk Verification Table rows are required for these files (nothing was merged); the classifier's structured output is the evidence. If EVERY backed-up file is Incorporated, still emit the table with a header row and a single note line naming the incorporated files, so the Step 5b absent-table halt is not tripped. Step 5a passes these files without special-casing — every user-added line is present in the untouched install.
- This is what ends the re-graft cycle: because the installed file stays identical to what
the release ships, the next update's hash comparison no longer flags it as modified, and
the file drops out of
gsd-local-patches/on the next cycle.
Files NOT in INCORPORATED_FILES — reported needs_merge (with the lines still missing
from the new version) or unknown (no confirmable pristine baseline) — take the normal
merge paths below, unchanged. If the classifier cannot run at all (e.g. GSD_HOME
unset), fall back to merging every file as before; never report Incorporated without the
classifier's structured confirmation.
For each file in backup-meta.json:
- Read the backed-up version (user's modified copy from
gsd-local-patches/) - Read the newly installed version (current file after update)
- If available, read the pristine baseline (from git history or
gsd-pristine/)
Three-way merge (when baseline is available)
Compare the three versions to isolate changes:
- User changes = diff(pristine → user's version) — these are the customizations to preserve
- Upstream changes = diff(pristine → new version) — these are version updates to accept
Merge rules:
- Sections changed only by user → apply user's version
- Sections changed only by upstream → accept upstream version
- Sections changed by both → flag as CONFLICT, show both, ask user
- Sections unchanged by either → use new version (identical to all three)
- User-added content already present verbatim in the new version → already incorporated
upstream — do NOT re-apply it (the file-level outcome is
Incorporatedwhen ALL user-added content is thus present, as determined by the Step 4 pre-flight classifier)
Two-way merge (fallback when no baseline)
When no pristine baseline is available, use these strengthened heuristics:
CRITICAL RULE: Every file in this backup directory was explicitly detected as modified by the installer's SHA-256 hash comparison. "No custom content" is never a valid conclusion.
For each file: a. Read both versions completely b. Identify ALL differences, then classify each as:
- Mechanical drift — path substitutions (e.g.
/Users/xxx/.claude/→$HOME/.claude/), variable additions (${GSD_WS},${AGENT_SKILLS_*}), error handling additions (|| true) - User customization — added steps/sections, removed sections, reordered content, changed behavior, added frontmatter fields, modified instructions
c. If ANY differences remain after filtering out mechanical drift → those are user customizations. Merge them. d. If ALL differences appear to be mechanical drift → still flag as CONFLICT. The installer's hash check already proved this file was modified. Ask the user: "This file appears to only have path/variable differences. Were there intentional customizations?" Do NOT silently skip.
Git-enhanced two-way merge
When the config directory is a git repo but the pristine install commit can't be found, use commit history to identify user changes:
# Find non-update commits that touched this file
git -C "$CONFIG_DIR" log --oneline --no-merges -- "{file_path}" | grep -v "gsd:update\|gsd-update\|GSD update\|gsd-install"
Each matching commit represents an intentional user modification. Use the commit messages and diffs to understand what was changed and why.
- Write merged result to the installed location
Post-merge verification
After writing each merged file, verify that user modifications survived the merge:
-
Line-count check: Count lines in the backup and the merged result. If the merged result has fewer lines than the backup minus the expected upstream removals, flag for review.
-
Hunk presence check: For each user-added section identified during diff analysis, search the merged output for at least the first significant line (non-blank, non-comment) of each addition. Missing signature lines indicate a dropped hunk.
-
Report warnings inline (do not block):
⚠ Potential dropped content in {file_path}: - Missing hunk near line {N}: "{first_line_preview}..." ({line_count} lines) - Backup available: {patches_dir}/{file_path} -
Produce a Hunk Verification Table — one row per hunk per file. This table is mandatory output and must be produced before Step 5 can proceed. Format:
file hunk_id signature_line line_count verified {file_path} {N} {first_significant_line} {count} yes {file_path} {N} {first_significant_line} {count} no hunk_id— sequential integer per file (1, 2, 3…)signature_line— first non-blank, non-comment line of the user-added sectionline_count— total lines in the hunkverified—yesif the signature_line is present in the merged output,nootherwise
-
Track verification status — add to per-file report:
Merged (verified)vsMerged (⚠ {N} hunks may be missing) -
Report status per file:
Merged— user modifications applied cleanly (show summary of what was preserved)Conflict— user reviewed and chose resolutionIncorporated— user's modification was already adopted upstream (only valid when pristine baseline confirms this — determined exclusively by the Step 4 pre-flight classifier'sincorporated_files, never by inspection)
Never report Skipped — no custom content. If a file is in the backup, it has custom content.
Step 5: Hunk Verification Gate
Two layered gates. Both must pass before proceeding to cleanup.
5a: Deterministic verifier (binding gate, #2969)
Run the deterministic verifier script. Do NOT rely solely on the free-text verified: yes/no Hunk Verification Table from Step 4 — bug #2969 traced repeated false-positive verified: yes reports to that table being filled in without an actual content-presence check. The script performs the check structurally and exits non-zero on any miss.
Run the verifier as a child process (the gsd-tools binary directory is not required — the script ships under gsd-core/bin/ in the source repo and is installed to ${GSD_HOME}/gsd-core/bin/):
PRISTINE_DIR="${CONFIG_DIR}/gsd-pristine"
# Build args as a bash array so paths with spaces survive expansion intact
# (string-concat + unquoted expansion would split incorrectly on whitespace).
VERIFY_ARGS=(
--patches-dir "$PATCHES_DIR"
--config-dir "$CONFIG_DIR"
)
if [ -d "$PRISTINE_DIR" ]; then
VERIFY_ARGS+=(--pristine-dir "$PRISTINE_DIR")
fi
VERIFY_ARGS+=(--json)
# Capture stdout (the structured JSON report) separately from stderr so that
# Node warnings, deprecation notices, or stack traces do not corrupt the
# JSON parse downstream. Stderr is preserved on the controlling terminal
# for operator visibility.
VERIFY_OUTPUT="$(node "${GSD_HOME}/gsd-core/bin/verify-reapply-patches.cjs" "${VERIFY_ARGS[@]}")"
VERIFY_STATUS=$?
Step 5a: drift check — even when VERIFY_STATUS is 0, the report may signal that one or more files were skipped due to pristine-snapshot drift (Bug #3657) or a missing baseline (Bug #934). Parse the JSON and check:
DRIFTED_COUNT="$(echo "$VERIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));process.stdout.write(String(d.drifted||0))")"
DRIFTED_FILES="$(echo "$VERIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));(d.drifted_files||[]).forEach(f=>process.stdout.write(f+'\n'))")"
NO_BASELINE_COUNT="$(echo "$VERIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));process.stdout.write(String(d.no_baseline||0))")"
NO_BASELINE_FILES="$(echo "$VERIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));(d.no_baseline_files||[]).forEach(f=>process.stdout.write(f+'\n'))")"
BASELINE_COVERED="$(echo "$VERIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));process.stdout.write(String(d.baseline_covered||0))")"
CHECKED_COUNT="$(echo "$VERIFY_OUTPUT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));process.stdout.write(String(d.checked||0))")"
Baseline coverage headline (#4135) — BEFORE any pass/fail statement, print the coverage N-of-M line. A run where 12 of 13 files were not diff-verified must never present the same way as a fully-verified one:
Baseline coverage: {BASELINE_COVERED} of {CHECKED_COUNT} file(s) verified against a pristine
baseline ({CHECKED_COUNT - BASELINE_COVERED} unverified)
The verifier also reports this on every run (JSON field baseline_covered; the human summary
leads with the same line). Operators who want the gate itself to fail on low coverage (cautious
environments) pass the opt-in strict flag when invoking the verifier:
--min-baseline-coverage <fraction between 0 and 1> — below the threshold the verifier exits
with code 3 (a real content failure still exits 1 and outranks it). Without the flag the
default advisory posture is unchanged.
If NO_BASELINE_COUNT is greater than 0, emit an advisory warning (non-blocking — the gate still exits 0 for these files). Do NOT halt:
ADVISORY: {NO_BASELINE_COUNT} file(s) could not be diff-verified because no pristine
baseline exists on disk despite a hash being recorded in backup-meta.json (Bug #934:
the installer discarded the only pristine candidate because it was from a newer release).
These files were skipped rather than false-failed; their user customisations may or
may not have survived the merge.
Unverified files:
{each path in NO_BASELINE_FILES, one per line, indented two spaces}
Recommended: manually inspect each file above and confirm your customisations survived.
If DRIFTED_COUNT is greater than 0, STOP and report to the user, then set DRIFT_DETECTED=true and halt — do not proceed to 5b or cleanup:
HALT: {DRIFTED_COUNT} file(s) were skipped by the deterministic verifier because the
gsd-pristine/ snapshot on disk does not match the hash recorded in backup-meta.json
(pristine drift — the snapshot was refreshed to a newer GSD version after the backup
was captured). These files were NOT diff-verified; their user customisations may or
may not have survived the merge.
Drifted files:
{each path in DRIFTED_FILES, one per line, indented two spaces}
Resolve before re-running:
(a) Re-anchor the pristine snapshot to the version recorded in backup-meta.json, or
(b) Restore the affected file(s) from backup and re-merge manually:
cp {patches_dir}/{file} {installed_path} # then re-apply customisations
(c) If the upstream changes are acceptable, update the backup-meta.json
pristine_hashes entry for each drifted file to the current on-disk hash, then
re-run /gsd:update --reapply to re-verify with the refreshed baseline.
Then re-run /gsd:update --reapply to re-verify.
DRIFT_DETECTED=true
# Abort — subsequent steps must not execute when drift is unresolved.
exit 1
If VERIFY_STATUS is non-zero, STOP and report to the user, parsing the JSON output:
ERROR: {failures} file(s) failed deterministic post-merge verification (#2969 gate).
The verifier compared user-added lines (computed from the diff between
the backup and the pristine baseline) against the merged installed file.
Lines listed below are present in the backup but absent from the merged result.
For each failed file:
{file}
missing: {first significant missing line, up to 5 per file}
backup: {patches_dir}/{file}
Resolve before proceeding:
(a) Re-merge the missing content into the installed file by hand, or
(b) Restore from backup: cp {patches_dir}/{file} {installed_path}
Then re-run /gsd:update --reapply to re-verify.
Do not proceed to cleanup until the verifier exits 0.
Only when VERIFY_STATUS is 0 (or when all files had zero significant user-added lines, which the verifier reports as Failures: 0) may execution continue to gate 5b.
5b: Hunk Verification Table review (advisory gate, #1999)
The Hunk Verification Table produced in Step 4 must also be reviewed before proceeding. This is advisory after the script gate but is preserved as a defense-in-depth check — if the script ever has a bug or the pristine baseline is unavailable, the table-based gate still catches obvious regressions.
If the Hunk Verification Table is absent (Step 4 silently produced nothing), STOP and report:
ERROR: Hunk Verification Table is missing — Step 4 did not produce it.
The deterministic verifier (5a) may still have passed, but a missing table
means post-merge verification was not fully completed. Rerun
/gsd:update --reapply to retry with full verification.
A missing table absent from the workflow output cannot bypass this gate.
If any row in the Hunk Verification Table shows verified: no, STOP and report:
ERROR: {N} hunk(s) failed Step 5b verification — content may have been dropped during merge.
Unverified hunks:
{file} hunk {hunk_id}: signature line "{signature_line}" not found in merged output
The backup is preserved at: {patches_dir}/{file}
Review the merged file manually, then either:
(a) Re-merge the missing content by hand, or
(b) Restore from backup: cp {patches_dir}/{file} {installed_path}
Do not proceed to cleanup until both gates (5a and 5b) pass.
Why both gates? 5a (the script) is the binding gate — it does the actual substring check structurally and cannot be shortcut by the LLM. 5b (the table review) is the advisory gate — it provides a redundant safety net via the Step 4 prose summary, ensuring that even a script regression or absent pristine baseline cannot silently allow a verified: no row to slip past, nor can a missing table go unnoticed. Layered gates favour false-positive halts (recoverable) over silent successes on lost content (unrecoverable).
Step 6: Cleanup option
Ask user:
- "Keep patch backups for reference?" → preserve
gsd-local-patches/ - "Clean up patch backups?" → remove
gsd-local-patches/directory
Step 7: Report
## Patches Reapplied
| # | File | Result | User Changes Preserved |
|---|------|--------|----------------------|
| 1 | {file_path} | Merged | Added step X, modified section Y |
| 2 | {file_path} | Incorporated | Already in upstream v{version} |
| 3 | {file_path} | Conflict resolved | User chose: keep custom section |
{count} file(s) updated. Your local modifications are active again.
<success_criteria>
- All backed-up patches processed — zero files left unhandled
- No file classified as "no custom content" or "SKIP" — every backed-up file is definitionally modified
- Three-way merge used when pristine baseline available (git history or gsd-pristine/)
- User modifications identified and merged into new version
- Superseded customizations classified
Incorporatedby the deterministic pre-flight classifier (pristine-confirmed) and not re-grafted - Conflicts surfaced to user with both versions shown
- Status reported for each file with summary of what was preserved
- Post-merge verification checks each file for dropped hunks and warns if content appears missing </success_criteria>