Repository navigation
feat(setup): preview and roll back migration to the skills-only plugin - #339
Merged
Merged
Conversation
Add `hack setup migrate` for moving one scope at a time from standalone Hack skill and rule files to the skills-only `hack` plugin (HACK-1132). - The default preview is read-only. It lists each standalone target as remove, blocked, preserve or absent, using the W11 ownership classifier. Customized, unknown and symlinked paths are preserved. - Replacement checks read client configuration per scope: Claude Code settings precedence plus an installed copy carrying the replacement skills; Codex `[plugins."hack@hack-dance"] enabled = true`. Cursor is not inspectable and needs an explicit --skip-plugin-check. - --apply is all or nothing. It refuses without writing when a target is blocked, commits a 0600 backup manifest with exact bytes and mode before removing anything, and rechecks bytes before each removal. - --rollback restores only into empty paths, never overwrites, refuses manifests whose paths, digests or bytes are not Hack-owned targets, and recovers an interrupted apply. - Hooks, MCP entries and AGENTS.md/CLAUDE.md sections are untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
argv is parsed once against the union of every command's options keyed by long name. Declaring `setup migrate --rollback <manifest>` as a value option retyped the existing boolean `config adopt --rollback`, so `hack config adopt --rollback --json` consumed `--json` as its value and failed with a usage error. That broke every legacy-compose adoption rollback in the docker E2E shards. Rename the migration flag to `--rollback-manifest <manifest>` and add a command-tree guard that each long option keeps one value type, plus a parse regression for `config adopt --rollback --json`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 20, 2026 13:57
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…#370) ## Root cause `parseCliArgv` parsed argv once against the union of every command's options, keyed by long name (`buildUnionParseOptions`). Two options shared a short flag: `--path -p` (project commands) and `ssh --port -p`. Bun's `parseArgs` resolved `-p` to whichever came last, which was `ssh --port`. So `hack up -p <dir>` (and `init`, `down`, `ps`, `logs`, `env ...`, `config ...`, `setup ...`) parsed as `--port`, and `run.ts` refused it with `Option(s) not valid for "up": --port`. The same union let one long name's type in one command change how every other command parsed it (#339 hit this with `--rollback`), which is why a guard test banned cross-command type conflicts. ## Fix `src/cli/command.ts` now parses in two passes: 1. **`scanInvocation`** resolves the command path, and the child-command boundary for passthrough positionals, from the leading positionals. It reads each option with the options of the command resolved so far, plus the global and built-in options, and skips that option's value token, including the omitted-value form of options with a default. An option placed before the command name falls back to the other declarations of that spelling. If those declarations disagree about taking a value, the option is read as a flag, so it never consumes the command name. 2. **`parseCliArgv`** then parses with that command's own options plus the global and built-in options, by long and short spelling. Options that only other commands declare are still parsed by long name, never by short flag. A misplaced option therefore keeps its existing `Option(s) not valid for "<command>"` refusal. `findPassthroughStart` and `hackOwnedArgv` use the same scan, so #359's child-argv passthrough uses the resolved command's option types. That covers `--` handling, flags after the child command, and the JSON envelope selected only by Hack's own `--json`. Default-value normalization is scoped the same way. `run.ts` is unchanged. The only parse difference found by comparing old and new output on edge cases (`--help --all`, `help --all`, `-- up`, `--json run web tool --json`, `--path /x up`, `logs --tail`, `up --tail`, `ssh --path /x`, `x ext cmd -p 1`, `--no-interactive up -p /x`) is the intended one: `up -p /x` now sets `path` instead of `port`. ## Tests - New `tests/cli-per-command-options.test.ts`: - `-p` sets `--path` for every command that declares `--path -p`. The test walks the spec and asserts that it includes init/up/down/ps/logs/env list/config validate/setup migrate. - `ssh -p` sets `--port`. - `-p` before the command name follows that command. - A misplaced foreign long option is still refused as "not valid here", while an unknown short flag is unknown. - `config adopt --rollback --json` stays boolean, and `setup migrate --rollback-manifest <file>` takes a value. - A synthetic CLI where `--rollback`/`-r` is a flag in one command and takes a value in another parses correctly per command, before the command name, and at the passthrough boundary. - A source-CLI spawn under `privateCliEnv` checks that `hack ps -p <dir> --json` no longer reports `--port`. - `tests/cli-command.test.ts`: the cross-tree "one value type per long option" guard is replaced by a per-scope guard. Within a command's scope (built-ins, globals and the command's own options), each long and short spelling must name exactly one option. - The #359 passthrough tests in `tests/cli-passthrough-argv.test.ts` are unchanged and pass. Each new test was checked by mutation: - With the old `command.ts`, 11 of the new tests fail, including the spawn test. It printed `Option(s) not valid for "ps": --port`. - Making disagreeing declarations keep the last type fails the before-command-name test. - Removing other commands' long names from the parse table fails the "not valid here" test. - Giving `ssh --port` the short flag `-h` fails the per-scope guard with `hack ssh: -h names --help and --port`. ## Evidence Bun 1.4.2 (`mise x bun@1.4.2`), base ace57ee. `next` has since moved to f2278c7; it only adds an unrelated `run.ts` hunk and merges cleanly. - `bun run --cwd packages/cli typecheck`: 0 errors. `bun run typecheck`: passes. - `bun run --cwd packages/cli check` and `bun run check`: 0 errors. The changed files lint clean, with no new warnings. - Focused suites pass: 131/131 for the new test file, cli-passthrough-argv, cli-command, cli- spawn-env-isolation, cli-reference-docs, integration-ownership, mcp and mcp-inspect-tools. One run had a transient failure in an `mcp.test.ts` spawn test under load; it passed alone and in the rerun. - `bun run docs:cli-reference`: no change to `docs/reference/cli.md`, because the help surface is unchanged. - After `bun run build`, `dist/hack ps -p <dir> --json` and `dist/hack up -p <dir> --json` (private HOME) reach project discovery ("No .hack/ found") instead of the `--port` refusal. - No native refusal codes or messages changed, so the native plan-CLI scripts were not rerun. - gitleaks 8.30.1 over a `git log -p origin/next..HEAD` export: no leaks. ## Remaining gaps - Full `bun run test` ran on a heavily loaded host for 4253s and ended with 6824 pass and 640 fail across 105 files. Rerun serially with `--timeout 60000`, 89 of those files pass. - The 16 that still fail are native-compose/native-authored files. Rerun with `origin/next`'s `command.ts` swapped in, they fail the same way or worse; for example, `native-compose- adoption-generation` fails 0/111 on both. - Two files with a single failure each (`dependency-startup-progress`, `native-compose- adoption-probe`) pass alone, and neither imports the CLI parser. - None of these failures is caused by this change. A green full run on a quiet runner or CI is still outstanding. - Edge case, documented in `scanInvocation`: an option placed before the command name whose spelling is a flag in one command and takes a value in another is read as a flag. No such spelling exists in the current CLI. Refs HACK-1293 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: HACK-1132 (WU19, unit F16). Scoped migration with preview, replacement verification and rollback.
Final behavior
New
hack setup migratesubcommand. It moves one scope at a time from standalone Hack skill and rule files to the skills-onlyhackplugin (the #305 layout).Preview (default, read-only). Lists every standalone target in the selected scope (
--path,--global,--all-scopes) and clients (--claude,--codex,--cursor; default all) with one action:remove: Hack-owned (W11 classifier: current or known earlier render) and the replacement plugin is verified or explicitly skipped.blocked: Hack-owned, but the replacement is not verified.preserve: customized, unknown, or reached through a symlink. Each has a closed, value-free reason.absent.Targets: the Codex
hack-cli/hack-initskills, the Claude Codehack-initskill, the Cursorhack.mdcrule, and the shared~/.ai/skills/hack-cli. The shared skill follows Claude Code or Codex.Replacement verification reads configuration only (
src/agents/plugin-replacement.ts):hack@hack-dance(projectsettings.local.json, then projectsettings.json, then user).~/.claude/plugins/installed_plugins.jsonmust also list an installed copy that carries thehack-cliandhack-initskills.[plugins."hack@hack-dance"] enabled = truein the project or userconfig.toml.not-inspectable, so it needs--skip-plugin-check cursor.--skip-plugin-check <clients>acceptsclaude,codex,cursororall.--applyis all or nothing.$HACK_HOME/agent-migrations/<id>.json. The write is fsynced, then renamed into place. The manifest holds the exact bytes, sha256 and mode of each file.changedand left in place, with exit 1.--rollback-manifest <manifest>:already-present; anything else isconflict(exit 1) and left unchanged.Claude primer hooks, MCP entries and
AGENTS.md/CLAUDE.mdsections are never touched.--jsonreturns the usual{ok, data}envelope. Messages never include file contents.setup-sync.tsnow exportslistOwnedFileTargetsso migration and sync share one path/surface table.Docs:
docs/integrations.md(new "Migrate standalone files to the plugin" section, and the switching guidance now points to it),docs/cli.md, thetests/e2e/README.mdfreeze description, and a regenerateddocs/reference/cli.md. No consumer guidance or render changes, so there are no fingerprint or revision updates.Validation (local, macOS, source CLI)
tests/plugin-migration.test.ts(15 tests). Temp project and HOME roots are passed explicitly, so the process HOME is never touched. Covered:already-presentchangedtests/setup-migrate-command.test.ts(4 tests). Runs the source CLI throughprivateCliEnvwith private HOME and HACK_HOME, and covers preview JSON, refused apply, skip-verified apply, rollback, flag conflicts and--skip-plugin-checkparsing.--rollback-manifest, not--rollback. The CLI parses argv once against the union of every command's options, keyed by long name. A value-typedsetup migrate --rollbackretyped the booleanconfig adopt --rollback, soconfig adopt --rollback --jsontook--jsonas its value and failed with a usage error. That failed all tennative-compose-adoption-*-worktreesdocker E2E scenarios in CI run 39280939414.tests/cli-command.test.tsnow requires each long option to keep one value type across the command tree, and checks thatconfig adopt --rollback --jsonstill parses as two booleans.agent-surface-freezee2e: addedsetup migrate --all-scopesandsetup migrate --all-scopes --apply --skip-plugin-check all, twice each. Ran locally withbun tests/e2e/run.ts --only=agent-surface-freeze: pass, all customized seeded surfaces byte-identical, positive control intact.tests/integration-ownership.test.ts,tests/cli-spawn-env-isolation.test.ts,tests/cli-reference-docs.test.ts,tests/cli-help.test.tsandtests/integration-sync.test.tspass.bun run --cwd packages/cli typecheck: 0 errors.bun run --cwd packages/cli check: 0 errors. Its 34 warnings come from files this PR does not change.bunx ultracite checkon the changed files is clean.bun run privacy:checkpasses.Negative controls (mutation proofs)
Each mutation below was applied alone. The named test file failed, and the source was then restored:
enabledforced true--skip-plugin-checkaccepts unknown names--rollback-manifestaccepts extra flagssetup migrateflag named--rollbackagain (value-typed): both newtests/cli-command.test.tsguards failRemaining gaps (host-bound)
config.tomlenablement is checked. The Claudeinstalled_plugins.jsonshape is the v2 registry form (array or object, both accepted), and this PR does not re-observe it against the current client release.setup migrate --applyruns are not serialized. Each removal rechecks bytes and ownership, which leaves the same single read-then-unlink window documented for W11.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.