github dyoshikawa/rulesync v16.21.0

latest releases: v39.0.0, v38.0.0, v37.1.0...
one month ago

What's Changed

New Features

  • A root language key steers the language the AI answers in. rulesync.jsonc (and rulesync.local.jsonc, which overrides it per developer) accepts language with one of en, ja, zh-CN, zh-TW, ko, fr, de, es, pt-BR, ru. Claude Code has a native setting, so for claudecode / claudecode-legacy the rules feature writes "language": "japanese" (and so on) into .claude/settings.local.json (project scope) or ~/.claude/settings.json (global scope) and touches nothing else in the file, while CLAUDE.md is left as it is. Every other target gets the instruction simulated as one block appended after a thematic break to the generated root rule file only — nested rules never carry it. rulesync import recognizes that block for every supported language and strips it (with a warning, since the language key is what keeps the instruction), so an import-then-generate loop yields exactly one block, and rulesync convert re-adds it to the destination's root file. When the key is omitted Rulesync says nothing about language, so en is an explicit instruction rather than the default, and an unset key never creates or modifies the Claude Code settings file (#2889, closes #2863).
  • agentsmd.subprojectPath can be derived from a rule's globs. A new rulesync.jsonc option deriveSubprojectPathFromGlobs (default false, also honored from rulesync.local.jsonc) places each non-root rule as a nested AGENTS.md under the directory its globs already point at (["packages/api/**/*"] becomes packages/api) for the targets that nest, so the directory no longer has to be repeated by hand. A single rule opts in with agentsmd: { subprojectPath: "auto" } regardless of the option, or opts out with agentsmd: { subprojectPath: "" } when the option is on; an explicit directory always wins and root: true rules never nest. Only an explicit "auto" that cannot be honored (no shared wildcard-free prefix, disagreeing globs, negation, .., an absolute path, or a root rule) warns, once per rule; config-driven derivation falls back silently so ordinary activation globs such as ["src/**/*.ts", "test/**/*.ts"] stay quiet. The MCP rule operations return the authored frontmatter, so a get, edit and put round trip keeps "auto" in the file (#2884, closes #2864).
  • Generated Claude Code settings files carry $schema. Every generated .claude/settings.json and .claude/settings.local.json, in project and global scope, now starts with "$schema": "https://json.schemastore.org/claude-code-settings.json" whichever feature (permissions, ignore, or hooks) writes the file, so editors validate and autocomplete it. The key is only ensured, never forced: a $schema the file already states is left untouched, whatever its value, and non-Claude shared files are unaffected (#2883, closes #2862).
  • Skill frontmatter accepts root-level license, compatibility and metadata. In .rulesync/skills/<name>/SKILL.md these three fields can now be written once at the root instead of repeated per tool section. A root value applies to every target that models the field, and a tool section that sets the same key overrides it outright for that target only, the same precedence rule as disable-model-invocation / user-invocable. The Agent Skills writers (agentsskills, hermesagent, agentsmd) apply their usual normalization to root values (a compatibility object becomes a string, metadata values are stringified), Copilot and Copilot CLI take only license, Cursor takes only metadata, and Kilo now accepts compatibility as a string or an object like every other Agent Skills target (#2882, closes #2874).
  • A shared JSONC file that falls back to a whole-document rewrite is reported. v16.20.0 made generate edit .vscode/settings.json, .amp/settings.json, opencode.json(c), kilo.json(c) and their kin in place, silently rewriting the whole document only when there is nothing to edit against. That rewrite now emits one warning per file per run naming the file and the reason: the file could not be parsed, its root is not an object, a key is stated twice, a prototype-pollution key is present, the write or edit budget was exceeded, or the editor threw. An empty file, the in-place edit path and the non-JSONC formats stay silent, so a comment that disappears is no longer a mystery (#2895, closes #2858).

Bug Fixes

  • The skill picker counts East Asian Ambiguous characters as two columns. The width measurement behind the fetch skill picker treated the UAX #11 Ambiguous class (box drawing, Greek, Cyrillic, accented Latin-1, the em dash, the ellipsis) at one column, which is what a Latin terminal draws, while a terminal set to draw the class wide, common in CJK locales, draws it at two. A skill name padded with box-drawing characters therefore passed the hidden-character check, fit the label budget, and was hard-wrapped by the terminal into a forged row with no pointer and no checkbox. The class is now counted at two columns, so such names are shortened before they can wrap; the cost is that names carrying Greek, Cyrillic or accented letters are cut a little earlier on Latin terminals (#2890, closes #2845).
  • deepagents no longer creates an empty global config.toml. When no permission rule mapped to the deepagents allowlist, a fresh ~/.deepagents/config.toml holding a lone newline was created on a machine that had none. The file is deepagents' own: Rulesync merges into it when it exists and now has no reason to bring it into existence to hold nothing. An existing file is still rewritten as before, so no user content is dropped (#2891, closes #2859).
  • fetch drops skill names that Windows folds or aliases. A skill name with a trailing dot or space (deploy., deploy ) is resolved by Win32 to the plain name, and a NAME~<digits> name (DEPLOY~1) is an 8.3 short-name alias that opens whatever long name it stands for, so under the default --conflict overwrite a remote deploy. could replace the files of a local deploy skill the repository never published. Such names are now left out of the interactive picker, rejected by --skills as unknown, never written, and reported through a warning that quotes each name as it is so a trailing space shows. Names such as data~2parser and v1.0 are still fetched (#2888, closes #2848).
  • OpenCode skills.paths entries are judged by where they resolve. import filtered the skills.paths entries of opencode.json(c) only by their spelling, so a relative entry naming a symbolic link that points outside the config directory passed the check and its SKILL.md files were read. Each entry is now also resolved and dropped when it escapes the config directory, the same rule the nested Claude Code skills roots already follow; a link that stays inside is kept, and a non-existent entry is still accepted as before (#2887, closes #2850).
  • Nested .claude/skills roots recognize a symlinked project root. The nested-root scan for Claude Code skills is now seeded with both the literal .claude/skills and where it resolves to, so a nested root that reaches the project's own symlinked skills directory is recognized as the tool's own root rather than read a second time. A root reached through a link is deliberately still recorded under the spelling the scan reported, since that is the path that is read and that the skill's paths are scoped to; this behavior is now pinned and documented, alongside two clarity cleanups in the path helpers (#2885, closes #2852).
  • The skill picker's label budget honors CLI_WIDTH. The inquirer renderer measures its width through cli-width, which skips a zero-column TTY in favor of the CLI_WIDTH environment variable before falling back to 80, while the label budget read the terminal width directly. A TTY reporting zero columns under a CLI_WIDTH narrower than 77 therefore had its labels cut to 72 while the rows were broken at CLI_WIDTH, letting a label wrap onto the second line the cap exists to prevent. The budget now parses CLI_WIDTH the way the renderer does when the terminal reports no width (#2886, closes #2847).
  • Executable bits on skill scripts survive generate and fetch. A skill's supporting files were copied byte for byte but written as plain buffers, so a script that was executable in .rulesync/skills/<name>/scripts/ lost its executable bit in every generated skill directory; fetch had the same gap because the GitHub contents API carries no file mode. generate now records and applies the mode of any executable supporting file, and restores a stripped bit even when the directory is otherwise unchanged. fetch reads the repository tree once and marks every path git records as 100755 executable; if the tree is truncated or unreadable the files are still fetched and a warning says the bit was not applied, so the loss is never silent. Nothing is applied in dry-run mode, and both paths are no-ops on Windows (#2880, closes #2866).
  • generate --delete prunes stale files inside generated skill directories. The orphan sweep removed a whole skill directory that no longer had a source and the flat files of flattening tools, but never looked inside a directory whose skill still existed. Deleting references/old.md from .rulesync/skills/<name>/ while keeping the skill therefore left the generated copy in place forever, and generate --check reported the tree as up to date. A third sweep now removes files that no source produced from every directory Rulesync owns outright and generated in this run, and --check reports them and exits 1. Hidden entries such as .gitkeep and symbolic links are left alone because Rulesync never writes them, generated names are matched case-insensitively so a case-only rename does not delete the file it just wrote, and the sweep stands down when the source read was incomplete (#2876, closes #2867, closes #2739).
  • Cursor import no longer invents globs on Always Apply rules. rulesync import --targets cursor rewrote an Always Apply .mdc rule with an empty or missing globs into globs: ["**/*"] on both the canonical globs and cursor.globs, and generate wrote that glob back out next to alwaysApply: true, a pairing Cursor's documentation says is ignored and that some versions read as an Agent Requested rule. The canonical globs still becomes ["**/*"] so tools with no Always Apply equivalent keep applying the rule everywhere, but cursor.globs now mirrors the source file and becomes an explicit [] when it carried no patterns, so the regenerated .mdc goes back out without a globs line. After upgrading, regenerate (or drop a stale cursor.globs) to clear rules imported earlier (#2875, closes #2868).
  • Amp settings are sanitized before both the permissions and MCP adapters read them. .amp/settings.json is now parsed through the strict, sanitizing JSONC reader that rebuilds every object from its own entries, so __proto__, constructor and prototype keys are removed recursively before the document is inspected or re-serialized. The permissions adapter got this first; the MCP adapter still held a near-identical copy of the old parser, through which a root __proto__ could inject an amp.mcpServers entry that appears nowhere in the file and have it imported into generated output. Both adapters now share one parser, comments and trailing commas are still preserved, and the same malformed syntax is still rejected with the Amp-specific error context (#2860, closes #2855; #2873, closes #2869).
  • Every scan runs from a literal root, so a project path with glob metacharacters is read as a directory. The orphan sweeps and the input scans embedded the project root into their glob patterns, so a directory named project(glob), project{a,b} or project*x was read as a pattern: files were skipped, or picked up from a sibling directory. The input side was the dangerous one: with proj(x)/.rulesync/rules/a.md present the rule scan returned nothing, which read as "every rule was deleted", so generate --delete removed generated files it could no longer regenerate and still reported success. The subagents, commands and checks sweeps were converted first, and the rules, commands, skills and nested .claude/skills scans now follow, each passing the root as a literal working directory with a root-relative pattern so a tool cannot reintroduce the bug by spelling the root into its own pattern (#2811; #2872, closes #2870).

Other Changes

  • Every log line that quotes a remote-derived name now goes through one quoteForLog helper that strips control characters before JSON-quoting, replacing 45 hand-spelled copies; no log wording changes (#2893, closes #2842).
  • A goal-issues-and-release project skill was added under .rulesync/skills/ for this repository's own unattended maintenance runs; it is not a distributed skill (#2877).
  • An official rulesync-mcp skill was added and then reverted within this release, so nothing distributed changes (#2871, #2879).
  • Dependencies were bumped as a group (107 updates including @inquirer/checkbox, fastmcp, js-yaml, @anthropic-ai/claude-agent-sdk, cspell, oxfmt, oxlint, secretlint), along with two GitHub Actions (#2809, #2808).
  • The Homebrew formula was updated to v16.20.0 (#2865).

Contributors

Full Changelog

v16.20.0...v16.21.0

Don't miss a new rulesync release

NewReleases is sending notifications on new releases.