diff --git a/README.md b/README.md index 0fc4c36c..20560b96 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,9 @@ Canonical user memory is the first existing file in this order: `~/.agents/AGENT `$CLAUDE_CONFIG_DIR/CLAUDE.md` (default `~/.claude/CLAUDE.md`), and `$CODEX_HOME/AGENTS.md` (default `~/.codex/AGENTS.md`). User-level skill extractions default to `~/.agents/skills`, with a warning if Claude's active `skills` path is a -real directory rather than the usual symlink. See [Configuration](#configuration) for -using an existing harness-loaded directory instead. +real directory rather than the usual symlink. Backpass leaves that directory untouched: +see [Configuration](#configuration) for the scope-specific settings and manual merge +guidance. In user scope every `add`, `rewrite`, or `remove` edit also clears `minGapProjects` (default `1`): the distinct projects behind its own quotes, counted from the gap @@ -735,8 +736,13 @@ regular settings; its path and user-only settings include `memoryFiles`, `skills `skillsDirs`, `minGapProjects` (default `1`), and these discovery controls: `skillsDir` defaults to `.agents/skills`. To use an existing harness-loaded directory -instead, such as `.claude/skills`, configure that path; a missing configured directory -falls back to the default. Backpass normalizes path separators and trailing slashes. +instead, such as `.claude/skills`, configure that path: use `--skills-dir ` for one +run, `skillsDir` in `.backpassrc.json` for project scope, or `skillsDir` under the `user` +block in `$XDG_CONFIG_HOME/backpass/config.json` (default `~/.config/backpass/config.json`) +for user scope. A missing configured directory falls back to the default. Backpass leaves +a real `.claude/skills` directory untouched and recommends manually merging new skill +directories after checking for conflicts. It never recommends replacing that directory +with a symlink. Backpass normalizes path separators and trailing slashes. ```json { diff --git a/src/skills.js b/src/skills.js index c458c34d..f7be3d65 100644 --- a/src/skills.js +++ b/src/skills.js @@ -385,11 +385,18 @@ function invalidSkillsDir(configuredDir) { function claudeSkillsDirWarning(claudeSkillsDir = CLAUDE_SKILLS_LINK, target = CLAUDE_SKILLS_LINK_TARGET) { return ( `${claudeSkillsDir} is a real directory, not a symlink to ${target}; ` + - `left untouched. Claude will not see skills written to ${CANONICAL_SKILLS_DIR} until you ` + - `merge it in and replace it with the symlink (ln -s ${target} ${claudeSkillsDir}).` + `left untouched. Backpass writes new skills to ${CANONICAL_SKILLS_DIR}, which Claude will not load ` + + `through ${claudeSkillsDir}. To write directly to the existing directory, use ` + + `--skills-dir ${shellQuote(claudeSkillsDir)} for one run, set "skillsDir": "${claudeSkillsDir}" in .backpassrc.json ` + + `for project scope, or set it under the "user" block in $XDG_CONFIG_HOME/backpass/config.json for user ` + + `scope. Otherwise merge new skill directories manually after checking for conflicts.` ); } +function shellQuote(value) { + return `'${value.replaceAll("'", "'\\''")}'`; +} + function inspectClaudeSkillsLink(repoRoot, claudeSkillsDir = CLAUDE_SKILLS_LINK) { const link = path.isAbsolute(claudeSkillsDir) ? claudeSkillsDir : path.join(repoRoot, claudeSkillsDir); let stat; diff --git a/test/skills.test.js b/test/skills.test.js index e5a9e407..2fa962a1 100644 --- a/test/skills.test.js +++ b/test/skills.test.js @@ -229,6 +229,11 @@ test("a real .claude/skills directory is warned about and never clobbered", () = assert.equal(resolved.dir, CANONICAL_SKILLS_DIR); assert.equal(resolved.warnings.length, 1); assert.match(resolved.warnings[0], /\.claude\/skills is a real directory/); + assert.match(resolved.warnings[0], /--skills-dir '\.claude\/skills'/); + assert.match(resolved.warnings[0], /"skillsDir": "\.claude\/skills"/); + assert.match(resolved.warnings[0], /"user" block in \$XDG_CONFIG_HOME\/backpass\/config\.json/); + assert.match(resolved.warnings[0], /merge new skill directories manually/); + assert.doesNotMatch(resolved.warnings[0], /ln -s|replace it/); const result = writeSkill(root, SKILL); assert.equal(result.warnings.length, 1); @@ -240,6 +245,21 @@ test("a real .claude/skills directory is warned about and never clobbered", () = assert.ok(fs.existsSync(path.join(root, CANONICAL_SKILLS_DIR, "release-signing", "SKILL.md"))); }); +test("quotes spaces in the one-run skills directory guidance", () => { + const root = tmpRepo(); + const claudeSkillsDir = ".claude config/skills"; + fs.mkdirSync(path.join(root, claudeSkillsDir), { recursive: true }); + + const resolved = resolveOverflowTarget(root, CANONICAL_SKILLS_DIR, { claudeSkillsDir }); + + assert.equal(resolved.warnings.length, 1); + assert.match(resolved.warnings[0], /--skills-dir '\.claude config\/skills'/); + assert.match(resolved.warnings[0], /project scope/); + assert.match(resolved.warnings[0], /user.*scope/); + assert.match(resolved.warnings[0], /merge new skill directories manually after checking for conflicts/); + assert.doesNotMatch(resolved.warnings[0], /ln -s|replace it/); +}); + test("applyDecisions writes accepted extractions through the skills layout and surfaces warnings", () => { const root = tmpRepo(); fs.writeFileSync(path.join(root, "AGENTS.md"), "# Memory\n\n- Sign releases with the key.\n");