Skip to content

feat(commands): add flattenedCommandNaming option for collision-free flattened command names#2314

Merged
dyoshikawa merged 1 commit into
dyoshikawa:mainfrom
Millon15:feat/flattened-command-naming
Jul 20, 2026
Merged

feat(commands): add flattenedCommandNaming option for collision-free flattened command names#2314
dyoshikawa merged 1 commit into
dyoshikawa:mainfrom
Millon15:feat/flattened-command-naming

Conversation

@Millon15

Copy link
Copy Markdown
Contributor

Problem

For tools with supportsSubdirectory: false (Cursor, Antigravity IDE, Codex CLI, …), nested commands are flattened to their basename (introduced in #1108). When two nested commands share a basename, they map to the same output file and the last one processed silently wins (with a warning since the collision-warn was added).

This is easy to hit in real repos that namespace commands by category. Our project hit it with 12 of 45 nested commands, e.g.:

  • plan/run.md, qa/run.md, ralphex/run.md → all run.md
  • update/docs.md, update/confluence/docs.md, update/google/docs.md, update/slack/docs.md → all docs.md

The warning helps, but there is currently no way to get all nested commands generated for flat-namespace tools — some are always lost.

Solution

Add an opt-in, config-file-only option flattenedCommandNaming:

Value Behavior
"basename" (default) Current behavior, unchanged: pj/test.mdtest.md
"path" Join directory segments into the filename: pj/test.mdpj-test.md, update/confluence/docs.mdupdate-confluence-docs.md
  • Tools with supportsSubdirectory: true (Claude Code, OpenCode, Roo, …) are unaffected — nested paths are preserved as before.
  • The existing collision warning still fires if joined names ever collide (e.g. a literal pj-test.md next to pj/test.md).
  • Config-file-only (like sources), no CLI flag — it is a project-level generation policy, mirroring how ConfigResolverResolveParams already excludes sources.

Changes

  • src/types/features.tsFlattenedCommandNamingSchema ("basename" | "path")
  • src/config/config.ts, src/config/config-resolver.ts — schema field, defaults, local-config merge, getter
  • src/features/commands/commands-processor.ts — naming strategy in flattenRelativeFilePath
  • src/lib/generate.ts — pass the option through
  • docs/guide/configuration.md + skills/rulesync/configuration.md — option documented (kept in sync)
  • Tests: 3 new cases in commands-processor.test.ts (path naming, no-collision assertion, subdirectory tools unaffected); Config mocks extended in the two generate test suites

Test Plan

  • pnpm vitest run src/features/commands/commands-processor.test.ts — 47/47
  • pnpm test — 323 files, 7270 tests passed
  • pnpm run check — oxfmt, oxlint, tsgo all clean; pnpm cspell clean
  • Real-world check: applied the same change as a patch to rulesync 12.0.0 in our monorepo — 45 nested commands now generate 59 unique files per flat-namespace target with zero collisions, generate --check idempotent

🤖 Generated with Claude Code

…flattened command names

For tools without subdirectory command support (supportsSubdirectory:
false, e.g. Cursor, Antigravity IDE), nested commands are flattened to
their basename, so pj/test.md and ops/test.md both map to test.md and
the last one processed silently wins (a warning is logged).

Add an opt-in, config-file-only option flattenedCommandNaming:
- "basename" (default): current behavior, unchanged
- "path": join directory segments into the filename
  (pj/test.md -> pj-test.md), keeping flattened names unique

Tools that support subdirectories (e.g. Claude Code, OpenCode) are
unaffected by the option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 16:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in configuration option to control how nested command paths are flattened for tools that don’t support subdirectories, enabling collision-free generation by deriving filenames from the full command path.

Changes:

  • Introduces flattenedCommandNaming config option ("basename" default, "path" join strategy).
  • Threads the option through config resolution and generate into CommandsProcessor’s flattening logic.
  • Adds/updates tests and documentation for the new option.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/types/features.ts Adds FlattenedCommandNamingSchema and type for the new option.
src/config/config.ts Extends config schema/state with flattenedCommandNaming and a getter.
src/config/config-resolver.ts Adds defaults/merge behavior and enforces config-file-only resolution.
src/features/commands/commands-processor.ts Implements "basename" vs "path" flattening strategy when subdirectories aren’t supported.
src/lib/generate.ts Passes flattenedCommandNaming from Config into CommandsProcessor.
src/lib/generate.test.ts Updates config mocks to include getFlattenedCommandNaming.
src/cli/commands/generate.test.ts Updates config mocks to include getFlattenedCommandNaming.
src/features/commands/commands-processor.test.ts Adds tests covering path-join naming and subdirectory-tool behavior.
docs/guide/configuration.md Documents the new flattenedCommandNaming option.
skills/rulesync/configuration.md Documents the new flattenedCommandNaming option (synced copy).

Comment thread src/types/features.ts
// Naming strategy for command files flattened for tools without subdirectory
// support: "basename" keeps only the filename (`pj/test.md` -> `test.md`,
// colliding paths overwrite each other), "path" joins the directory segments
// into the filename (`pj/test.md` -> `pj-test.md`), keeping names unique.
Comment on lines +57 to +58
// (`pj/test.md` -> `pj-test.md`), keeping flattened names unique.
// Tools that support subdirectories (e.g. Claude Code) are unaffected.
Comment on lines +57 to +58
// (`pj/test.md` -> `pj-test.md`), keeping flattened names unique.
// Tools that support subdirectories (e.g. Claude Code) are unaffected.
expect(calledArgs.rulesyncCommand.getRelativeFilePath()).toBe("update-confluence-docs.md");
});

it('should not warn about collisions when flattenedCommandNaming is "path" keeps names unique', async () => {
@dyoshikawa
dyoshikawa merged commit f691730 into dyoshikawa:main Jul 20, 2026
6 checks passed
@dyoshikawa

Copy link
Copy Markdown
Owner

@Millon15 Thank you!

shaneholloman pushed a commit to shaneholloman/rulesync that referenced this pull request Jul 20, 2026
…ow-ups

Resolves the concrete follow-ups from the PR dyoshikawa#2314 review (dyoshikawa#2323):

- wire config.getFlattenedCommandNaming() into the CommandsProcessor
  constructed by the convert commands strategy, with a regression test
  (previously the configured "path" naming was silently ignored by
  rulesync convert and fell back to "basename")
- soften the uniqueness wording for "path" naming in
  src/types/features.ts and docs (collisions are reduced, not ruled
  out; the collision warning still applies) and add a migration note
  about stale flat-named files and delete: true
- add config-resolution tests: file value reaches Config, local config
  override, default, and invalid-value rejection
- use toPosixPath() instead of a hand-rolled separator regex for the
  path flattening, per the coding guidelines

Item 6 of the issue (top-level key vs featureOptions placement) is a
design decision deliberately split out to dyoshikawa#2325.

Closes dyoshikawa#2323

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants