Skip to content

AIS 1.0: explicit commands and predictable global workflows - #46

Open
lbb00 wants to merge 18 commits into
mainfrom
feat/cli-1.0-redesign
Open

lbb00 wants to merge 18 commits into
mainfrom
feat/cli-1.0-redesign

Conversation

@lbb00

@lbb00 lbb00 commented Aug 18, 2026 •

Copy link
Copy Markdown
Owner

Summary

This completes the AIS 1.0 command redesign and hardens the global asset workflow around it.

  • Commands are explicit (ais <tool> <subtype> ...) and shared asset types support broadcast operations across selected tools.
  • Named profiles can replace repeated tool lists, for example ais skills add review --profile personal --global.
  • Global installs report their real targets, warn when the source is not reproducible from its remote, and remind active agent sessions to restart.
  • AIS_CONFIG_HOME and XDG_CONFIG_HOME isolate global state; newer config schemas fail explicitly, while status reports unknown tools/subtypes and minimum-version requirements without deleting them.
  • env, target-aware status, asset-centric search, remote-aware doctor, and non-destructive repository remove/prune make the active executable, config, source, and symlink state inspectable.

The complete breaking-command migration and compatibility notes remain in .changeset/wise-lions-decide.md.

Review notes

Repository cleanup only removes AIS config records; it never deletes a local repository. Remote checks are opt-in for doctor, while add commands only emit a warning for unpublished or dirty sources so local skill development remains supported.

Verification

  • pnpm run build
  • pnpm exec vitest run — 565 passed, 0 failed/skipped
  • Test run left the real ~/.config/ai-rules-sync config hash unchanged
  • pnpm run docs:build
  • Isolated CLI flow: profile-based Claude/Pi/Codex global install, exact target output, three healthy symlinks, remote recoverability failure, schema rejection, and non-destructive repository prune
  • pnpm run quality — unavailable in the current checkout because pawl is not installed or declared (pawl: command not found)

🤖 Generated with Claude Code

lbb00 and others added 18 commits August 17, 2026 19:27
Persists the agreed command-surface redesign (explicit tool/subtype
dispatch + broadcast groups, sourceDir wildcard, breaking 1.0.0) with
user personas and journeys, as the implementation reference.
resolveSourceDir() exposes which precedence tier (override/explicit/
wildcard/default) resolved a tool's source directory, needed for
broadcast-command dry-run output; getSourceDir() becomes a thin
wrapper over it. cli-groups.ts centralizes the CLI-group-name-to-
adapter-tool mapping and defines the broadcast subtype groups
(skills/agents/rules/commands/md/prompts) used by upcoming top-level
commands.
registerToolGroup() generates ais <tool>'s full command set (subtype
subgroups, install/add-all/import, hidden add/remove stubs) from
adapterRegistry instead of ~30 hand-written, drifting blocks. Also
fixes import's subtype auto-detection to match on adapter.mode/
fileSuffixes instead of bare fs.pathExists, which mis-detected nested
targetDirs (e.g. cline-commands inside cline-rules' directory). As a
side effect, the five adapters that had no real CLI command
(cline-commands, cline-agents, windsurf-commands, opencode-rules,
codex-agents) are now fully wired.

Flat `<tool> add`/`<tool> remove` no longer guess a default subtype;
they're hidden stubs that redirect to the explicit subtype command.
…stry

TOOL_CLI_GROUPS/UNWIRED_ADAPTER_NAMES existed only to describe how the
old hand-written per-tool command blocks diverged from what
adapterRegistry knew about; registerToolGroup removed that divergence,
so the hand-maintained table is gone too. Completion now includes the
five adapters that previously had no real CLI command, and no longer
suggests the removed flat <tool> add/remove shortcuts.
buildTemplateSourceDirConfig used to write one explicit sourceDir line
per adapter, so a fresh repo started as a 29-line hand-enumerated map
even though the wildcard resolver has supported "*" fallback all
along. Subtypes whose content is tool-agnostic (skills/agents/rules/
commands/prompts) now collapse into a single "*" entry when ≥2 tools
share them; per-tool file subtypes (md, instructions, workflows, ...)
stay explicit since their content differs by tool. createDirs follows
the generated config instead of enumerating adapters separately.
Adds add/remove/list for each BROADCAST_GROUPS entry (skills, agents,
rules, commands, md, prompts), so one skill can be pushed to several
tools in a single command instead of repeating <tool> skills add per
tool. --tools/--all is mandatory (no default-to-everything), unknown
tool names get a did-you-mean suggestion, and a tool with no adapter
in the group but a known equivalent concept (e.g. copilot's
"instructions" for the rules group) gets pointed at the exact
replacement command via CONCEPT_HINTS. --dry-run's origin column
surfaces which sourceDir precedence tier resolved each tool's source
directory (resolveSourceDir), and --strict turns "repo has nothing at
that path" from a warning into a failure.
BREAKING CHANGE: top-level `ais add`/`ais remove` (guessed-tool
dispatch) and `ais user install` are removed with no replacement
shim. Use the explicit `ais <tool> <subtype> add/remove` or the
`ais <subtype> add/remove --tools <list>` broadcast commands instead;
`ais user install` is now `ais install -g`. `status`'s JSON output no
longer has a `project.inferredMode` field.

Deletes the inference layer that caused most of the silent-failure
bugs fixed in the registry-driven dispatch work: inferDefaultMode,
requireExplicitMode, resolveSingleAdapterForMode,
getToolsForInstallMode, TOOLS_WITH_DEFAULT_ADD_SUBTYPE,
getRegisteredTools, resolveCopilotAliasFromConfig, and
findAdapterForAlias (plus its private helpers and dedicated tests) —
all dead once the commands that called them are gone.

`ais install` no longer inspects project state to guess which tools
to install for; it just installs every adapter's already-configured
entries (each adapter already no-ops cleanly when it has none) and
gains `-g/--global` (folding in the deleted `ais user install`) and
`--json`. `ais add-all --tools` now hard-errors on an unknown tool
name instead of warning and skipping, matching the broadcast groups'
severity, and resolves CLI-group aliases (agy/droid) through
cli-groups.ts instead of a second hand-maintained table. Top-level
`--help` collapses the 30 registry-driven tool groups into one index
line so the core commands and broadcast groups aren't buried.
Documents the breaking changes from the mode-inference removal,
registry-driven tool/broadcast commands, and install/help simplification.
…ll noise

- repoHasEntry/detectImportAdapter's hybrid branch was a bare pathExists on
  the un-suffixed path, missing every cursor-rules entry stored as
  "<name>.mdc"/"<name>.md" — broadcast add/dry-run and `ais <tool> import`
  now check hybridFileSuffixes the same way createMultiSuffixResolver does.
  Also fixes file-mode lookups when the caller already passed a suffixed name.
- Stub redirects (`ais <tool> add`) no longer suggest a nonexistent broadcast
  command for subtypes with no group (e.g. copilot's "instructions").
- `ais install` (project scope) no longer prints one "No X Y found" line per
  adapter (~80 lines); it aggregates into a single summary line.
- Shell completion no longer offers the deleted `add`/`remove`/`user`
  top-level commands, and now covers the 6 new `ais <subtype>` broadcast
  groups (add/remove/list, plus entry-name completion after add).
- RESERVED_TOP_LEVEL_WORDS now includes git/completion/_complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ort name passthrough

- repoHasEntry now delegates to adapter.resolveSource when present (the same
  function real add() calls via GitRepoSource) instead of a hand-rolled
  mode/suffix reimplementation. Fixes three related preview/dry-run drift
  bugs: false "will-add" for ~30 file-mode adapters that have no
  resolveSource and only match an exact un-suffixed name; false
  "skip: not-in-repo" for file-mode adapters using createMultiSuffixResolver
  (e.g. cline-rules directory entries); and dry-run ignoring sourceFile/
  sourceDir repo config overrides (claude-md, codebuddy-md).
- detectImportAdapter now returns the resolved on-disk name alongside the
  adapter, and `ais <tool> import <name>` passes that through instead of the
  user's raw input — detection matching a suffixed file (e.g. "foo" ->
  "foo.mdc") used to still hand the unsuffixed name to importEntry, which has
  no suffix resolution of its own and would fail with a confusing
  "File ... does not exist" after printing "Detected rules: foo".
- buildAddPreviewRow's "already configured" check now also matches suffixed
  config keys (manager.add() stores file/hybrid entries under their
  suffix-resolved name), so re-running dry-run after a real add reports
  "skip: already-configured" instead of "will-add" forever.
- Broadcast groups' remove/list commands get real `rm`/`ls` aliases —
  completion already advertised them (added in the previous commit) but
  Commander had no matching alias registered.
- `ais init --only/--exclude` now accepts CLI group names (e.g. "agy"), not
  just the adapter registry's internal tool field, matching --tools elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…resolveSource errors

isEntryConfigured and runList's config-section lookup keyed by
adapter.tool/subtype, which is wrong for agents-md (tool: 'agents-md',
configPath: ['agentsMd', 'file']) — its manifest entries are written
under configPath, so already-configured detection and `ais md list`
always missed it.

repoHasEntry also collapsed every resolveSource rejection into a bare
"not found", including errors that mean the entry exists but needs
disambiguation (copilot's duplicate-suffix error) or is invalid
(agents-md's non-AGENTS.md validation error) — masking an actionable
message behind a misleading "not in repository". Non-"not found"
errors now propagate through to dry-run rows and real add's output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the existing (but previously unused) DotfileManager.status()/diff()
into a new read-only `ais doctor` command that reports ok/missing/conflict/
stale per entry, with --user and --json, exiting non-zero for CI use.

Actually exercising status()/diff() end-to-end for the first time surfaced
three real bugs, all fixed here with regression tests:
- GitSource.ensureCloned() ran a bare `git pull` for any repo with a .git
  dir, failing on a purely local (never-pushed) rules repo.
- diff()'s stale check compared a relative readlink() result against an
  absolute path as a raw string, misclassifying every healthy relative
  symlink as stale.
- status()/diff() resolved the on-disk filename from the raw manifest key,
  ignoring suffixes a hybrid/file-mode adapter appends at link time — so
  entries added via the documented string-shorthand config format
  (`"react": "<url>"`) on e.g. cursor rules showed as missing even when
  correctly linked.

Also adds user-scope support (UserRulesSyncManifest) so `ais doctor --user`
can check ~/.config/ai-rules-sync/user.json entries too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings docs (EN+ZH), README, and README banners in line with the new
explicit-only command surface: ais doctor, broadcast command groups,
the generated per-tool command shape, and updated migration guidance.
Verified with `npm run docs:build` (no dead links).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… and false-success bugs

Broadcast add/remove now accept a comma-separated entry list ("ais skills
add a,b,c --tools ..."), and a new "ais <subtype> add-all" discovers and
adds every entry a repo has for that subtype across the targeted tools.

Dogfooding these against a real user-level setup surfaced two pre-existing
bugs, unrelated to the new feature itself:

- User/global-scope remove deleted whatever real file or directory
  happened to sit at the target path when it wasn't an ais-created symlink
  (unlinkEntry in sync-engine.ts had no isSymbolicLink() guard, unlike the
  project-scope path). Now refuses and reports instead of deleting.

- User/global-scope add silently reported success and wrote a user-config
  dependency even when DotfileManager.add() had correctly refused to
  overwrite a real file and returned linked: false. handleAdd's user
  branch, and broadcast add/add-all, never checked that flag. Now all
  three surface skip: conflict (or a non-zero exit for the single-tool
  command) instead of claiming success for a link that never happened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e, fix alias-blindness in add-all/search

- claude-agents was mode: 'directory' but real Claude Code subagents are
  single .md files, so add/add-all/search never found any real agent.
- ~30 other file/hybrid-mode adapters (trae/junie/kilo/kimi/qwen/cline/codex/
  augment/codebuddy/factorydroid/continue/kiro/windsurf/antigravity-cli/
  opencode/pi) declared fileSuffixes but had no resolveSource, so add only
  matched a name typed with the literal suffix. All now use the same
  createMultiSuffixResolver/createSuffixAwareTargetResolver pair the working
  adapters already used.
- add-all and search --configured checked raw config-key membership, so an
  entry configured under an alias was invisible to both, causing add-all to
  re-add (and re-link under a duplicate name) anything already aliased. New
  isSourceNameConfigured helper checks both the plain key and every entry's
  rule field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e .md files

Same bug class as the claude-agents fix in the previous commit: cursor-agents
was mode: 'directory' but real Cursor subagents are single .md files with
YAML frontmatter (.cursor/agents/<name>.md), confirmed against Cursor's
official docs and matching third-party writeups. Checked deepagents-agents
against the same question — DeepAgents subagents are each a directory
containing a fixed AGENTS.md, so mode: 'directory' is correct there and it's
left unchanged.

Live-verified: a real .md agent file in a scratch repo now resolves and
links via `ais cursor agents add <bare-name>`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…or file-mode adapters

Found while checking a related shape question: a shared sourceDir
(sourceDir['*'].agents) can hold a directory-shaped entry for one tool
(deepagents: <name>/AGENTS.md) alongside a file-shaped entry for another
(claude/cursor/etc: <name>.md) under the same bare name. createMultiSuffixResolver
checked the un-suffixed exact name first and matched it even for adapters
declared mode: 'file', so e.g. `ais claude agents add reviewer` could silently
link the deepagents-shaped directory instead of the correctly-shaped .md file,
with no error at all.

Directory matching is now opt-in via allowDirectoryMatch, defaulting to false.
Only cursor-rules (the one adapter that's genuinely mode: 'hybrid') sets it.
Every other adapter now rejects a same-named directory with a clear
"exists as a directory, but this tool expects a single <suffix> file" error
instead of silently matching it.

Live-verified: a repo with both agents/reviewer.md and agents/reviewer/
(deepagents shape) now correctly resolves reviewer.md for `ais claude agents
add`; a repo with only the directory shape now fails with the new clear
error instead of matching the directory or a generic "not found".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lbb00 lbb00 changed the title 1.0.0: explicit-only command surface, no more mode guessing AIS 1.0: explicit commands and predictable global workflows Aug 20, 2026

This branch has not been deployed

No deployments
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.

1 participant