From 0e9782f191fc6745ef3584efc9a2ff2e8ab4464e Mon Sep 17 00:00:00 2001 From: OpenCode Bot Date: Fri, 10 Jul 2026 23:36:07 +0200 Subject: [PATCH 1/2] fix(deploy): only record catalog.hash on successful skill import The boot reimport wrote catalog.hash unconditionally, so a failed import was masked: the DB stayed stale but the hash matched, and every later boot skipped the retry. Now the hash is written only when the import exits 0; a failure logs a WARNING and leaves the hash untouched so the next boot retries. Also log the before->after skill count for visibility into whether the catalog actually changed. Co-Authored-By: Claude Opus 4.8 --- packages/codegraph/entrypoint.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/codegraph/entrypoint.sh b/packages/codegraph/entrypoint.sh index 24fc35d..cc44711 100644 --- a/packages/codegraph/entrypoint.sh +++ b/packages/codegraph/entrypoint.sh @@ -39,13 +39,19 @@ run_import() { fi # Run import (additive — never prunes on boot; use `import-skills --full` manually for that) - node dist/cli.js import-skills "$DATA_DIR" 2>&1 + BEFORE=$(sqlite3 "$DATA_DIR/.codegraph/graph.db" "SELECT COUNT(*) FROM skills;" 2>/dev/null || echo "?") + if node dist/cli.js import-skills "$DATA_DIR" 2>&1; then + AFTER=$(sqlite3 "$DATA_DIR/.codegraph/graph.db" "SELECT COUNT(*) FROM skills;" 2>/dev/null || echo "?") + echo "Skill catalog: ${BEFORE} -> ${AFTER} skills" + # Record the imported bundle hash ONLY on success, so a failed import retries + # on the next boot instead of being permanently masked by a stale hash. + [ -n "$BUNDLE_HASH" ] && printf '%s' "$BUNDLE_HASH" > "$HASH_FILE" + else + echo "WARNING: skill import FAILED (exit $?) — catalog.hash left unchanged so the next boot retries." >&2 + fi # Cleanup symlinks (data stays in SQLite) rm -rf "$DATA_DIR/.opencode" "$DATA_DIR/.agents" - - # Record the imported bundle hash so unchanged redeploys skip the import. - [ -n "$BUNDLE_HASH" ] && printf '%s' "$BUNDLE_HASH" > "$HASH_FILE" } if [ "$SKILL_COUNT" = "0" ] || [ "$SKILL_COUNT" = "" ]; then From 5438d7a2ae5776b5304fc54b6b53b652a2253cf2 Mon Sep 17 00:00:00 2001 From: OpenCode Bot Date: Sat, 11 Jul 2026 11:39:03 +0200 Subject: [PATCH 2/2] fix(import): refuse --full prune when 0 skills are discovered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A misconfigured workspace path (e.g. `import-skills /data` without the .opencode/.agents symlinks entrypoint.sh creates) makes the importer discover 0 skills. With --full that emptied the "discovered" set and deprecated the entire active catalog. Guard it: if nothing was discovered, skip the prune and warn — an empty bundle is a path mistake, not an instruction to wipe the catalog. +1 regression test. Co-Authored-By: Claude Opus 4.8 --- packages/storage/src/import-skills.ts | 8 ++++++- packages/storage/tests/import-skills.test.ts | 25 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/storage/src/import-skills.ts b/packages/storage/src/import-skills.ts index 2af68ba..d35a64c 100644 --- a/packages/storage/src/import-skills.ts +++ b/packages/storage/src/import-skills.ts @@ -368,7 +368,13 @@ export function importSkills( // Optional full-sync: deprecate active skills that vanished from the bundle. let pruned = 0 - if (opts.prune) { + if (opts.prune && skills.length === 0) { + // Safety guard: an empty discovered set almost always means the bundle path + // was wrong (e.g. `import-skills /data` without the .opencode/.agents symlinks + // that entrypoint.sh sets up), NOT an intent to deprecate the entire catalog. + // Refuse to prune — otherwise a misconfigured path silently wipes every skill. + console.warn('[import-skills] --full: 0 skills discovered — skipping prune to avoid deprecating the whole catalog. Check the workspace path.') + } else if (opts.prune) { const discovered = new Set(skills.map((s) => s.name)) const activeNames = (db .prepare(`SELECT name FROM skills WHERE status = 'active' AND category NOT IN ('System','Lifecycle')`) diff --git a/packages/storage/tests/import-skills.test.ts b/packages/storage/tests/import-skills.test.ts index 441f772..a490bd3 100644 --- a/packages/storage/tests/import-skills.test.ts +++ b/packages/storage/tests/import-skills.test.ts @@ -109,4 +109,29 @@ describe('importSkills()', () => { db.close() } }) + + it('--full does NOT prune when the bundle is empty (0 skills discovered)', () => { + const workspace = makeWorkspace() // no .claude/skill — nothing to discover + importSkills(workspace) // creates the DB, imports 0 skills + + // Seed an active catalog skill directly (simulates an existing prod catalog). + const dbPath = path.join(workspace, '.codegraph', 'graph.db') + const seed = new Database(dbPath) + seed.prepare( + `INSERT INTO skills (name, category, description, content, type, tags, lines, updated_at, status) + VALUES ('keepme', 'Backend', 'd', '#', 'domain', '[]', 1, ?, 'active')`, + ).run(new Date().toISOString()) + seed.close() + + // --full against an empty bundle must NOT deprecate the existing catalog. + const result = importSkills(workspace, { prune: true }) + expect(result.pruned).toBe(0) + + const db = new Database(dbPath) + try { + expect((db.prepare("SELECT status AS s FROM skills WHERE name = 'keepme'").get() as { s: string }).s).toBe('active') + } finally { + db.close() + } + }) })