dwarvesf / dwarvesf/dwarves-kit

install.sh and adopt.sh clobber the kit when it is installed where the README says to put it

Open
#359 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Shell
Stars
11
Forks
2
Avg merge
42m
Merged PRs (30d)
247

Description

Three bugs that all come from the same blind spot: **`install.sh` and `adopt.sh` assume the kit lives somewhere other than where they are writing to.** On the layout README Option 2 documents — `git clone ... ~/.claude/dwarves-kit && cd ... && bash install.sh` — source and destination are the same directory, and on a machine that also has the plugin, `adopt.sh` re-wires hooks the plugin already registered.

All three are silent. Nothing errors, nothing warns, and the kit keeps appearing to work.

| # | Bug | Trigger | Effect |
|---|---|---|---|
| 1 | `kit.toml` truncated to a 12-line stub | in-place clone | whole config layer stops resolving |
| 2 | `AGENTS.md` / `WORKFLOW.md` become self-symlinks | in-place clone + plugin | `adopt.sh` cannot run at all |
| 3 | `adopt.sh` re-registers plugin hooks | plugin installed | every board/session hook fires twice |

PR #358 fixes **1**. Bugs 2 and 3 are reported here, unfixed.

---

## Bug 1 — `install.sh` truncates its own `kit.toml`

`kit_render_install_toml "$KIT_DIR/kit.toml" "$KIT_TOML"` (install.sh L672) is handed the same path twice: `KIT_DIR` **is** `$CLAUDE_DIR/dwarves-kit`. The shell truncates `> "$dst"` before `awk` opens `src`; `echo` writes the header into the now-empty file; `awk` opens the same path, reads that header back, and passes it through its catch-all `{ print }` — which is why the stub carries the header **twice from a single run**.

```bash
SB=$(mktemp -d); mkdir -p "$SB/home/.claude"
git clone -q https://github.com/dwarvesf/dwarves-kit.git "$SB/home/.claude/dwarves-kit"
wc -l "$SB/home/.claude/dwarves-kit/kit.toml" # 160
cd "$SB/home/.claude/dwarves-kit"
env HOME="$SB/home" CLAUDE_DIR="$SB/home/.claude" bash install.sh >/dev/null 2>&1
wc -l "$SB/home/.claude/dwarves-kit/kit.toml" # 12
```

Every non-`[modules]` key stops resolving — `kit_config_get mega.wave_cap` → empty instead of `2`, `gate.understanding_gate` → empty instead of `true`. Nothing errors, because each caller passes its own default.

**Fix (PR #358):** render to `$dst.tmp.$$`, then `mv`. Plus a regression case in `tests/test-install-modules.sh` — every existing case installs *from* the checkout *into* a temp `HOME`, so `src != dst` and the truncation was invisible to CI.

## Bug 2 — compat mode symlinks files onto themselves

`install.sh` L355-356 (plugin-aware compat branch):

```bash
for f in bin lib WORKFLOW.md AGENTS.md docs/WORKFLOW.md docs/impl-playbook; do
ln -sfn "$KIT_DIR/$f" "$CLAUDE_DIR/dwarves-kit/$f"
```

Same `src == dst` situation. For the three **files**, `ln -sfn` replaces each with a symlink pointing at itself. For the three **directories**, `-n` makes `ln` drop the link *inside* the directory instead, leaving `bin/bin`, `lib/lib`, `docs/impl-playbook/impl-playbook`.

```bash
SB=$(mktemp -d)
mkdir -p "$SB/home/.claude/plugins/cache/dwarves-marketplace/kit/2.0.0/lib" # trip compat mode
git clone -q https://github.com/dwarvesf/dwarves-kit.git "$SB/home/.claude/dwarves-kit"
cd "$SB/home/.claude/dwarves-kit"
env HOME="$SB/home" CLAUDE_DIR="$SB/home/.claude" bash install.sh >/dev/null 2>&1

readlink AGENTS.md # -> .../dwarves-kit/AGENTS.md (itself)
ls -d bin/bin lib/lib docs/impl-playbook/impl-playbook
```

The install log reports all six as `[ok] compat symlink ...`, so it reads as success.

Downstream, this is what actually bites — `adopt.sh` resolves `AGENTS.md` through that install and gets nothing:

```
adopt: no source AGENTS.md (looked in ~/.claude/dwarves-kit, ~/.claude/dwarves-kit)
```

`/kit:adopt` is dead on that machine until the checkout is repaired. Recovery, since the files are tracked:

```bash
git -C ~/.claude/dwarves-kit checkout -- AGENTS.md WORKFLOW.md docs/WORKFLOW.md
rm ~/.claude/dwarves-kit/{bin/bin,lib/lib,docs/impl-playbook/impl-playbook}
```

**Suggested fix:** skip the link when it would point at itself — the compat symlinks exist to make `~/.claude/dwarves-kit/` resolve, and in this layout it already does.

```diff
for f in bin lib WORKFLOW.md AGENTS.md docs/WORKFLOW.md docs/impl-playbook; do
+ if [ "$KIT_DIR/$f" -ef "$CLAUDE_DIR/dwarves-kit/$f" ] 2>/dev/null; then
+ echo "[ok] $f already in place (in-place clone); no symlink needed"
+ continue
+ fi
ln -sfn "$KIT_DIR/$f" "$CLAUDE_DIR/dwarves-kit/$f"
```

## Bug 3 — `adopt.sh` is not plugin-aware, so it double-registers hooks

`install.sh` has an explicit compat branch for exactly this hazard, with a comment naming it:

> Running the full bash install here would DOUBLE-register hooks (settings.json AND the plugin) […] which is exactly the "don't run both paths" hazard the README warns about.

`adopt.sh` has no equivalent. Its only mention of the plugin is `KIT_ROOT="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/dwarves-kit}"` (L34) — a source-root lookup, not a dedup check. Step 6 merges hook entries into `/.claude/settings.json` purely from `.kit.toml [modules]`, with no check for what the plugin already provides.

The plugin's `hooks/hooks.json` registers **all 25 hooks globally and unconditionally**. So on a plugin machine, adopting a repo with the default `board = true, session = true` writes 10 more entries covering hooks the plugin already fires:

```
PostToolUse output-offload.sh, post-compact-reinject.sh
PreCompact pre-compact-backup.sh, harvest.sh
SessionEnd backlog-stage.sh, harvest.sh
SessionStart context-readiness.sh
Stop session-state-save.sh, citation-guard.sh
SubagentStop session-state-save.sh
```

All 10 are in the plugin's 25. In the adopted repo each fires twice per event — `harvest` harvesting twice, `session-state-save` writing two snapshots per `Stop`.

Repro: on a machine with `kit@dwarves-marketplace` installed, `bash lib/adopt.sh `, then intersect `/.claude/settings.json` hooks against `plugins/cache/dwarves-marketplace/kit/*/hooks/hooks.json`.

There is also a documentation trap: the honest workaround is `board = false, session = false` in `.kit.toml`, which reads as "disable the board and session modules" when it actually means "let the plugin be the only registrar." Anyone reading that config later will likely flip it back.

**Suggested fix:** give `adopt.sh` the same plugin probe `install.sh` already uses —

```bash
PLUGIN_LIB="$(ls -d "$CLAUDE_DIR"/plugins/cache/dwarves-marketplace/kit/*/lib 2>/dev/null | sort -V | tail -1 || true)"
```

— and when it is non-empty, skip step 6's settings.json merge (still writing every contract file), with a line in the output saying the plugin already provides the hooks.

## Environment

- macOS (Darwin 25.5.0), bash, `kit@dwarves-marketplace` v2.0.0 installed alongside an in-place `~/.claude/dwarves-kit` checkout
- kit `892d25c` (v2.0.0); all three reproduced against `1f7209c`

Contributor guide

Open the contributing guide

Research direction

Start with the compat branch in install.sh and step 6 of lib/adopt.sh, then run the supplied in-place clone and plugin repros. Compare the plugin hook list in hooks/hooks.json with adopted settings and add regression coverage alongside tests/test-install-modules.sh. Done means in-place files and directories remain usable, plugin hooks are not registered twice, and the affected tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
shell
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.