Prevent partial jangbu wrapper installs on collisions

Promoted upstream jangbu skills now preflight every Claude and agents top-level destination before the installer mutates home skill directories. This keeps unrelated user-authored skills from causing a mixed partial discovery state, while preserving the existing managed-marker overwrite path and the explicit override escape hatch.

The installer also prints the namespace re-sync warning next to the upstream runtime install command so users know to restore wrapper-managed top-level skills after running upstream's Claude-only installer.

Constraint: Upstream skill contents must be checked out before collision preflight can validate promoted SKILL.md files.

Rejected: Roll back cache checkout on collision | cache writes are outside the advertised home skill discovery namespace and are needed to inspect pinned upstream content.

Confidence: high

Scope-risk: narrow

Directive: Keep promoted-skill collision checks before install_wrapper_payload and sync_dir calls for home skill roots.

Tested: bash -n korean-jangbu-for/scripts/install.sh

Tested: node --test scripts/skill-docs.test.js --test-name-pattern='korean-jangbu-for'

Tested: temp HOME real pinned upstream install and .agents jangbu-tax collision preflight smoke

Tested: npm run ci
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-04-29 01:09:11 +09:00
commit c934b4076b
2 changed files with 59 additions and 5 deletions

View file

@ -91,9 +91,8 @@ is_managed_promoted_skill() {
[[ -f "${skill_file}" ]] && grep -q "${MANAGED_MARKER}" "${skill_file}"
}
sync_promoted_skill() {
local source_dir="$1"
local target_dir="$2"
assert_promoted_skill_writable() {
local target_dir="$1"
if [[ -e "${target_dir}" || -L "${target_dir}" ]]; then
if [[ "${KOREAN_JANGBU_FOR_OVERWRITE_SKILLS:-}" != "1" ]] && ! is_managed_promoted_skill "${target_dir}"; then
@ -102,10 +101,46 @@ sync_promoted_skill() {
exit 1
fi
fi
}
sync_promoted_skill() {
local source_dir="$1"
local target_dir="$2"
assert_promoted_skill_writable "${target_dir}"
sync_dir "${source_dir}" "${target_dir}"
}
preflight_promoted_skill() {
local source_dir="$1"
local target_dir="$2"
if [[ ! -f "${source_dir}/SKILL.md" ]]; then
echo "[korean-jangbu-for] missing upstream skill: ${source_dir}/SKILL.md" >&2
exit 1
fi
assert_promoted_skill_writable "${target_dir}"
}
preflight_promoted_skills() {
local home_skill_dir
local home_skills_root
local upstream_skill
local upstream_skill_dir
local home_upstream_skill_dir
for home_skill_dir in "${HOME_DIRS[@]}"; do
home_skills_root="$(dirname "${home_skill_dir}")"
for upstream_skill in "${UPSTREAM_SUBSKILLS[@]}"; do
upstream_skill_dir="${CLONE_DIR}/skills/${upstream_skill}"
home_upstream_skill_dir="${home_skills_root}/${upstream_skill}"
preflight_promoted_skill "${upstream_skill_dir}" "${home_upstream_skill_dir}"
done
done
}
append_response_policy() {
local skill_file="$1"
@ -152,6 +187,8 @@ HOME_DIRS=(
"${HOME}/.agents/skills/${SKILL_NAME}"
)
preflight_promoted_skills
for HOME_SKILL_DIR in "${HOME_DIRS[@]}"; do
HOME_UPSTREAM="${HOME_SKILL_DIR}/upstream"
HOME_SKILLS_ROOT="$(dirname "${HOME_SKILL_DIR}")"
@ -193,6 +230,7 @@ echo " pinned upstream SHA: ${UPSTREAM_SHA}"
echo " upstream repo: ${UPSTREAM_REPO}"
echo " runtime install: bash ~/.claude/skills/korean-jangbu-for/upstream/scripts/install.sh"
echo " verify command: bash ~/.claude/skills/korean-jangbu-for/upstream/scripts/verify.sh"
echo " namespace note: Re-run this wrapper installer after upstream runtime install to restore wrapper-managed top-level skills."
echo " subskills: /korean-jangbu-for /jangbu-connect /jangbu-import /jangbu-tag /jangbu-tax /jangbu-dash /jangbu-jongso"
echo " 원저작자: @kimlawtech (SpeciAI) — 응답마다 원본 링크와 함께 언급해야 한다."
echo " 생성물은 참고용 초안이며 공식 회계감사·세무신고를 대체하지 않는다."

View file

@ -2995,6 +2995,7 @@ test("korean-jangbu-for ships an install.sh wrapper and a pinned upstream SHA",
assert.match(install, /git clone --filter=blob:none/);
assert.match(install, /upstream\.pin/);
assert.match(install, /verify\.sh/);
assert.match(install, /Re-run this wrapper installer after upstream runtime install/);
const stat = fs.statSync(installPath);
assert.ok((stat.mode & 0o111) !== 0, "install.sh must be executable");
@ -3115,7 +3116,7 @@ test("korean-jangbu-for installer registers upstream subskills for Claude and ag
}
});
test("korean-jangbu-for installer refuses to overwrite unrelated promoted subskills", () => {
test("korean-jangbu-for installer preflights promoted subskill collisions before home writes", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "korean-jangbu-for-collision-"));
const homeDir = path.join(tmpDir, "home");
const upstreamDir = path.join(tmpDir, "upstream");
@ -3147,7 +3148,7 @@ test("korean-jangbu-for installer refuses to overwrite unrelated promoted subski
childProcess.execFileSync("git", ["commit", "-m", "seed upstream skills"], { cwd: upstreamDir, stdio: "ignore" });
const upstreamSha = childProcess.execFileSync("git", ["rev-parse", "HEAD"], { cwd: upstreamDir, encoding: "utf8" }).trim();
const unrelatedSkillDir = path.join(homeDir, ".claude", "skills", "jangbu-tax");
const unrelatedSkillDir = path.join(homeDir, ".agents", "skills", "jangbu-tax");
fs.mkdirSync(unrelatedSkillDir, { recursive: true });
fs.writeFileSync(
path.join(unrelatedSkillDir, "SKILL.md"),
@ -3174,6 +3175,21 @@ test("korean-jangbu-for installer refuses to overwrite unrelated promoted subski
/user-authored jangbu-tax/,
"unrelated existing subskill should be preserved after installer refusal",
);
for (const root of [".claude", ".agents"]) {
const skillRoot = path.join(homeDir, root, "skills");
assert.ok(
!fs.existsSync(path.join(skillRoot, "korean-jangbu-for")),
`${root} should not create the wrapper directory after a promoted-subskill preflight failure`,
);
for (const skillName of upstreamSubskills.filter((name) => name !== "jangbu-tax")) {
assert.ok(
!fs.existsSync(path.join(skillRoot, skillName)),
`${root} should not create promoted subskill ${skillName} after a promoted-subskill preflight failure`,
);
}
}
});
test("korean-jangbu-for feature doc documents source-first use and mandatory attribution", () => {