bigpowers 2.65.1 → 2.67.0
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/.fix_regex.sh +16 -0
- package/.hermes-verify-sed-fix.py +53 -0
- package/.pi/package.json +2 -2
- package/.pi/prompts/build-epic.md +8 -0
- package/.pi/prompts/gate-trace.md +2 -1
- package/.pi/prompts/kickoff-branch.md +19 -0
- package/.pi/prompts/maintain-wiki.md +63 -0
- package/.pi/prompts/plan-tests.md +1 -1
- package/.pi/prompts/release-branch.md +17 -13
- package/.pi/prompts/verify-work.md +9 -5
- package/.pi/skills/build-epic/SKILL.md +8 -0
- package/.pi/skills/gate-trace/SKILL.md +2 -1
- package/.pi/skills/kickoff-branch/SKILL.md +19 -0
- package/.pi/skills/maintain-wiki/SKILL.md +65 -0
- package/.pi/skills/plan-tests/SKILL.md +1 -1
- package/.pi/skills/release-branch/SKILL.md +17 -13
- package/.pi/skills/verify-work/SKILL.md +9 -5
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/SKILL-INDEX.md +1 -1
- package/_test_e28s01.sh +2 -0
- package/hermes-verify-sed-fix.py +30 -0
- package/package.json +1 -1
- package/scripts/build-skill-graph.sh +151 -183
- package/scripts/check-spec-drift.sh +121 -0
- package/scripts/check-spec-version-gap.sh +143 -0
- package/scripts/decompose-conventions.sh +147 -0
- package/scripts/generate-agent-guide.sh +89 -0
- package/scripts/hermes-verify-e39s02.sh +28 -0
- package/scripts/migrate-version.sh +721 -0
- package/scripts/okf-post-sync.sh +163 -0
- package/scripts/run-golden-suite.sh +235 -27
- package/scripts/validate-okf.sh +18 -0
- package/skills/build-epic/SKILL.md +8 -0
- package/skills/gate-trace/SKILL.md +2 -1
- package/skills/kickoff-branch/SKILL.md +19 -0
- package/skills/maintain-wiki/SKILL.md +66 -0
- package/skills/plan-tests/SKILL.md +1 -1
- package/skills/release-branch/SKILL.md +17 -13
- package/skills/verify-work/SKILL.md +9 -5
- package/skills-lock.json +11 -6
- package/tests/fixtures/v1.x-project/CLAUDE.md +4 -0
- package/tests/fixtures/v1.x-project/CONVENTIONS.md +4 -0
- package/tests/fixtures/v2.0.0-project/CLAUDE.md +4 -0
- package/tests/fixtures/v2.0.0-project/CONVENTIONS.md +3 -0
- package/tests/fixtures/v2.20-project/CLAUDE.md +4 -0
- package/tests/fixtures/v2.20-project/CONVENTIONS.md +4 -0
- package/tests/run-golden-migrate-version.sh +155 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e44s03 e44s04
|
|
3
|
+
# migrate-version.sh — one-shot ordered migration engine with triple safety net.
|
|
4
|
+
# Safety: (1) backup, (2) dry-run diff, (3) auto-commit.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
9
|
+
BIGPOWERS_ROOT="$REPO_ROOT"
|
|
10
|
+
YAML_TOOLS="$REPO_ROOT/scripts/yaml-tools.py"
|
|
11
|
+
SPECS_DIR="$REPO_ROOT/specs"
|
|
12
|
+
MIGRATIONS_DIR="$SPECS_DIR/migrations"
|
|
13
|
+
REGISTRY="$MIGRATIONS_DIR/registry.okf.md"
|
|
14
|
+
BACKUP_DIR="$SPECS_DIR/.pre-upgrade-backup"
|
|
15
|
+
|
|
16
|
+
DRY_RUN=false
|
|
17
|
+
FORCE=false
|
|
18
|
+
SKIP_COMMIT=false
|
|
19
|
+
VERBOSE=false
|
|
20
|
+
|
|
21
|
+
# ── usage ────────────────────────────────────────────────────────────────
|
|
22
|
+
usage() {
|
|
23
|
+
cat <<'EOF'
|
|
24
|
+
Usage: scripts/migrate-version.sh [flags]
|
|
25
|
+
|
|
26
|
+
Flags:
|
|
27
|
+
--help Show this message
|
|
28
|
+
--dry-run Compose migration plan and show diff, but make no changes
|
|
29
|
+
--force Skip active-work block detection
|
|
30
|
+
--no-commit Skip auto-commit (still applies transforms and stamps)
|
|
31
|
+
--verbose Show detailed progress for each transform step
|
|
32
|
+
EOF
|
|
33
|
+
exit 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# ── helpers ───────────────────────────────────────────────────────────────
|
|
37
|
+
installed_version() {
|
|
38
|
+
node -e "console.log(require('$BIGPOWERS_ROOT/package.json').version)" 2>/dev/null || echo "unknown"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
yaml_get() {
|
|
42
|
+
local file="$1" key="$2"
|
|
43
|
+
python3 -c "
|
|
44
|
+
import yaml, sys
|
|
45
|
+
try:
|
|
46
|
+
d = yaml.safe_load(open('$file'))
|
|
47
|
+
val = d
|
|
48
|
+
for k in '$key'.split('.'):
|
|
49
|
+
if not isinstance(val, dict): sys.exit(1)
|
|
50
|
+
val = val.get(k)
|
|
51
|
+
if val is None: sys.exit(1)
|
|
52
|
+
print(val)
|
|
53
|
+
except: sys.exit(1)
|
|
54
|
+
" 2>/dev/null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log_info() { echo " [INFO] $*"; }
|
|
58
|
+
log_warn() { echo " [WARN] $*"; }
|
|
59
|
+
log_done() { echo " [OK] $*"; }
|
|
60
|
+
log_fail() { echo " [FAIL] $*"; }
|
|
61
|
+
log_skip() { echo " [SKIP] $*"; }
|
|
62
|
+
|
|
63
|
+
# ── argument parsing ──────────────────────────────────────────────────────
|
|
64
|
+
PROJECT_DIR=""
|
|
65
|
+
|
|
66
|
+
for arg in "$@"; do
|
|
67
|
+
case "$arg" in
|
|
68
|
+
--help|-h) usage ;;
|
|
69
|
+
--dry-run) DRY_RUN=true ;;
|
|
70
|
+
--force) FORCE=true ;;
|
|
71
|
+
--no-commit) SKIP_COMMIT=true ;;
|
|
72
|
+
--verbose) VERBOSE=true ;;
|
|
73
|
+
--project)
|
|
74
|
+
# Next argument is the project dir
|
|
75
|
+
;;
|
|
76
|
+
*)
|
|
77
|
+
if [ "${PREV_ARG:-}" = "--project" ]; then
|
|
78
|
+
PROJECT_DIR="$arg"
|
|
79
|
+
else
|
|
80
|
+
echo "migrate-version: unknown flag: $arg" >&2; exit 4
|
|
81
|
+
fi
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
PREV_ARG="$arg"
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
# If --project given, override paths to target project; migrations remain in bigpowers
|
|
88
|
+
if [ -n "$PROJECT_DIR" ]; then
|
|
89
|
+
REPO_ROOT="$PROJECT_DIR"
|
|
90
|
+
SPECS_DIR="$REPO_ROOT/specs"
|
|
91
|
+
BACKUP_DIR="$SPECS_DIR/.pre-upgrade-backup"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# ── pre-flight: active-work block ─────────────────────────────────────────
|
|
95
|
+
if [ "$FORCE" != "true" ]; then
|
|
96
|
+
branch="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" || true
|
|
97
|
+
if [ -n "${branch:-}" ] && [ "$branch" != "main" ] && [ "$branch" != "master" ]; then
|
|
98
|
+
echo "migrate-version: BLOCKED — on non-main branch '$branch'. Use --force to override." >&2
|
|
99
|
+
exit 3
|
|
100
|
+
fi
|
|
101
|
+
if ! git -C "$REPO_ROOT" diff --quiet -- specs/ 2>/dev/null || \
|
|
102
|
+
! git -C "$REPO_ROOT" diff --cached --quiet -- specs/ 2>/dev/null; then
|
|
103
|
+
echo "migrate-version: BLOCKED — uncommitted changes in specs/. Commit or stash first." >&2
|
|
104
|
+
exit 3
|
|
105
|
+
fi
|
|
106
|
+
if [ -f "$SPECS_DIR/state.yaml" ]; then
|
|
107
|
+
af="$(yaml_get "$SPECS_DIR/state.yaml" "active_flow" 2>/dev/null)" || true
|
|
108
|
+
if [ -n "${af:-}" ] && [ "$af" != "null" ]; then
|
|
109
|
+
echo "migrate-version: BLOCKED — active_flow '$af' is set (mid-epic work). Use --force." >&2
|
|
110
|
+
exit 3
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# ── verify pre-conditions ─────────────────────────────────────────────────
|
|
116
|
+
if [ ! -d "$SPECS_DIR" ]; then
|
|
117
|
+
echo "migrate-version: no specs/ directory found" >&2
|
|
118
|
+
exit 2
|
|
119
|
+
fi
|
|
120
|
+
if [ ! -f "$REGISTRY" ]; then
|
|
121
|
+
echo "migrate-version: migration registry not found at $REGISTRY" >&2
|
|
122
|
+
exit 4
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
INSTALLED="$(installed_version)"
|
|
126
|
+
|
|
127
|
+
# Detect version
|
|
128
|
+
DETECTED=""
|
|
129
|
+
if [ -f "$SPECS_DIR/state.yaml" ]; then
|
|
130
|
+
stamped="$(yaml_get "$SPECS_DIR/state.yaml" "bigpowers_version" 2>/dev/null)" || true
|
|
131
|
+
if [ -n "${stamped:-}" ] && [ "$stamped" != "null" ]; then
|
|
132
|
+
DETECTED="$stamped"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
if [ -z "$DETECTED" ]; then
|
|
136
|
+
if [ -f "$SPECS_DIR/STATE.md" ] && [ ! -f "$SPECS_DIR/state.yaml" ]; then
|
|
137
|
+
DETECTED="1.0.0"
|
|
138
|
+
elif [ -f "$SPECS_DIR/state.yaml" ]; then
|
|
139
|
+
if yaml_get "$SPECS_DIR/state.yaml" "epic_cycle" >/dev/null 2>&1; then
|
|
140
|
+
DETECTED="2.20.0"
|
|
141
|
+
else
|
|
142
|
+
DETECTED="2.0.0"
|
|
143
|
+
fi
|
|
144
|
+
else
|
|
145
|
+
echo "migrate-version: cannot determine spec version" >&2
|
|
146
|
+
exit 4
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
echo "migrate-version: detected=$DETECTED installed=$INSTALLED"
|
|
151
|
+
|
|
152
|
+
if [ "$DETECTED" = "$INSTALLED" ]; then
|
|
153
|
+
echo "migrate-version: no version gap — specs are current"
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# ── build migration plan ──────────────────────────────────────────────────
|
|
158
|
+
echo "migrate-version: building migration plan from $REGISTRY …"
|
|
159
|
+
|
|
160
|
+
MIGRATION_PLAN="$(python3 -c "
|
|
161
|
+
import yaml, json, sys
|
|
162
|
+
|
|
163
|
+
with open('$REGISTRY') as f:
|
|
164
|
+
parts = f.read().split('---')
|
|
165
|
+
fm = yaml.safe_load(parts[1])
|
|
166
|
+
|
|
167
|
+
detected = '$DETECTED'
|
|
168
|
+
installed = '$INSTALLED'
|
|
169
|
+
|
|
170
|
+
plan = []
|
|
171
|
+
for m in sorted(fm.get('migrations', []), key=lambda x: x.get('order', 99)):
|
|
172
|
+
sid = m['id']
|
|
173
|
+
since = m.get('since_version', '0')
|
|
174
|
+
order = m.get('order', 99)
|
|
175
|
+
deps = m.get('depends_on', [])
|
|
176
|
+
plan.append({'id': sid, 'file': m.get('file', sid + '.okf.md'),
|
|
177
|
+
'since_version': since, 'order': order,
|
|
178
|
+
'depends_on': deps, 'title': m.get('title', sid)})
|
|
179
|
+
|
|
180
|
+
applicable = [m for m in plan if m['since_version'] > detected and m['since_version'] <= installed]
|
|
181
|
+
print(json.dumps(applicable))
|
|
182
|
+
" 2>/dev/null)"
|
|
183
|
+
|
|
184
|
+
MIGRATION_COUNT="$(echo "$MIGRATION_PLAN" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null)"
|
|
185
|
+
|
|
186
|
+
if [ "${MIGRATION_COUNT:-0}" -eq 0 ]; then
|
|
187
|
+
echo "migrate-version: no applicable migrations for $DETECTED → $INSTALLED — stamping version only"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
echo "migrate-version: $MIGRATION_COUNT migration(s) to apply"
|
|
191
|
+
|
|
192
|
+
if [ "$VERBOSE" = "true" ]; then
|
|
193
|
+
echo "$MIGRATION_PLAN" | python3 -c "
|
|
194
|
+
import json,sys
|
|
195
|
+
for m in json.load(sys.stdin):
|
|
196
|
+
print(f\" {m['order']}. {m['id']}: {m['title']} (since {m['since_version']})\")
|
|
197
|
+
"
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# ── Safety Layer 1: backup ────────────────────────────────────────────────
|
|
201
|
+
if [ "$DRY_RUN" != "true" ]; then
|
|
202
|
+
log_info "Creating backup: $BACKUP_DIR"
|
|
203
|
+
rm -rf "$BACKUP_DIR"
|
|
204
|
+
mkdir -p "$BACKUP_DIR"
|
|
205
|
+
cp -r "$SPECS_DIR"/* "$BACKUP_DIR"/ 2>/dev/null || true
|
|
206
|
+
rm -rf "$BACKUP_DIR/.pre-upgrade-backup" 2>/dev/null || true
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# ── Safety Layer 2: dry-run ───────────────────────────────────────────────
|
|
210
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
211
|
+
echo ""
|
|
212
|
+
echo "=========================================="
|
|
213
|
+
echo " DRY RUN — no changes will be made"
|
|
214
|
+
echo "=========================================="
|
|
215
|
+
echo ""
|
|
216
|
+
echo "Detected version: $DETECTED"
|
|
217
|
+
echo "Target version: $INSTALLED"
|
|
218
|
+
echo "Migrations:"
|
|
219
|
+
echo "$MIGRATION_PLAN" | python3 -c "
|
|
220
|
+
import json,sys
|
|
221
|
+
for m in json.load(sys.stdin):
|
|
222
|
+
deps = m.get('depends_on', [])
|
|
223
|
+
dep_str = ' (depends on: ' + ', '.join(deps) + ')' if deps else ''
|
|
224
|
+
print(f\" [{m['order']}] {m['id']}: {m['title']}{dep_str}\")
|
|
225
|
+
"
|
|
226
|
+
|
|
227
|
+
echo ""
|
|
228
|
+
echo "Transforms preview:"
|
|
229
|
+
echo "$MIGRATION_PLAN" | python3 -c "
|
|
230
|
+
import json, sys, os
|
|
231
|
+
for m in json.load(sys.stdin):
|
|
232
|
+
bundle_path = os.path.join('$MIGRATIONS_DIR', m['file'])
|
|
233
|
+
if not os.path.exists(bundle_path):
|
|
234
|
+
print(f\" WARN {m['id']}: bundle file not found ({m['file']})\")
|
|
235
|
+
continue
|
|
236
|
+
with open(bundle_path) as f:
|
|
237
|
+
parts = f.read().split('---')
|
|
238
|
+
fm = __import__('yaml').safe_load(parts[1])
|
|
239
|
+
transforms = fm.get('transforms', [])
|
|
240
|
+
print(f\"\n -- {m['id']}: {m['title']} --\")
|
|
241
|
+
for t in transforms:
|
|
242
|
+
action = t.get('action', '?')
|
|
243
|
+
note = t.get('note', '')
|
|
244
|
+
note_str = f' ({note})' if note else ''
|
|
245
|
+
if action == 'convert_md_to_yaml':
|
|
246
|
+
src = t.get('source', '?')
|
|
247
|
+
tgt = t.get('target', '?')
|
|
248
|
+
unc = t.get('uncertainty', [])
|
|
249
|
+
print(f\" {action}: {src} -> {tgt}{note_str}\")
|
|
250
|
+
for u in unc:
|
|
251
|
+
print(f\" GUESS: {u.get('field','?')} - {u.get('reason','?')}\")
|
|
252
|
+
elif action in ('rename_file', 'move_file'):
|
|
253
|
+
print(f\" {action}: {t.get('source','?')} -> {t.get('target','?')}{note_str}\")
|
|
254
|
+
elif action == 'delete_file':
|
|
255
|
+
print(f\" {action}: {t.get('path','?')}{note_str}\")
|
|
256
|
+
elif action in ('set_yaml_key', 'rename_yaml_key', 'delete_yaml_key'):
|
|
257
|
+
print(f\" {action}: {t.get('file','?')} {t.get('key','?') or t.get('old_key','?')} = {t.get('value','?')}{note_str}\")
|
|
258
|
+
elif action == 'create_file_from_template':
|
|
259
|
+
print(f\" {action}: {t.get('source','?')} -> {t.get('target','?')}{note_str}\")
|
|
260
|
+
else:
|
|
261
|
+
print(f\" {action}{note_str}\")
|
|
262
|
+
for a in fm.get('actions_needed', []):
|
|
263
|
+
v = a.get('verify', '')
|
|
264
|
+
if v: print(f\" verify: {v}\")
|
|
265
|
+
" 2>/dev/null
|
|
266
|
+
|
|
267
|
+
echo ""
|
|
268
|
+
echo "Run without --dry-run to apply these migrations."
|
|
269
|
+
exit 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# ── Apply phase ───────────────────────────────────────────────────────────
|
|
273
|
+
echo ""
|
|
274
|
+
echo "Applying migrations …"
|
|
275
|
+
SUCCEEDED=()
|
|
276
|
+
FAILED=()
|
|
277
|
+
SKIPPED=()
|
|
278
|
+
# Default empty values for subshell-safe access
|
|
279
|
+
SUCCEEDED_COUNT=0
|
|
280
|
+
FAILED_COUNT=0
|
|
281
|
+
SKIPPED_COUNT=0
|
|
282
|
+
UNCERTAINTY_COUNT=0
|
|
283
|
+
|
|
284
|
+
apply_transform() {
|
|
285
|
+
local mig_id="$1" action="$2" tjson="$3"
|
|
286
|
+
|
|
287
|
+
case "$action" in
|
|
288
|
+
convert_md_to_yaml)
|
|
289
|
+
local src tgt hm unc_count arch
|
|
290
|
+
src="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
|
|
291
|
+
tgt="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
|
|
292
|
+
arch="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('archive_source','false'))" 2>/dev/null)" || arch="false"
|
|
293
|
+
if [ ! -f "$REPO_ROOT/$src" ]; then
|
|
294
|
+
log_skip "$action: source '$src' not found"
|
|
295
|
+
SKIPPED+=("$action:$src"); return
|
|
296
|
+
fi
|
|
297
|
+
hm="$(echo "$tjson" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('heuristic_map',{})))")"
|
|
298
|
+
python3 -c "
|
|
299
|
+
import json, re, yaml, sys, os
|
|
300
|
+
src = '$REPO_ROOT/$src'
|
|
301
|
+
tgt = '$REPO_ROOT/$tgt'
|
|
302
|
+
heuristic_map = json.loads('''$hm''')
|
|
303
|
+
content = open(src).read()
|
|
304
|
+
result = {}
|
|
305
|
+
for md_key, yaml_key in heuristic_map.items():
|
|
306
|
+
pat = re.compile(r'^' + re.escape(md_key.rstrip(':')) + r'\\s*[:=]\\s*(.+)$', re.MULTILINE | re.IGNORECASE)
|
|
307
|
+
m = pat.search(content)
|
|
308
|
+
if m:
|
|
309
|
+
val = m.group(1).strip()
|
|
310
|
+
try: val = yaml.safe_load(val) or val
|
|
311
|
+
except: pass
|
|
312
|
+
parts = yaml_key.split('.')
|
|
313
|
+
cur = result
|
|
314
|
+
for p in parts[:-1]:
|
|
315
|
+
if p not in cur or not isinstance(cur[p], dict):
|
|
316
|
+
cur[p] = {}
|
|
317
|
+
cur = cur[p]
|
|
318
|
+
cur[parts[-1]] = val
|
|
319
|
+
os.makedirs(os.path.dirname(tgt), exist_ok=True)
|
|
320
|
+
with open(tgt, 'w') as f:
|
|
321
|
+
yaml.safe_dump(result, f, sort_keys=False, allow_unicode=True, width=100)
|
|
322
|
+
"
|
|
323
|
+
unc_count="$(echo "$tjson" | python3 -c "import json,sys; print(len(json.load(sys.stdin).get('uncertainty',[])))")"
|
|
324
|
+
UNCERTAINTY_COUNT=$((UNCERTAINTY_COUNT + unc_count))
|
|
325
|
+
# Archive source file if requested
|
|
326
|
+
if [ "$arch" = "true" ] || [ "$arch" = "True" ]; then
|
|
327
|
+
local archive_dir="$REPO_ROOT/specs/archive"
|
|
328
|
+
mkdir -p "$archive_dir"
|
|
329
|
+
mv "$REPO_ROOT/$src" "$archive_dir/"
|
|
330
|
+
log_done "$action: $src -> $tgt (source archived to specs/archive/)"
|
|
331
|
+
else
|
|
332
|
+
log_done "$action: $src -> $tgt"
|
|
333
|
+
fi
|
|
334
|
+
;;
|
|
335
|
+
|
|
336
|
+
rename_file)
|
|
337
|
+
local src tgt
|
|
338
|
+
src="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
|
|
339
|
+
tgt="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
|
|
340
|
+
if [ ! -e "$REPO_ROOT/$src" ]; then
|
|
341
|
+
log_skip "$action: source '$src' not found — idempotent"
|
|
342
|
+
SKIPPED+=("$action:$src"); return
|
|
343
|
+
fi
|
|
344
|
+
if [ -e "$REPO_ROOT/$tgt" ]; then
|
|
345
|
+
log_skip "$action: target '$tgt' already exists — idempotent"
|
|
346
|
+
SKIPPED+=("$action:$src"); return
|
|
347
|
+
fi
|
|
348
|
+
mkdir -p "$(dirname "$REPO_ROOT/$tgt")"
|
|
349
|
+
mv "$REPO_ROOT/$src" "$REPO_ROOT/$tgt"
|
|
350
|
+
log_done "$action: $src -> $tgt"
|
|
351
|
+
;;
|
|
352
|
+
|
|
353
|
+
move_file)
|
|
354
|
+
local src tgt
|
|
355
|
+
src="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
|
|
356
|
+
tgt="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
|
|
357
|
+
if [ ! -e "$REPO_ROOT/$src" ]; then
|
|
358
|
+
log_skip "$action: source '$src' not found"
|
|
359
|
+
SKIPPED+=("$action:$src"); return
|
|
360
|
+
fi
|
|
361
|
+
if [ -e "$REPO_ROOT/$tgt" ]; then
|
|
362
|
+
log_skip "$action: target '$tgt' already exists — idempotent"
|
|
363
|
+
SKIPPED+=("$action:$src"); return
|
|
364
|
+
fi
|
|
365
|
+
mkdir -p "$(dirname "$REPO_ROOT/$tgt")"
|
|
366
|
+
mv "$REPO_ROOT/$src" "$REPO_ROOT/$tgt"
|
|
367
|
+
log_done "$action: $src -> $tgt"
|
|
368
|
+
;;
|
|
369
|
+
|
|
370
|
+
delete_file)
|
|
371
|
+
local path
|
|
372
|
+
path="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('path',''))")"
|
|
373
|
+
if [ ! -e "$REPO_ROOT/$path" ]; then
|
|
374
|
+
log_skip "$action: '$path' not found — idempotent"
|
|
375
|
+
SKIPPED+=("$action:$path"); return
|
|
376
|
+
fi
|
|
377
|
+
rm -f "$REPO_ROOT/$path"
|
|
378
|
+
log_done "$action: $path"
|
|
379
|
+
;;
|
|
380
|
+
|
|
381
|
+
set_yaml_key)
|
|
382
|
+
local file key val if_missing
|
|
383
|
+
file="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('file',''))")"
|
|
384
|
+
key="$(echo "$tjson" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('path') or d.get('key',''))")"
|
|
385
|
+
val="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('value',''))")"
|
|
386
|
+
if_missing="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('if_missing','false'))" 2>/dev/null)" || if_missing="false"
|
|
387
|
+
if [ "$if_missing" = "true" ] || [ "$if_missing" = "True" ]; then
|
|
388
|
+
if yaml_get "$REPO_ROOT/$file" "$key" >/dev/null 2>&1; then
|
|
389
|
+
log_skip "$action: $file $key already set (if_missing=true) — idempotent"
|
|
390
|
+
SKIPPED+=("$action:$key")
|
|
391
|
+
return
|
|
392
|
+
fi
|
|
393
|
+
fi
|
|
394
|
+
python3 "$YAML_TOOLS" set "$REPO_ROOT/$file" "$key" "$val"
|
|
395
|
+
log_done "$action: $file $key = $val"
|
|
396
|
+
;;
|
|
397
|
+
|
|
398
|
+
rename_yaml_key)
|
|
399
|
+
local file old_key new_key old_val
|
|
400
|
+
file="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('file',''))")"
|
|
401
|
+
old_key="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('old_key',''))")"
|
|
402
|
+
new_key="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('new_key',''))")"
|
|
403
|
+
old_val="$(yaml_get "$REPO_ROOT/$file" "$old_key" 2>/dev/null)" || true
|
|
404
|
+
if [ -z "${old_val:-}" ]; then
|
|
405
|
+
log_skip "$action: key '$old_key' not found in $file"
|
|
406
|
+
SKIPPED+=("$action:$old_key"); return
|
|
407
|
+
fi
|
|
408
|
+
python3 "$YAML_TOOLS" set "$REPO_ROOT/$file" "$new_key" "$old_val"
|
|
409
|
+
python3 -c "
|
|
410
|
+
import yaml
|
|
411
|
+
data = yaml.safe_load(open('$REPO_ROOT/$file'))
|
|
412
|
+
parts = '$old_key'.split('.')
|
|
413
|
+
cur = data
|
|
414
|
+
for p in parts[:-1]:
|
|
415
|
+
if not isinstance(cur, dict): break
|
|
416
|
+
cur = cur.get(p, {})
|
|
417
|
+
if isinstance(cur, dict) and parts[-1] in cur:
|
|
418
|
+
del cur[parts[-1]]
|
|
419
|
+
with open('$REPO_ROOT/$file', 'w') as f:
|
|
420
|
+
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True, width=100)
|
|
421
|
+
" 2>/dev/null
|
|
422
|
+
log_done "$action: $file $old_key -> $new_key = $old_val"
|
|
423
|
+
;;
|
|
424
|
+
|
|
425
|
+
delete_yaml_key)
|
|
426
|
+
local file key
|
|
427
|
+
file="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('file',''))")"
|
|
428
|
+
key="$(echo "$tjson" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('path') or d.get('key',''))")"
|
|
429
|
+
if ! yaml_get "$REPO_ROOT/$file" "$key" >/dev/null 2>&1; then
|
|
430
|
+
log_skip "$action: key '$key' not found in $file — idempotent"
|
|
431
|
+
SKIPPED+=("$action:$key"); return
|
|
432
|
+
fi
|
|
433
|
+
python3 -c "
|
|
434
|
+
import yaml
|
|
435
|
+
data = yaml.safe_load(open('$REPO_ROOT/$file'))
|
|
436
|
+
parts = '$key'.split('.')
|
|
437
|
+
cur = data
|
|
438
|
+
for p in parts[:-1]:
|
|
439
|
+
if not isinstance(cur, dict): break
|
|
440
|
+
cur = cur.get(p, {})
|
|
441
|
+
if isinstance(cur, dict) and parts[-1] in cur:
|
|
442
|
+
del cur[parts[-1]]
|
|
443
|
+
with open('$REPO_ROOT/$file', 'w') as f:
|
|
444
|
+
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True, width=100)
|
|
445
|
+
" 2>/dev/null
|
|
446
|
+
log_done "$action: $file $key"
|
|
447
|
+
;;
|
|
448
|
+
|
|
449
|
+
create_file_from_template)
|
|
450
|
+
local src tgt subs inline_tmpl
|
|
451
|
+
src="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
|
|
452
|
+
tgt="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
|
|
453
|
+
subs="$(echo "$tjson" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('substitutions',{})))")"
|
|
454
|
+
inline_tmpl="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('template',''))")"
|
|
455
|
+
if [ -e "$REPO_ROOT/$tgt" ]; then
|
|
456
|
+
log_skip "$action: target '$tgt' already exists — idempotent"
|
|
457
|
+
SKIPPED+=("$action:$tgt"); return
|
|
458
|
+
fi
|
|
459
|
+
mkdir -p "$(dirname "$REPO_ROOT/$tgt")"
|
|
460
|
+
# Inline template (content embedded in OKF bundle)
|
|
461
|
+
if [ -n "$inline_tmpl" ]; then
|
|
462
|
+
echo "$inline_tmpl" > "$REPO_ROOT/$tgt"
|
|
463
|
+
if [ "$subs" != "{}" ]; then
|
|
464
|
+
python3 -c "
|
|
465
|
+
import json
|
|
466
|
+
subs = json.loads('''$subs''')
|
|
467
|
+
content = open('$REPO_ROOT/$tgt').read()
|
|
468
|
+
for k, v in subs.items():
|
|
469
|
+
content = content.replace(k, v)
|
|
470
|
+
with open('$REPO_ROOT/$tgt', 'w') as f:
|
|
471
|
+
f.write(content)
|
|
472
|
+
" 2>/dev/null
|
|
473
|
+
fi
|
|
474
|
+
log_done "$action: inline template -> $tgt"
|
|
475
|
+
return
|
|
476
|
+
fi
|
|
477
|
+
# File-based template resolution
|
|
478
|
+
template_path=""
|
|
479
|
+
if [ -n "$src" ]; then
|
|
480
|
+
# Try source as relative path first, then as template name
|
|
481
|
+
if [ -f "$REPO_ROOT/$src" ]; then
|
|
482
|
+
template_path="$REPO_ROOT/$src"
|
|
483
|
+
elif [ -f "$REPO_ROOT/specs/templates/${src##*/}" ]; then
|
|
484
|
+
template_path="$REPO_ROOT/specs/templates/${src##*/}"
|
|
485
|
+
fi
|
|
486
|
+
fi
|
|
487
|
+
if [ -z "$template_path" ] || [ ! -f "$template_path" ]; then
|
|
488
|
+
log_skip "$action: template not found (src='$src')"
|
|
489
|
+
SKIPPED+=("$action:$src"); return
|
|
490
|
+
fi
|
|
491
|
+
cp "$template_path" "$REPO_ROOT/$tgt"
|
|
492
|
+
python3 -c "
|
|
493
|
+
import json
|
|
494
|
+
subs = json.loads('''$subs''')
|
|
495
|
+
content = open('$REPO_ROOT/$tgt').read()
|
|
496
|
+
for k, v in subs.items():
|
|
497
|
+
content = content.replace(k, v)
|
|
498
|
+
with open('$REPO_ROOT/$tgt', 'w') as f:
|
|
499
|
+
f.write(content)
|
|
500
|
+
" 2>/dev/null
|
|
501
|
+
log_done "$action: $src -> $tgt"
|
|
502
|
+
;;
|
|
503
|
+
|
|
504
|
+
*)
|
|
505
|
+
log_skip "$action: unknown transform type"
|
|
506
|
+
SKIPPED+=("$action")
|
|
507
|
+
;;
|
|
508
|
+
esac
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# Execute each migration in order
|
|
512
|
+
MIG_LIST="$(echo "$MIGRATION_PLAN" | python3 -c "import json,sys; [print(json.dumps(m)) for m in json.load(sys.stdin)]")"
|
|
513
|
+
while IFS= read -r mig_json; do
|
|
514
|
+
[ -z "$mig_json" ] && continue
|
|
515
|
+
MIG_ID="$(echo "$mig_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")"
|
|
516
|
+
MIG_FILE="$(echo "$mig_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['file'])")"
|
|
517
|
+
MIG_TITLE="$(echo "$mig_json" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")"
|
|
518
|
+
|
|
519
|
+
BUNDLE_PATH="$MIGRATIONS_DIR/$MIG_FILE"
|
|
520
|
+
if [ ! -f "$BUNDLE_PATH" ]; then
|
|
521
|
+
log_warn "$MIG_ID: bundle file not found — skipping"
|
|
522
|
+
SKIPPED+=("$MIG_ID:bundle_missing")
|
|
523
|
+
continue
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
echo ""
|
|
527
|
+
echo "── $MIG_ID: $MIG_TITLE ──"
|
|
528
|
+
|
|
529
|
+
TRANSFORMS="$(python3 -c "
|
|
530
|
+
import yaml, json
|
|
531
|
+
with open('$BUNDLE_PATH') as f:
|
|
532
|
+
parts = f.read().split('---')
|
|
533
|
+
fm = yaml.safe_load(parts[1])
|
|
534
|
+
print(json.dumps(fm.get('transforms', []), default=str))
|
|
535
|
+
" 2>/dev/null)"
|
|
536
|
+
|
|
537
|
+
T_LIST="$(echo "$TRANSFORMS" | python3 -c "import json,sys; [print(json.dumps(t, default=str)) for t in json.load(sys.stdin)]")"
|
|
538
|
+
while IFS= read -r tjson; do
|
|
539
|
+
[ -z "$tjson" ] && continue
|
|
540
|
+
action="$(echo "$tjson" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action',''))")"
|
|
541
|
+
apply_transform "$MIG_ID" "$action" "$tjson"
|
|
542
|
+
done <<< "$T_LIST"
|
|
543
|
+
|
|
544
|
+
# Run verify commands
|
|
545
|
+
VERIFY_CMDS="$(python3 -c "
|
|
546
|
+
import yaml, json
|
|
547
|
+
with open('$BUNDLE_PATH') as f:
|
|
548
|
+
parts = f.read().split('---')
|
|
549
|
+
fm = yaml.safe_load(parts[1])
|
|
550
|
+
for a in fm.get('actions_needed', []):
|
|
551
|
+
v = a.get('verify', '')
|
|
552
|
+
if v: print(v)
|
|
553
|
+
" 2>/dev/null)"
|
|
554
|
+
|
|
555
|
+
VERIFY_OK=true
|
|
556
|
+
while IFS= read -r vcmd; do
|
|
557
|
+
[ -z "$vcmd" ] && continue
|
|
558
|
+
log_info "verify: $vcmd"
|
|
559
|
+
if eval "$vcmd" 2>&1; then
|
|
560
|
+
log_done "verify passed"
|
|
561
|
+
else
|
|
562
|
+
log_fail "verify failed"
|
|
563
|
+
VERIFY_OK=false
|
|
564
|
+
fi
|
|
565
|
+
done <<< "$VERIFY_CMDS"
|
|
566
|
+
|
|
567
|
+
if [ "$VERIFY_OK" = "true" ]; then
|
|
568
|
+
SUCCEEDED+=("$MIG_ID")
|
|
569
|
+
else
|
|
570
|
+
FAILED+=("$MIG_ID")
|
|
571
|
+
fi
|
|
572
|
+
done <<< "$MIG_LIST"
|
|
573
|
+
|
|
574
|
+
# ── stamp ─────────────────────────────────────────────────────────────────
|
|
575
|
+
if [ -f "$SPECS_DIR/state.yaml" ]; then
|
|
576
|
+
log_info "Stamping bigpowers_version=$INSTALLED in state.yaml"
|
|
577
|
+
python3 "$YAML_TOOLS" set "$SPECS_DIR/state.yaml" "bigpowers_version" "$INSTALLED"
|
|
578
|
+
log_done "stamp applied"
|
|
579
|
+
fi
|
|
580
|
+
|
|
581
|
+
# ── validate-okf soft gate ────────────────────────────────────────────────
|
|
582
|
+
if [ -f "$SCRIPT_DIR/validate-okf.sh" ]; then
|
|
583
|
+
log_info "Running validate-okf.sh (soft gate) …"
|
|
584
|
+
if bash "$SCRIPT_DIR/validate-okf.sh" --dir "$MIGRATIONS_DIR" 2>&1; then
|
|
585
|
+
log_done "validate-okf: PASS"
|
|
586
|
+
else
|
|
587
|
+
log_warn "validate-okf: some checks failed (soft gate — non-blocking)"
|
|
588
|
+
fi
|
|
589
|
+
else
|
|
590
|
+
log_info "validate-okf.sh not found — skipping soft gate"
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
# ── global validate-specs-yaml gate ───────────────────────────────────────
|
|
594
|
+
if [ -f "$SCRIPT_DIR/validate-specs-yaml.sh" ]; then
|
|
595
|
+
log_info "Running global validate-specs-yaml.sh …"
|
|
596
|
+
if bash "$SCRIPT_DIR/validate-specs-yaml.sh" 2>&1; then
|
|
597
|
+
log_done "validate-specs-yaml: PASS"
|
|
598
|
+
else
|
|
599
|
+
log_fail "validate-specs-yaml: FAIL"
|
|
600
|
+
FAILED+=("validate-specs-yaml")
|
|
601
|
+
fi
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
# ── CLAUDE.md / CONVENTIONS.md staleness check ────────────────────────────
|
|
605
|
+
STALE_FILES=()
|
|
606
|
+
BIGPOWERS_CLAUDE="$SCRIPT_DIR/../CLAUDE.md"
|
|
607
|
+
if [ -f "$REPO_ROOT/CLAUDE.md" ] && [ -f "$BIGPOWERS_CLAUDE" ]; then
|
|
608
|
+
proj_lines="$(wc -l < "$REPO_ROOT/CLAUDE.md")"
|
|
609
|
+
bp_lines="$(wc -l < "$BIGPOWERS_CLAUDE")"
|
|
610
|
+
delta=$((bp_lines - proj_lines))
|
|
611
|
+
if [ $delta -gt 20 ]; then
|
|
612
|
+
STALE_FILES+=("CLAUDE.md")
|
|
613
|
+
log_warn "CLAUDE.md: $delta lines behind bigpowers template — consider updating"
|
|
614
|
+
fi
|
|
615
|
+
fi
|
|
616
|
+
if [ -f "$REPO_ROOT/CONVENTIONS.md" ] && [ -f "$SCRIPT_DIR/../CONVENTIONS.md" ]; then
|
|
617
|
+
proj_lines="$(wc -l < "$REPO_ROOT/CONVENTIONS.md")"
|
|
618
|
+
bp_lines="$(wc -l < "$SCRIPT_DIR/../CONVENTIONS.md")"
|
|
619
|
+
delta=$((bp_lines - proj_lines))
|
|
620
|
+
if [ $delta -gt 20 ]; then
|
|
621
|
+
STALE_FILES+=("CONVENTIONS.md")
|
|
622
|
+
log_warn "CONVENTIONS.md: $delta lines behind bigpowers template — consider updating"
|
|
623
|
+
fi
|
|
624
|
+
fi
|
|
625
|
+
|
|
626
|
+
# ── generate migration report ─────────────────────────────────────────────
|
|
627
|
+
ISO_DATE="$(date -u +%Y-%m-%dT%H%M%SZ)"
|
|
628
|
+
REPORT_FILE="$SPECS_DIR/migration-report-$ISO_DATE.md"
|
|
629
|
+
|
|
630
|
+
cat > "$REPORT_FILE" <<REPORT
|
|
631
|
+
# Migration Report — $ISO_DATE
|
|
632
|
+
|
|
633
|
+
## Gap Summary
|
|
634
|
+
|
|
635
|
+
| Field | Value |
|
|
636
|
+
|-------|-------|
|
|
637
|
+
| Detected version | $DETECTED |
|
|
638
|
+
| Installed version | $INSTALLED |
|
|
639
|
+
| Migration count | ${MIGRATION_COUNT:-0} |
|
|
640
|
+
|
|
641
|
+
## Migrations Applied
|
|
642
|
+
|
|
643
|
+
REPORT
|
|
644
|
+
|
|
645
|
+
for m in "${SUCCEEDED[@]:-}"; do
|
|
646
|
+
[ -z "$m" ] && continue
|
|
647
|
+
echo "- $m PASS" >> "$REPORT_FILE"
|
|
648
|
+
done
|
|
649
|
+
FAILED_COUNT="${#FAILED[@]:-0}"
|
|
650
|
+
if [ "${FAILED_COUNT}" -gt 0 ]; then
|
|
651
|
+
echo "" >> "$REPORT_FILE"
|
|
652
|
+
echo "## Migrations Failed" >> "$REPORT_FILE"
|
|
653
|
+
for m in "${FAILED[@]}"; do
|
|
654
|
+
echo "- $m FAIL" >> "$REPORT_FILE"
|
|
655
|
+
done
|
|
656
|
+
fi
|
|
657
|
+
if [ ${#SKIPPED[@]} -gt 0 ]; then
|
|
658
|
+
echo "" >> "$REPORT_FILE"
|
|
659
|
+
echo "## Skipped" >> "$REPORT_FILE"
|
|
660
|
+
for m in "${SKIPPED[@]}"; do
|
|
661
|
+
echo "- $m" >> "$REPORT_FILE"
|
|
662
|
+
done
|
|
663
|
+
fi
|
|
664
|
+
echo "" >> "$REPORT_FILE"
|
|
665
|
+
echo "## Uncertainties" >> "$REPORT_FILE"
|
|
666
|
+
echo "WARN count: $UNCERTAINTY_COUNT — review specs/ for GUESS markers and adjust if needed." >> "$REPORT_FILE"
|
|
667
|
+
echo "" >> "$REPORT_FILE"
|
|
668
|
+
echo "## Verification" >> "$REPORT_FILE"
|
|
669
|
+
echo "- validate-specs-yaml: ran as final gate" >> "$REPORT_FILE"
|
|
670
|
+
|
|
671
|
+
if [ ${#STALE_FILES[@]:-0} -gt 0 ]; then
|
|
672
|
+
echo "" >> "$REPORT_FILE"
|
|
673
|
+
echo "## Staleness Notice" >> "$REPORT_FILE"
|
|
674
|
+
for f in "${STALE_FILES[@]:-}"; do
|
|
675
|
+
[ -z "$f" ] && continue
|
|
676
|
+
echo "- $f is behind the current bigpowers template" >> "$REPORT_FILE"
|
|
677
|
+
done
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
echo "" >> "$REPORT_FILE"
|
|
681
|
+
echo "## Next Steps" >> "$REPORT_FILE"
|
|
682
|
+
echo "1. Review the migration report and verify spec correctness" >> "$REPORT_FILE"
|
|
683
|
+
echo "2. Run \`bash scripts/sync-skills.sh\` to regenerate agent artifacts" >> "$REPORT_FILE"
|
|
684
|
+
echo "3. Commit changes if not auto-committed" >> "$REPORT_FILE"
|
|
685
|
+
echo "" >> "$REPORT_FILE"
|
|
686
|
+
echo "## Rollback" >> "$REPORT_FILE"
|
|
687
|
+
echo "To rollback, restore from backup:" >> "$REPORT_FILE"
|
|
688
|
+
echo "\`\`\`bash" >> "$REPORT_FILE"
|
|
689
|
+
echo "rm -rf specs/ && cp -r $BACKUP_DIR/ specs/" >> "$REPORT_FILE"
|
|
690
|
+
echo "\`\`\`" >> "$REPORT_FILE"
|
|
691
|
+
|
|
692
|
+
log_done "Migration report: $REPORT_FILE"
|
|
693
|
+
|
|
694
|
+
# ── terminal summary ──────────────────────────────────────────────────────
|
|
695
|
+
echo ""
|
|
696
|
+
echo "=========================================="
|
|
697
|
+
echo " Migration complete: $DETECTED -> $INSTALLED"
|
|
698
|
+
echo "=========================================="
|
|
699
|
+
echo " Applied: ${#SUCCEEDED[@]:-0}"
|
|
700
|
+
echo " Failed: ${#FAILED[@]:-0}"
|
|
701
|
+
echo " Skipped: ${#SKIPPED[@]:-0}"
|
|
702
|
+
echo " WARN: $UNCERTAINTY_COUNT"
|
|
703
|
+
[ ${#STALE_FILES[@]:-0} -gt 0 ] && echo " Stale: ${STALE_FILES[*]:-}"
|
|
704
|
+
echo " Report: $REPORT_FILE"
|
|
705
|
+
echo "=========================================="
|
|
706
|
+
echo " Remember: run 'bash scripts/sync-skills.sh'"
|
|
707
|
+
echo "=========================================="
|
|
708
|
+
|
|
709
|
+
# ── Safety Layer 3: auto-commit ───────────────────────────────────────────
|
|
710
|
+
if [ "$SKIP_COMMIT" != "true" ] && [ "${MIGRATION_COUNT:-0}" -gt 0 ]; then
|
|
711
|
+
log_info "Auto-committing migration …"
|
|
712
|
+
git -C "$REPO_ROOT" add specs/ 2>/dev/null || true
|
|
713
|
+
COMMIT_MSG="migrate-version: $DETECTED -> $INSTALLED (${MIGRATION_COUNT:-0} migration(s))"
|
|
714
|
+
if git -C "$REPO_ROOT" commit -m "$COMMIT_MSG" 2>/dev/null; then
|
|
715
|
+
log_done "committed: $COMMIT_MSG"
|
|
716
|
+
else
|
|
717
|
+
log_info "nothing to commit (already clean)"
|
|
718
|
+
fi
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
exit 0
|