@techgoblin/gobstack 0.0.0-stage → 0.4.4-beta.2
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/CHANGELOG.md +351 -0
- package/LICENSE +21 -0
- package/README.md +217 -2
- package/VERSION +1 -0
- package/adapters/_template/adapter.tsv +16 -0
- package/adapters/_template/detect.sh +10 -0
- package/adapters/_template/emit.sh +5 -0
- package/adapters/_template/verify.sh +4 -0
- package/adapters/claude/adapter.tsv +8 -0
- package/adapters/claude/detect.sh +8 -0
- package/adapters/claude/verify.sh +47 -0
- package/adapters/codex/adapter.tsv +12 -0
- package/adapters/codex/detect.sh +9 -0
- package/adapters/codex/verify.sh +45 -0
- package/adapters/copilot/adapter.tsv +10 -0
- package/adapters/copilot/detect.sh +8 -0
- package/adapters/copilot/verify.sh +45 -0
- package/adapters/cursor/adapter.tsv +11 -0
- package/adapters/cursor/detect.sh +10 -0
- package/adapters/cursor/verify.sh +45 -0
- package/adapters/gemini/adapter.tsv +15 -0
- package/adapters/gemini/detect.sh +11 -0
- package/adapters/gemini/verify.sh +49 -0
- package/adapters/hermes/adapter.tsv +9 -0
- package/adapters/hermes/detect.sh +8 -0
- package/adapters/hermes/verify.sh +27 -0
- package/adapters/opencode/adapter.tsv +14 -0
- package/adapters/opencode/detect.sh +9 -0
- package/adapters/opencode/verify.sh +45 -0
- package/automations/README.md +53 -0
- package/automations/bugreporter-intake.sh +145 -0
- package/automations/drift-audit.sh +139 -0
- package/automations/report.schema.tsv +10 -0
- package/bans/README.md +82 -0
- package/bans/grep-ban.sh +84 -0
- package/bans/layer-check.sh +57 -0
- package/bin/goblin +119 -0
- package/bin/goblin-audit +145 -0
- package/bin/goblin-bans +178 -0
- package/bin/goblin-doctor +233 -0
- package/bin/goblin-emit +484 -0
- package/bin/goblin-init +519 -0
- package/bin/goblin-install +720 -0
- package/bin/goblin-lib.sh +289 -0
- package/bin/goblin-model +105 -0
- package/bin/goblin-upgrade +572 -0
- package/bin/goblin-verify +2798 -0
- package/bin/goblin.js +103 -0
- package/docs/ADOPTION.md +168 -0
- package/docs/CI.md +187 -0
- package/docs/CONTRACTS.md +197 -0
- package/docs/DESIGN.md +92 -0
- package/docs/ENFORCEMENT.md +225 -0
- package/docs/FLOWS.md +164 -0
- package/docs/GUARDRAILS.md +126 -0
- package/docs/GUIDE.md +610 -0
- package/docs/INTEGRATION.md +92 -0
- package/docs/LIMITS.md +591 -0
- package/docs/LOOP.md +165 -0
- package/docs/RE-PLAYBOOK.md +183 -0
- package/docs/RISKS.md +70 -0
- package/docs/ROLES.md +105 -0
- package/manifest/bans.tsv +9 -0
- package/manifest/classes.tsv +61 -0
- package/manifest/enforcement.tsv +88 -0
- package/manifest/glossary.tsv +25 -0
- package/manifest/playbooks.tsv +16 -0
- package/package.json +37 -4
- package/presets/A-shipped-software.yaml +48 -0
- package/presets/B-service-config.yaml +40 -0
- package/presets/C-game.yaml +38 -0
- package/presets/D-knowledge.yaml +41 -0
- package/presets/E-fleet-config.yaml +42 -0
- package/presets/F-electron.yaml +67 -0
- package/roles.yaml +54 -0
- package/skills/goblin-bootstrap/SKILL.md +51 -0
- package/skills/goblin-bugfix/SKILL.md +26 -0
- package/skills/goblin-bugreporter/SKILL.md +52 -0
- package/skills/goblin-drift-audit/SKILL.md +43 -0
- package/skills/goblin-eval/SKILL.md +68 -0
- package/skills/goblin-feature/SKILL.md +26 -0
- package/skills/goblin-feature-map/SKILL.md +140 -0
- package/skills/goblin-handoff/SKILL.md +28 -0
- package/skills/goblin-investigation/SKILL.md +26 -0
- package/skills/goblin-judge/SKILL.md +74 -0
- package/skills/goblin-loop/SKILL.md +88 -0
- package/skills/goblin-mode/SKILL.md +70 -0
- package/skills/goblin-overnight/SKILL.md +42 -0
- package/skills/goblin-pr-gate/SKILL.md +42 -0
- package/skills/goblin-re-mobile/SKILL.md +51 -0
- package/skills/goblin-refactor/SKILL.md +23 -0
- package/skills/goblin-sweep/SKILL.md +23 -0
- package/skills/goblin-tdd-repro/SKILL.md +27 -0
- package/skills/goblin-verify-author/SKILL.md +50 -0
- package/skills/practice/SKILL.md +37 -0
- package/templates/AGENTS.md.tmpl +23 -0
- package/templates/HANDOFF.md.tmpl +43 -0
- package/templates/SPEC.md.tmpl +34 -0
- package/templates/audit-waiver.tsv.tmpl +10 -0
- package/templates/boundary-waivers.tmpl +8 -0
- package/templates/checks/assert.mjs.tmpl +60 -0
- package/templates/checks/gate.sh.tmpl +29 -0
- package/templates/ci/goblin-gate.yml.tmpl +46 -0
- package/templates/goblin.yaml.tmpl +138 -0
- package/templates/install-hooks.allowlist.tmpl +9 -0
- package/templates/loop/decisions.tsv.tmpl +1 -0
- package/templates/loop/predicate.tmpl +16 -0
- package/templates/report.yaml.tmpl +16 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# goblin-upgrade — the 0.4.4 per-repo → global migration (W3, W3-SPEC §2).
|
|
3
|
+
#
|
|
4
|
+
# goblin upgrade [--target <dir>] [--dry-run] [--yes] [--engine-dir <path>]
|
|
5
|
+
#
|
|
6
|
+
# What it does (the corrected 7-step sequence, W3-SPEC §2 — the record is rewritten
|
|
7
|
+
# BEFORE the deletion, and every verify runs against a committed tree, CM-03):
|
|
8
|
+
# 0. preflight: resolve the input state (I1/I2/I3/I4), refuse R1–R10
|
|
9
|
+
# 1. hash-verify every recorded file (IN-02 clause-1 logic) — drift exits 1
|
|
10
|
+
# 2. verify #1 — the untouched repo must already be green
|
|
11
|
+
# 3. land the global engine at --engine-dir (default ~/.goblin/engine), 0700,
|
|
12
|
+
# by copying this repo's own 18 verified bytes (same-hash reuse; different
|
|
13
|
+
# hash → R6 refusal — never overwrite a global engine other repos declare)
|
|
14
|
+
# 4. commit A: engine_dir: active in goblin.yaml + engine: block in
|
|
15
|
+
# installed.json (18 engine entries dropped, skills retained) + gate.sh and
|
|
16
|
+
# the CI run line re-pointed
|
|
17
|
+
# 5. verify #2 — green with BOTH engines present (the vendored one judges; the
|
|
18
|
+
# point is that the rewritten record and gate.sh parse mid-sequence)
|
|
19
|
+
# 6. commit B: git rm exactly the 18 engine paths (the only irreversible step)
|
|
20
|
+
# 7. verify #3 — expected, measured: 39 passed, 0 failed, 11 advisory,
|
|
21
|
+
# 32 skipped, rc 0 (W3-SPEC MA5)
|
|
22
|
+
# 8. report: counts, engine identity, both commit SHAs, the rollback command
|
|
23
|
+
#
|
|
24
|
+
# Exit contract (PLAN-V1 §2.3): 0 ok or no-op · 1 refusal, with the path and the
|
|
25
|
+
# fix · 2 bad input. Refusals R1–R10 are W3-SPEC §4 verbatim in intent.
|
|
26
|
+
#
|
|
27
|
+
# Non-negotiables inherited from the rest of bin/: no npm, no jq, no yq, no
|
|
28
|
+
# network (bash/awk/sed/git only); the engine stays bash; nothing outside the
|
|
29
|
+
# repo is written except the engine lift, which is additive; rollback is pure
|
|
30
|
+
# `git revert` of this command's own two commits (W3-SPEC §5).
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
GOBLIN_UPGRADE_VERSION="0.4.4"
|
|
34
|
+
|
|
35
|
+
usage() { sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
36
|
+
|
|
37
|
+
g_err() { printf 'error: %s\n' "$*" >&2; }
|
|
38
|
+
|
|
39
|
+
# g_expand_tilde <path> — ~ and ~/... to $HOME (the goblin-install:81 idiom).
|
|
40
|
+
g_expand_tilde() {
|
|
41
|
+
case "$1" in
|
|
42
|
+
"~") printf '%s\n' "$HOME" ;;
|
|
43
|
+
"~"/*) printf '%s\n' "$HOME/${1#\~/}" ;;
|
|
44
|
+
*) printf '%s\n' "$1" ;;
|
|
45
|
+
esac
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# sha256 <file> — the one hash spelling, used everywhere.
|
|
49
|
+
sha256() { sha256sum "$1" 2>/dev/null | awk '{print $1}'; }
|
|
50
|
+
|
|
51
|
+
# ---- options ------------------------------------------------------------------
|
|
52
|
+
TARGET="."
|
|
53
|
+
DRY_RUN=0 # --dry-run is the DEFAULT: the CLI never changes a repo
|
|
54
|
+
# without an explicit --yes (PLAN-V1 §8.1 item 8).
|
|
55
|
+
YES=0
|
|
56
|
+
ENGINE_DIR_OPT=""
|
|
57
|
+
while [ $# -gt 0 ]; do
|
|
58
|
+
case "$1" in
|
|
59
|
+
--target) TARGET="${2:-}"; shift 2 ;;
|
|
60
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
61
|
+
--yes) YES=1; shift ;;
|
|
62
|
+
--engine-dir) ENGINE_DIR_OPT="${2:-}"; shift 2 ;;
|
|
63
|
+
--help|-h) usage; exit 0 ;;
|
|
64
|
+
*) g_err "unknown option: $1"; usage >&2; exit 2 ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
if [ "$DRY_RUN" -eq 1 ] && [ "$YES" -eq 1 ]; then
|
|
68
|
+
g_err "--dry-run and --yes are mutually exclusive"
|
|
69
|
+
exit 2
|
|
70
|
+
fi
|
|
71
|
+
[ -n "$TARGET" ] || { g_err "--target needs a directory"; exit 2; }
|
|
72
|
+
[ -z "$ENGINE_DIR_OPT" ] || [ -n "${ENGINE_DIR_OPT#-}" ] || { g_err "--engine-dir needs a path"; exit 2; }
|
|
73
|
+
|
|
74
|
+
for dep in git awk sed grep sha256sum; do
|
|
75
|
+
command -v "$dep" >/dev/null 2>&1 || { g_err "missing dependency: $dep"; exit 2; }
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
[ -d "$TARGET" ] || { g_err "--target is not a directory: $TARGET"; exit 2; }
|
|
79
|
+
REPO=$(cd "$TARGET" && pwd)
|
|
80
|
+
[ -n "$ENGINE_DIR_OPT" ] && ENGINE_DIR_OPT=$(g_expand_tilde "$ENGINE_DIR_OPT")
|
|
81
|
+
ED="${ENGINE_DIR_OPT:-$HOME/.goblin/engine}"
|
|
82
|
+
|
|
83
|
+
CONFIG="$REPO/.goblin/goblin.yaml"
|
|
84
|
+
INSTALLED="$REPO/.goblin/installed.json"
|
|
85
|
+
|
|
86
|
+
# The 18 engine paths (W3-SPEC MA1): .goblin/bin/* (the 4 installable) +
|
|
87
|
+
# .goblin/manifest/* (5) + .goblin/bans/* (3) + .goblin/automations/* (5) +
|
|
88
|
+
# .goblin/roles.yaml. The checkout's bin/ holds 8 files; only 4 are installable
|
|
89
|
+
# (goblin-install:509-522), so the vendored payload is exactly these four.
|
|
90
|
+
ENGINE_BIN_FILES="goblin-verify goblin-lib.sh goblin-audit goblin-bans"
|
|
91
|
+
|
|
92
|
+
# vendored_list — the repo's engine payload, one path per line, .goblin-prefixed.
|
|
93
|
+
# Empty when there is no payload (the engine is already gone).
|
|
94
|
+
vendored_list() {
|
|
95
|
+
local f
|
|
96
|
+
[ -d "$REPO/.goblin/bin" ] && for f in $ENGINE_BIN_FILES; do
|
|
97
|
+
[ -f "$REPO/.goblin/bin/$f" ] && printf '%s\n' ".goblin/bin/$f"
|
|
98
|
+
done
|
|
99
|
+
for d in manifest bans automations; do
|
|
100
|
+
[ -d "$REPO/.goblin/$d" ] || continue
|
|
101
|
+
for f in "$REPO/.goblin/$d/"*; do
|
|
102
|
+
[ -f "$f" ] && printf '%s\n' ".goblin/$d/$(basename "$f")"
|
|
103
|
+
done
|
|
104
|
+
done
|
|
105
|
+
[ -f "$REPO/.goblin/roles.yaml" ] && printf '%s\n' ".goblin/roles.yaml"
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# g_installed_files_strict <record> — only the `files:` map's entries (the same
|
|
110
|
+
# reader goblin-verify's IN-04 uses: the `engine:` block's key/value pairs must
|
|
111
|
+
# never read as files).
|
|
112
|
+
g_installed_files_strict() {
|
|
113
|
+
awk '
|
|
114
|
+
/^ "files": \{[[:space:]]*\},?$/ { next }
|
|
115
|
+
/^ "files": \{/ { f = 1; next }
|
|
116
|
+
f && /^ \},$/ { f = 0 }
|
|
117
|
+
f && /^ "/ {
|
|
118
|
+
line = $0
|
|
119
|
+
sub(/^ "/, "", line)
|
|
120
|
+
p = line; sub(/".*/, "", p)
|
|
121
|
+
h = line; sub(/[^"]*"[[:space:]]*:[[:space:]]*"/, "", h); sub(/".*/, "", h)
|
|
122
|
+
if (p != "") print p "\t" h
|
|
123
|
+
}
|
|
124
|
+
' "$1"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# engine_block_field <key> — one field of the record's engine: block (empty if absent).
|
|
128
|
+
engine_block_field() {
|
|
129
|
+
[ -f "$INSTALLED" ] || return 0
|
|
130
|
+
awk -v k="$1" '
|
|
131
|
+
index($0, "\"engine\"") && /:[[:space:]]*\{/ { inf = 1; next }
|
|
132
|
+
inf && /^[[:space:]]*\}/ { inf = 0 }
|
|
133
|
+
inf && $0 ~ ("\"" k "\"") {
|
|
134
|
+
v = $0; sub(/^[^:]*:[[:space:]]*/, "", v); sub(/,?[[:space:]]*$/, "", v)
|
|
135
|
+
gsub(/"/, "", v); print v; exit
|
|
136
|
+
}
|
|
137
|
+
' "$INSTALLED"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
record_scalar() { # <key>
|
|
141
|
+
sed -n "s/^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}$/\1/p" "$INSTALLED" | head -n 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# record_files_count — entries in the record's files map.
|
|
145
|
+
record_files_count() { g_installed_files_strict "$INSTALLED" | grep -c .; }
|
|
146
|
+
|
|
147
|
+
record_engine_entries_count() { # engine-path entries still in the record
|
|
148
|
+
g_installed_files_strict "$INSTALLED" | cut -f1 | grep -cE '^\.goblin/(bin/|manifest/|bans/|automations/|roles\.yaml$)' || true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# ---- verify #N runner ----------------------------------------------------------
|
|
152
|
+
# The verify INSIDE the sequence must see the target repo's own state: HOME and
|
|
153
|
+
# GOBLIN_ENGINE_DIR are re-pointed for the child only, so the operator's shell
|
|
154
|
+
# environment can never override the migration's own declaration (the engine-dir
|
|
155
|
+
# escape hatch, --engine-dir, must win over an inherited GOBLIN_ENGINE_DIR).
|
|
156
|
+
VERIFY_SCRIPT="" # resolved per step: vendored while it exists, else the engine's
|
|
157
|
+
run_verify() {
|
|
158
|
+
local ed="$ED"
|
|
159
|
+
( cd "$REPO" && env -u GOBLIN_ENGINE_DIR \
|
|
160
|
+
${ed:+GOBLIN_ENGINE_DIR="$ed"} HOME="${GOBLIN_UPGRADE_HOME:-$HOME}" \
|
|
161
|
+
bash "$VERIFY_SCRIPT" "$@" )
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
step_line() { printf 'step %s: %s\n' "$1" "$2"; }
|
|
165
|
+
|
|
166
|
+
# ---- preflight: resolve the input state (W3-SPEC §1.2) -------------------------
|
|
167
|
+
# I1 fresh 0.4.x per-repo install → migrate
|
|
168
|
+
# I2 already migrated → idempotent no-op, exit 0
|
|
169
|
+
# I3 absent/foreign/unparseable → refusal (2 / 1)
|
|
170
|
+
# I4 partial crash shapes → refusal naming resume/rollback (R8/R9)
|
|
171
|
+
|
|
172
|
+
MODE=""
|
|
173
|
+
|
|
174
|
+
# ---- R3: not a git repo --------------------------------------------------------
|
|
175
|
+
if ! git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1; then
|
|
176
|
+
MODE="refuse-R3"
|
|
177
|
+
R3_MSG="upgrade: $REPO is not a git repository - upgrade's safety (three verifies, two commits, revert) requires git. fix: git init && git add -A && git commit"
|
|
178
|
+
else
|
|
179
|
+
# ---- R2: detached HEAD -------------------------------------------------------
|
|
180
|
+
if [ -z "$(git -C "$REPO" symbolic-ref -q HEAD)" ]; then
|
|
181
|
+
MODE="refuse-R2"
|
|
182
|
+
R2_SHA=$(git -C "$REPO" rev-parse --short HEAD)
|
|
183
|
+
R2_MSG="upgrade: $REPO is in detached-HEAD state at $R2_SHA - upgrade's rollback is \`git revert\` of its commits, which needs a branch. fix: git switch -c <name>"
|
|
184
|
+
else
|
|
185
|
+
# ---- R1: dirty tree --------------------------------------------------------
|
|
186
|
+
R1_COUNT=$(git -C "$REPO" status --porcelain | grep -c . || true)
|
|
187
|
+
if [ "${R1_COUNT:-0}" -gt 0 ]; then
|
|
188
|
+
MODE="refuse-R1"
|
|
189
|
+
R1_MSG="upgrade: $R1_COUNT uncommitted entries in $REPO - commit or stash first (upgrade writes its own two commits and cannot mix them with yours)"
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# ---- the record (I3) -----------------------------------------------------------
|
|
195
|
+
RECORD_PRESENT=0
|
|
196
|
+
[ -f "$INSTALLED" ] && RECORD_PRESENT=1
|
|
197
|
+
RECORD_OK=0
|
|
198
|
+
if [ "$RECORD_PRESENT" -eq 1 ]; then
|
|
199
|
+
# It must parse, carry a version, and carry a files map (the strict reader must
|
|
200
|
+
# see at least the shape a 0.4.x install writes).
|
|
201
|
+
REC_VERSION=$(record_scalar version)
|
|
202
|
+
REC_FILES_N=$(record_files_count)
|
|
203
|
+
if grep -q '"version"' "$INSTALLED" 2>/dev/null && [ -n "$REC_VERSION" ] && grep -q '"files"' "$INSTALLED" 2>/dev/null; then
|
|
204
|
+
RECORD_OK=1
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
case "$RECORD_PRESENT:$RECORD_OK" in
|
|
209
|
+
0:*) MODE="${MODE:-refuse-I3-absent}" ;;
|
|
210
|
+
*:0) MODE="${MODE:-refuse-I3-parse}" ;;
|
|
211
|
+
esac
|
|
212
|
+
|
|
213
|
+
if [ "$RECORD_OK" -eq 1 ]; then
|
|
214
|
+
case "$REC_VERSION" in
|
|
215
|
+
0.4.*) : ;;
|
|
216
|
+
*) MODE="${MODE:-refuse-I3-version}" ;;
|
|
217
|
+
esac
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# ---- the state discriminator (I1/I2/I4), only on a well-formed 0.4.x record ----
|
|
221
|
+
if [ "${RECORD_OK:-0}" -eq 1 ] && [ -z "${MODE:-}" ]; then
|
|
222
|
+
BLOCK_MODE=$(engine_block_field mode)
|
|
223
|
+
BLOCK_DIR=$(engine_block_field engine_dir)
|
|
224
|
+
PAYLOAD_N=$(vendored_list | grep -c . || true)
|
|
225
|
+
RECORD_ENGINE_N=$(record_engine_entries_count)
|
|
226
|
+
if [ "$BLOCK_MODE" = "global" ] && [ "$PAYLOAD_N" -eq 0 ]; then
|
|
227
|
+
MODE="noop-R4" # I2: already migrated → idempotent no-op
|
|
228
|
+
elif [ "$BLOCK_MODE" = "global" ] && [ "$PAYLOAD_N" -gt 0 ]; then
|
|
229
|
+
MODE="refuse-R8" # crash between commits A and B
|
|
230
|
+
R8_MSG="upgrade: partial migration detected - the record says global but $PAYLOAD_N engine files are still here. fix: resume by removing them (the engine lives at ${BLOCK_DIR:-$ED}), or roll back: git revert <commit A>"
|
|
231
|
+
elif [ "$BLOCK_MODE" != "global" ] && [ "$RECORD_ENGINE_N" -lt 18 ]; then
|
|
232
|
+
MODE="refuse-R9" # hand-migration: entries PARTIALLY or fully dropped, no block
|
|
233
|
+
R9_MSG="upgrade: $REPO's record is missing its engine: block and carries only $RECORD_ENGINE_N of the 18 engine entries - dropped $((18 - RECORD_ENGINE_N)) by hand, a state goblin upgrade did not produce. fix: restore the record (git checkout .goblin/installed.json) and re-run"
|
|
234
|
+
elif [ "$PAYLOAD_N" -eq 0 ]; then
|
|
235
|
+
MODE="refuse-R5" # fresh-shape record but no payload to lift
|
|
236
|
+
R5_MSG="upgrade: no engine at $ED and no vendored payload to lift it from - install the engine first (goblin-install a fresh repo, or set --engine-dir)"
|
|
237
|
+
elif [ "$RECORD_ENGINE_N" -eq 18 ]; then
|
|
238
|
+
MODE="migrate" # I1: the migration runs (record intact, payload intact)
|
|
239
|
+
else
|
|
240
|
+
MODE="refuse-R9" # record count off in any unnamed combination: refuse, do not guess
|
|
241
|
+
R9_MSG="upgrade: $REPO's record carries $RECORD_ENGINE_N engine entries where a 0.4.4 install records 18 - an unnamed state. fix: restore the record (git checkout .goblin/installed.json) and re-run"
|
|
242
|
+
fi
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# ---- dry-run report -------------------------------------------------------------
|
|
246
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
247
|
+
printf 'goblin upgrade --dry-run — target %s\n' "$REPO"
|
|
248
|
+
printf ' engine-dir: %s\n' "$ED"
|
|
249
|
+
case "$MODE" in
|
|
250
|
+
migrate)
|
|
251
|
+
printf ' state: fresh 0.4.x per-repo install (record %s entries, %s engine payload files on disk)\n' \
|
|
252
|
+
"$(record_files_count)" "$PAYLOAD_N"
|
|
253
|
+
step_line 1 "hash-verify every recorded file (IN-02 clause-1 logic)"
|
|
254
|
+
step_line 2 "verify #1 — the untouched repo must be green"
|
|
255
|
+
step_line 3 "land the global engine at $ED (copy this repo's own $PAYLOAD_N verified bytes; reuse if the hash pair matches)"
|
|
256
|
+
step_line 4 "commit A — engine_dir: active in goblin.yaml + engine: block in installed.json (record $(record_files_count) → $(( $(record_files_count) - RECORD_ENGINE_N )) entries) + gate.sh and the CI run line re-pointed"
|
|
257
|
+
step_line 5 "verify #2 — green with both engines present (vendored judges; proves the rewritten record parses)"
|
|
258
|
+
step_line 6 "commit B — git rm exactly the $PAYLOAD_N engine paths (the only irreversible step)"
|
|
259
|
+
step_line 7 "verify #3 — expected: 39 passed, 0 failed, 11 advisory, 32 skipped, rc 0"
|
|
260
|
+
step_line 8 "report — counts, engine identity, both commit SHAs, the rollback command"
|
|
261
|
+
;;
|
|
262
|
+
noop-R4)
|
|
263
|
+
printf ' state: already migrated — the real run would print no-op and exit 0\n'
|
|
264
|
+
;;
|
|
265
|
+
refuse-R1) printf ' would refuse (exit 1): %s\n' "$R1_MSG" ;;
|
|
266
|
+
refuse-R2) printf ' would refuse (exit 1): %s\n' "$R2_MSG" ;;
|
|
267
|
+
refuse-R3) printf ' would refuse (exit 1): %s\n' "$R3_MSG" ;;
|
|
268
|
+
refuse-I3-absent) printf ' would refuse: nothing to upgrade: %s/.goblin/installed.json — this is not a per-repo goblin-stack install\n' "$REPO" ;;
|
|
269
|
+
refuse-I3-parse) printf ' would refuse: %s does not parse — this is not a per-repo goblin-stack install (exit 2)\n' "$INSTALLED" ;;
|
|
270
|
+
refuse-I3-version) printf ' would refuse: record says %s; this upgrade migrates 0.4.x installs — do not hand-edit the record\n' "$REC_VERSION" ;;
|
|
271
|
+
refuse-R8) printf ' would refuse (exit 1): %s\n' "$R8_MSG" ;;
|
|
272
|
+
refuse-R9) printf ' would refuse (exit 1): %s\n' "$R9_MSG" ;;
|
|
273
|
+
refuse-R5) printf ' would refuse (exit 1): %s\n' "$R5_MSG" ;;
|
|
274
|
+
*) printf ' state: unknown\n' ;;
|
|
275
|
+
esac
|
|
276
|
+
printf ' dry-run wrote nothing.\n'
|
|
277
|
+
exit 0
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# ---- the refusals, for a real run ----------------------------------------------
|
|
281
|
+
case "$MODE" in
|
|
282
|
+
refuse-R1) g_err "$R1_MSG"; exit 1 ;;
|
|
283
|
+
refuse-R2) g_err "$R2_MSG"; exit 1 ;;
|
|
284
|
+
refuse-R3) g_err "$R3_MSG"; exit 1 ;;
|
|
285
|
+
refuse-I3-absent)
|
|
286
|
+
g_err "nothing to upgrade: $INSTALLED — this is not a per-repo goblin-stack install"
|
|
287
|
+
exit 2 ;;
|
|
288
|
+
refuse-I3-parse)
|
|
289
|
+
g_err "$INSTALLED does not parse — this is not a per-repo goblin-stack install"
|
|
290
|
+
exit 2 ;;
|
|
291
|
+
refuse-I3-version)
|
|
292
|
+
g_err "record says $REC_VERSION; this upgrade migrates 0.4.x installs — do not hand-edit the record"
|
|
293
|
+
exit 1 ;;
|
|
294
|
+
refuse-R8) g_err "$R8_MSG"; exit 1 ;;
|
|
295
|
+
refuse-R9) g_err "$R9_MSG"; exit 1 ;;
|
|
296
|
+
refuse-R5) g_err "$R5_MSG"; exit 1 ;;
|
|
297
|
+
noop-R4)
|
|
298
|
+
printf 'no-op: already migrated to %s (cli %s)\n' \
|
|
299
|
+
"${BLOCK_DIR:-$ED}" "$(engine_block_field cli_version)"
|
|
300
|
+
exit 0 ;;
|
|
301
|
+
esac
|
|
302
|
+
|
|
303
|
+
# ================================================================================ step 1
|
|
304
|
+
# Hash-verify EVERY recorded file before writing anything. A hand-edited harness
|
|
305
|
+
# is not migrated silently: any drift exits 1 naming the file.
|
|
306
|
+
step_line 1 "hash-verify the record ($(record_files_count) files)"
|
|
307
|
+
VERIFY_SCRIPT="$REPO/.goblin/bin/goblin-verify"
|
|
308
|
+
while IFS=$'\t' read -r p h; do
|
|
309
|
+
[ -n "$p" ] || continue
|
|
310
|
+
if [ ! -e "$REPO/$p" ]; then
|
|
311
|
+
g_err "upgrade: recorded file is missing: $REPO/$p - restore it (git checkout -- $p) before upgrading"
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
cur=$(sha256 "$REPO/$p")
|
|
315
|
+
if [ "$cur" != "$h" ]; then
|
|
316
|
+
g_err "upgrade: hash drift: $p (recorded $h, now $cur) - restore it (git checkout -- $p) before upgrading; a hand-edited harness is not migrated silently"
|
|
317
|
+
exit 1
|
|
318
|
+
fi
|
|
319
|
+
done < <(g_installed_files_strict "$INSTALLED")
|
|
320
|
+
|
|
321
|
+
# ================================================================================ step 2
|
|
322
|
+
# Verify #1 — the untouched repo must already be green. Do not migrate a repo
|
|
323
|
+
# that is already red: the migration would be blamed for it.
|
|
324
|
+
step_line 2 "verify #1 — the untouched repo must be green"
|
|
325
|
+
VERIFY_SCRIPT="$REPO/.goblin/bin/goblin-verify"
|
|
326
|
+
V1=$(run_verify 2>&1); V1_RC=$?
|
|
327
|
+
if [ "$V1_RC" -ne 0 ]; then
|
|
328
|
+
printf '%s\n' "$V1"
|
|
329
|
+
g_err "upgrade: verify #1 is red (exit $V1_RC) - the repo is not green before the migration; fix it first (the migration would be blamed for a red it did not cause)"
|
|
330
|
+
exit 1
|
|
331
|
+
fi
|
|
332
|
+
printf 'verify #1: %s\n' "$(printf '%s' "$V1" | grep -E 'passed,' | head -n 1 | sed 's/^[[:space:]]*//')"
|
|
333
|
+
|
|
334
|
+
# ================================================================================ step 3
|
|
335
|
+
# Land the global engine. Copy THIS repo's own just-verified bytes — no trust
|
|
336
|
+
# question arises. Same hash pair → reuse; different → R6 refusal.
|
|
337
|
+
step_line 3 "land the global engine at $ED"
|
|
338
|
+
RH=$(sha256 "$REPO/.goblin/bin/goblin-verify")
|
|
339
|
+
RT=$(sha256 "$REPO/.goblin/manifest/enforcement.tsv")
|
|
340
|
+
CLI_VERSION=$(record_scalar version)
|
|
341
|
+
if [ -d "$ED" ]; then
|
|
342
|
+
if [ ! -f "$ED/manifest/enforcement.tsv" ]; then
|
|
343
|
+
g_err "upgrade: $ED exists but holds no manifest/enforcement.tsv - this is not an engine directory (a partial copy is a crashed step 3). fix: pass --engine-dir <path>, or remove $ED and re-run"
|
|
344
|
+
exit 1
|
|
345
|
+
fi
|
|
346
|
+
EH=$(sha256 "$ED/bin/goblin-verify")
|
|
347
|
+
if [ -z "$EH" ]; then
|
|
348
|
+
g_err "upgrade: $ED holds a manifest but no bin/goblin-verify - a partial engine copy. fix: pass --engine-dir <path>, or remove $ED and re-run"
|
|
349
|
+
exit 1
|
|
350
|
+
fi
|
|
351
|
+
ET=$(sha256 "$ED/manifest/enforcement.tsv")
|
|
352
|
+
if [ "$EH" = "$RH" ] && [ "$ET" = "$RT" ]; then
|
|
353
|
+
printf ' reusing existing engine (identical hash pair)\n'
|
|
354
|
+
else
|
|
355
|
+
g_err "upgrade: $ED holds a different engine (cli_sha256 ${EH:-absent}, enforcement ${ET:-absent}) than this repo's ($RH, $RT). I will not overwrite a global engine other repos may declare. fix: pass --engine-dir <path> for a second engine, or upgrade $ED first"
|
|
356
|
+
exit 1
|
|
357
|
+
fi
|
|
358
|
+
else
|
|
359
|
+
lift_rc=0
|
|
360
|
+
while IFS= read -r rel; do
|
|
361
|
+
[ -n "$rel" ] || continue
|
|
362
|
+
dst="$ED/${rel#.goblin/}"
|
|
363
|
+
mkdir -p "$(dirname "$dst")" || { lift_rc=1; break; }
|
|
364
|
+
cp -p "$REPO/$rel" "$dst" || { lift_rc=1; break; }
|
|
365
|
+
done < <(vendored_list)
|
|
366
|
+
if [ "$lift_rc" -ne 0 ]; then
|
|
367
|
+
g_err "upgrade: copying the engine payload to $ED failed - aborting before anything in the repo changed"
|
|
368
|
+
exit 1
|
|
369
|
+
fi
|
|
370
|
+
chmod 700 "$ED"
|
|
371
|
+
# Validate AFTER the copy: the landed engine must carry this repo's exact bytes.
|
|
372
|
+
EH=$(sha256 "$ED/bin/goblin-verify"); ET=$(sha256 "$ED/manifest/enforcement.tsv")
|
|
373
|
+
if [ "$EH" != "$RH" ] || [ "$ET" != "$RT" ]; then
|
|
374
|
+
g_err "upgrade: the engine landed at $ED does not match this repo's bytes (cli $EH/$RH, enforcement $ET/$RT) - a partial copy; remove $ED and re-run"
|
|
375
|
+
exit 1
|
|
376
|
+
fi
|
|
377
|
+
printf ' landed %s engine files at %s (mode 0700)\n' "$(vendored_list | grep -c .)" "$ED"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# ================================================================================ step 4
|
|
381
|
+
# Commit A — the declaration-and-record commit. Written in ONE commit:
|
|
382
|
+
# goblin.yaml the commented `# engine_dir:` placeholder becomes the active line
|
|
383
|
+
# installed.json gains the engine: block; exactly the 18 engine entries drop
|
|
384
|
+
# (skills stay — MA5 shows SK-01/02/04/AU-04 SKIP by mode)
|
|
385
|
+
# checks/gate.sh the source line re-pointed to the engine-resolution idiom
|
|
386
|
+
# .github/workflows/goblin-gate.yml run line re-pointed to npx @techgoblin/gobstack@<v>
|
|
387
|
+
# The record rewrite happens BEFORE any deletion (the plan's order is measured
|
|
388
|
+
# wrong: deleting with the record untouched is IN-02 FAIL, W3-SPEC MA3).
|
|
389
|
+
step_line 4 "commit A — the declaration-and-record commit"
|
|
390
|
+
|
|
391
|
+
engine_dir_line="$ED"
|
|
392
|
+
active_engine_line() { printf 'engine_dir: %s\n' "$engine_dir_line"; }
|
|
393
|
+
|
|
394
|
+
# ---- goblin.yaml: the commented placeholder becomes the active line ----
|
|
395
|
+
if grep -q '^engine_dir:' "$CONFIG"; then
|
|
396
|
+
cur_line=$(sed -n 's/^engine_dir:[[:space:]]*//p' "$CONFIG" | head -n 1)
|
|
397
|
+
if [ "$cur_line" != "$engine_dir_line" ]; then
|
|
398
|
+
g_err "upgrade: $CONFIG already declares engine_dir: $cur_line (wanted $engine_dir_line) - unparseable or hand-edited config; aborting"
|
|
399
|
+
exit 2
|
|
400
|
+
fi
|
|
401
|
+
else
|
|
402
|
+
if ! grep -q '^# engine_dir:' "$CONFIG"; then
|
|
403
|
+
g_err "upgrade: $CONFIG has neither an active nor a commented engine_dir: line - not a 0.4.4 template; aborting"
|
|
404
|
+
exit 2
|
|
405
|
+
fi
|
|
406
|
+
sed -i "s|^# engine_dir:.*|$(active_engine_line | sed 's/[&|]/\\&/g')|" "$CONFIG"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# ---- installed.json: engine block + drop exactly the 18 engine entries ----
|
|
410
|
+
REC_BEFORE=$(record_files_count)
|
|
411
|
+
PYJ=$(mktemp -d)
|
|
412
|
+
cat > "$PYJ/rewrite_record.py" <<'PYEOF'
|
|
413
|
+
import json, sys
|
|
414
|
+
path, ed, ver, cli_sha, tsv_sha = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]
|
|
415
|
+
rec = json.load(open(path))
|
|
416
|
+
rec["engine"] = {"mode": "global", "engine_dir": ed, "cli_version": ver,
|
|
417
|
+
"cli_sha256": cli_sha, "enforcement_tsv_sha256": tsv_sha}
|
|
418
|
+
def is_engine(p):
|
|
419
|
+
return p.startswith(".goblin/bin/") or p.startswith(".goblin/manifest/") \
|
|
420
|
+
or p.startswith(".goblin/bans/") or p.startswith(".goblin/automations/") \
|
|
421
|
+
or p == ".goblin/roles.yaml"
|
|
422
|
+
rec["files"] = {p: h for p, h in rec["files"].items() if not is_engine(p)}
|
|
423
|
+
json.dump(rec, open(path, "w"), indent=2)
|
|
424
|
+
PYEOF
|
|
425
|
+
cat > "$PYJ/rehash_record.py" <<'PYEOF'
|
|
426
|
+
import json, sys
|
|
427
|
+
path, key, new_sha = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
428
|
+
rec = json.load(open(path))
|
|
429
|
+
if key in rec.get("files", {}):
|
|
430
|
+
rec["files"][key] = new_sha
|
|
431
|
+
json.dump(rec, open(path, "w"), indent=2)
|
|
432
|
+
PYEOF
|
|
433
|
+
if ! python3 "$PYJ/rewrite_record.py" "$INSTALLED" "$ED" "$CLI_VERSION" "$RH" "$RT"; then
|
|
434
|
+
g_err "upgrade: rewriting $INSTALLED failed (python3 unavailable or the record unparseable) - the tree is uncommitted; fix: git checkout .goblin/goblin.yaml .goblin/installed.json checks/gate.sh"
|
|
435
|
+
exit 2
|
|
436
|
+
fi
|
|
437
|
+
REC_AFTER=$(record_files_count)
|
|
438
|
+
|
|
439
|
+
# ---- checks/gate.sh: source line re-pointed to the engine-resolution idiom ----
|
|
440
|
+
GATE="$REPO/checks/gate.sh"
|
|
441
|
+
if [ -f "$GATE" ] && grep -q '\.goblin/bin/goblin-lib.sh' "$GATE"; then
|
|
442
|
+
# No machine path is committed: the idiom resolves engine_dir: → the env
|
|
443
|
+
# default → ~/.goblin/engine, then sources <engine>/bin/goblin-lib.sh.
|
|
444
|
+
# The CONFIG line moves UP beside ROOT so the resolver can read it.
|
|
445
|
+
# gate.sh is a files-RECORDED artifact: its record entry is re-hashed below,
|
|
446
|
+
# in the same commit (IN-02 would otherwise read the rewrite as drift).
|
|
447
|
+
awk '
|
|
448
|
+
/^# shellcheck source=\/dev\/null$/ && !done {
|
|
449
|
+
print "# shellcheck source=/dev/null"
|
|
450
|
+
print "# The engine went global (goblin upgrade): resolve it exactly as"
|
|
451
|
+
print "# goblin-verify does - engine_dir:, then the machine default."
|
|
452
|
+
print "CONFIG=\"$ROOT/.goblin/goblin.yaml\""
|
|
453
|
+
print "ENGINE_DIR=$(sed -n \"s/^engine_dir:[[:space:]]*//p\" \"$CONFIG\" | head -n 1)"
|
|
454
|
+
print "case \"$ENGINE_DIR\" in \"~\") ENGINE_DIR=\"$HOME\" ;; \"~\"/*) ENGINE_DIR=\"$HOME/${ENGINE_DIR#\\~/}\" ;; esac"
|
|
455
|
+
print "if [ -z \"$ENGINE_DIR\" ] && [ -n \"${GOBLIN_ENGINE_DIR:-}\" ]; then ENGINE_DIR=$GOBLIN_ENGINE_DIR; fi"
|
|
456
|
+
print "if [ -z \"$ENGINE_DIR\" ] && [ -d \"$HOME/.goblin/engine\" ]; then ENGINE_DIR=$HOME/.goblin/engine; fi"
|
|
457
|
+
print ". \"$ENGINE_DIR/bin/goblin-lib.sh\""
|
|
458
|
+
done=1; next
|
|
459
|
+
}
|
|
460
|
+
/^CONFIG="/ { next }
|
|
461
|
+
/^\. \"\$ROOT\/\.goblin\/bin\/goblin-lib\.sh\"$/ { next }
|
|
462
|
+
{ print }
|
|
463
|
+
' "$GATE" > "$GATE.new" && mv "$GATE.new" "$GATE"
|
|
464
|
+
GATE_NEW=$(sha256 "$GATE")
|
|
465
|
+
python3 "$PYJ/rehash_record.py" "$INSTALLED" "checks/gate.sh" "$GATE_NEW" || {
|
|
466
|
+
g_err "upgrade: re-hashing the rewritten gate.sh in $INSTALLED failed - the tree is uncommitted; fix: git checkout ."
|
|
467
|
+
exit 2
|
|
468
|
+
}
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
# ---- the CI workflow: run line re-pointed to the pinned global form ----
|
|
472
|
+
WF="$REPO/.github/workflows/goblin-gate.yml"
|
|
473
|
+
if [ -f "$WF" ] && grep -q 'run: bash .goblin/bin/goblin-verify' "$WF"; then
|
|
474
|
+
# Names a package W5 publishes; until then CI on a migrated repo is expected
|
|
475
|
+
# red-by-absence — recorded in the commit message, not hidden. The workflow is
|
|
476
|
+
# a files-RECORDED artifact, so this rewrite re-hashes its record entry here,
|
|
477
|
+
# in the same commit (IN-02 hashes every recorded file; a rewritten artifact
|
|
478
|
+
# with a stale recorded hash would fail verify #2 — measured this session).
|
|
479
|
+
sed -i "s|run: bash .goblin/bin/goblin-verify|run: npx @techgoblin/gobstack@$CLI_VERSION run goblin-verify|" "$WF"
|
|
480
|
+
WF_NEW=$(sha256 "$WF")
|
|
481
|
+
python3 "$PYJ/rehash_record.py" "$INSTALLED" ".github/workflows/goblin-gate.yml" "$WF_NEW" || {
|
|
482
|
+
g_err "upgrade: re-hashing the rewritten workflow in $INSTALLED failed - the tree is uncommitted; fix: git checkout ."
|
|
483
|
+
exit 2
|
|
484
|
+
}
|
|
485
|
+
rm -rf "$PYJ"
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
git -C "$REPO" add -A
|
|
489
|
+
git -C "$REPO" -c commit.gpgsign=false commit -q -m "feat: migrate goblin-stack engine to global $ED (W3 step A)
|
|
490
|
+
|
|
491
|
+
The record is rewritten BEFORE the deletion (the plan's order is measured
|
|
492
|
+
wrong: deleting with the record untouched is IN-02 FAIL, W3-SPEC MA3).
|
|
493
|
+
goblin.yaml gains the active engine_dir line; installed.json gains the
|
|
494
|
+
engine block and drops exactly the engine entries (skills stay); gate.sh
|
|
495
|
+
and the CI run line are re-pointed. The CI run line names
|
|
496
|
+
npx @techgoblin/gobstack@$CLI_VERSION verify — a package W5 publishes; until then
|
|
497
|
+
CI on a migrated repo is expected red-by-absence."
|
|
498
|
+
COMMIT_A=$(git -C "$REPO" rev-parse --short HEAD)
|
|
499
|
+
printf ' commit A: %s (record %s → %s entries)\n' "$COMMIT_A" "$REC_BEFORE" "$REC_AFTER"
|
|
500
|
+
|
|
501
|
+
# ================================================================================ step 5
|
|
502
|
+
# Verify #2 — green with both engines present. The vendored engine judges this
|
|
503
|
+
# run (W1's A7 ordering); the point is that the rewritten record, declaration
|
|
504
|
+
# and gate.sh parse and the tree is green MID-sequence. Red → no deletion; the
|
|
505
|
+
# user reverts commit A.
|
|
506
|
+
step_line 5 "verify #2 — green with both engines present"
|
|
507
|
+
VERIFY_SCRIPT="$REPO/.goblin/bin/goblin-verify"
|
|
508
|
+
V2=$(run_verify 2>&1); V2_RC=$?
|
|
509
|
+
if [ "$V2_RC" -ne 0 ]; then
|
|
510
|
+
printf '%s\n' "$V2"
|
|
511
|
+
g_err "upgrade: verify #2 is red (exit $V2_RC) - nothing was deleted. fix: git revert $COMMIT_A"
|
|
512
|
+
exit 1
|
|
513
|
+
fi
|
|
514
|
+
printf 'verify #2: %s\n' "$(printf '%s' "$V2" | grep -E 'passed,' | head -n 1 | sed 's/^[[:space:]]*//')"
|
|
515
|
+
|
|
516
|
+
# ================================================================================ step 6
|
|
517
|
+
# Commit B — the deletion (the only irreversible step). Exactly the payload list
|
|
518
|
+
# the record just dropped; never skills/, installed.json, or any owned file.
|
|
519
|
+
step_line 6 "commit B — git rm the engine payload (the only irreversible step)"
|
|
520
|
+
EXPECT_N=$(vendored_list | grep -c .)
|
|
521
|
+
DELETE_N=0
|
|
522
|
+
while IFS= read -r rel; do
|
|
523
|
+
[ -n "$rel" ] || continue
|
|
524
|
+
case "$rel" in
|
|
525
|
+
.goblin/bin/goblin-verify|.goblin/bin/goblin-lib.sh|.goblin/bin/goblin-audit|.goblin/bin/goblin-bans|\
|
|
526
|
+
.goblin/manifest/*|.goblin/bans/*|.goblin/automations/*|.goblin/roles.yaml) : ;;
|
|
527
|
+
*) g_err "upgrade: refusing to delete anything outside the engine-path list: $rel - aborting, nothing deleted"
|
|
528
|
+
exit 1 ;;
|
|
529
|
+
esac
|
|
530
|
+
DELETE_N=$((DELETE_N + 1))
|
|
531
|
+
done < <(vendored_list)
|
|
532
|
+
if [ "$DELETE_N" -ne "$EXPECT_N" ]; then
|
|
533
|
+
g_err "upgrade: payload list changed mid-run ($EXPECT_N → $DELETE_N) - aborting, nothing deleted"
|
|
534
|
+
exit 1
|
|
535
|
+
fi
|
|
536
|
+
while IFS= read -r rel; do
|
|
537
|
+
[ -n "$rel" ] || continue
|
|
538
|
+
git -C "$REPO" rm -q "$rel"
|
|
539
|
+
done < <(vendored_list)
|
|
540
|
+
git -C "$REPO" -c commit.gpgsign=false commit -q -m "feat: remove the vendored engine payload (W3 step B; engine lives at $ED)
|
|
541
|
+
|
|
542
|
+
The one-way door, split from commit A so the rollback for a red verify #3
|
|
543
|
+
never has to resurrect a deleted file: \`git revert <A> <B>\` restores the
|
|
544
|
+
pre-upgrade tree byte-for-byte (W3-SPEC §5.2)."
|
|
545
|
+
COMMIT_B=$(git -C "$REPO" rev-parse --short HEAD)
|
|
546
|
+
printf ' commit B: %s (%s files deleted)\n' "$COMMIT_B" "$DELETE_N"
|
|
547
|
+
|
|
548
|
+
# ================================================================================ step 7
|
|
549
|
+
# Verify #3 — the post-deletion gate, run against a committed tree (CM-03).
|
|
550
|
+
# Expected, measured: 39 passed, 0 failed, 11 advisory, 32 skipped, rc 0 (MA5).
|
|
551
|
+
step_line 7 "verify #3 — the post-deletion gate"
|
|
552
|
+
if [ -f "$REPO/.goblin/bin/goblin-verify" ]; then
|
|
553
|
+
VERIFY_SCRIPT="$REPO/.goblin/bin/goblin-verify"
|
|
554
|
+
else
|
|
555
|
+
VERIFY_SCRIPT="$ED/bin/goblin-verify"
|
|
556
|
+
fi
|
|
557
|
+
V3=$(run_verify 2>&1); V3_RC=$?
|
|
558
|
+
if [ "$V3_RC" -ne 0 ]; then
|
|
559
|
+
printf '%s\n' "$V3"
|
|
560
|
+
g_err "upgrade: verify #3 is red (exit $V3_RC) - rollback: git revert $COMMIT_A $COMMIT_B"
|
|
561
|
+
exit 1
|
|
562
|
+
fi
|
|
563
|
+
printf 'verify #3: %s\n' "$(printf '%s' "$V3" | grep -E 'passed,' | head -n 1 | sed 's/^[[:space:]]*//')"
|
|
564
|
+
|
|
565
|
+
# ================================================================================ step 8
|
|
566
|
+
step_line 8 "report"
|
|
567
|
+
printf ' files moved: %s (now at %s)\n' "$DELETE_N" "$ED"
|
|
568
|
+
printf ' record entries: %s -> %s\n' "$REC_BEFORE" "$REC_AFTER"
|
|
569
|
+
printf ' engine identity: cli_sha256=%s enforcement_tsv_sha256=%s\n' "$RH" "$RT"
|
|
570
|
+
printf ' commits: %s (A, declaration+record), %s (B, deletion)\n' "$COMMIT_A" "$COMMIT_B"
|
|
571
|
+
printf ' rollback: git revert %s %s\n' "$COMMIT_A" "$COMMIT_B"
|
|
572
|
+
exit 0
|