dwarvesf / dwarvesf/dwarves-kit
install.sh and adopt.sh clobber the kit when it is installed where the README says to put it
- 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
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