fix(slash): /rollback stops accepting a count it never used, and stops lying about HEAD [SC-A3.1] - #74
Merged
Merged
Conversation
…s lying about HEAD
Lane SC-A3, slice 1 of the review-and-ship rail.
Three defects in two commands that discard the user's uncommitted work, and
which shipped with no tests at all.
1. `/rollback [n]` accepted a count that did nothing.
The count was parsed, range-checked to 1..50, and then never referenced
again — every invocation reverted the entire working tree, whatever number
was passed. The usage line advertised "revert last n filesystem changes",
a capability that does not exist: nothing in this codebase records per-step
filesystem state.
There is no checkpoint journal to hang a real `[n]` on, so this takes the
other option the contract allows and drops the argument. Passing one now
prints what it used to do and refuses, rather than silently ignoring it.
`/revert step-3` likewise stops saying "coming soon" for something no code
is working toward.
2. Both commands claimed changes were "restored to last commit". They were not.
`git checkout -- <path>` restores from the INDEX. With anything staged, the
content that comes back is the staged content — so the message was false
exactly when the user had staged work they might have been counting on.
Both commands now check the index and say which it was, and `/rollback`
prints the command that does go all the way back to HEAD. The confirmation
prompt names the same target as the completion line.
3. Neither command worked from a subdirectory.
Detection was `existsSync(join(cwd, ".git"))`, which reports "not a git
repository" for every subdirectory of one — where a REPL usually sits.
Now `git rev-parse --show-toplevel`.
Also: `git checkout --` becomes `git restore --`. Both restore from the index,
but restore only ever touches files, so it cannot be talked into switching
branches by a crafted pathspec.
Adds a testability seam. Both functions take an optional GitToolDeps
{ cwd, git } defaulting to the real runner. It reuses the existing GitRunner
from git_commit_guard.ts rather than introducing a third runner abstraction —
this repo already has two, which recon flagged as a hazard. The three call
sites in slash.ts are unchanged.
First behavioural coverage for these commands — 12 tests, previously zero:
the count is refused, and refusing mutates nothing
usage text no longer promises per-change undo
"last commit" is never claimed while the index is populated
"last commit" is claimed only when nothing is staged
untracked files are never removed (no `clean` in any argv)
declining the prompt issues no mutation
a clean tree issues no mutation
a subdirectory of a repo is still in the repo
outside a repo it refuses and mutates nothing
/revert passes the path after `--`, proven with a leading-dash filename
/revert on an untracked file refuses and mutates nothing
Mutation-checked: forcing restoreTargetLabel to always answer "last commit"
fails both truthfulness tests (10 pass / 2 fail); restoring gives 12 / 12.
Registry and COMMANDS.md updated to match — `/rollback` no longer documents an
argument, and both entries state the index-versus-HEAD behaviour rather than
leaving the reader to discover it.
Gates at this commit:
npm run typecheck exit 0
npm test 934 pass / 0 fail (922 on clean 41a7e26)
Not addressed here, and still open in this lane: hunk-level staging, the
commit and PR rail, and stage_diff.ts still parsing human `git diff --stat`
output with a regex.
This was referenced Aug 19, 2026
Merged
Merged
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.
Problem
Three defects in two commands that discard the user's uncommitted work, and which shipped with no tests at all (
grep -rln 'rollbackSlash|revertSlash' test/→ zero files).1.
/rollback [n]accepted a count that did nothingnappears at:16and:17and nowhere else in the file. Every invocation reverted the whole working tree, whatever number was passed. The usage line advertised "revert last n filesystem changes" — a capability that does not exist. Nothing in this codebase records per-step filesystem state.2. Both commands claimed "restored to last commit". They were not.
git checkout -- <path>restores from the index, not HEAD. With anything staged, the content that comes back is the staged content — so the message was false exactly when the user had staged work they might have been counting on.3. Neither command worked from a subdirectory
Detection was
existsSync(join(cwd, ".git")), which reports "not a git repository" for every subdirectory of one — where a REPL usually sits.Contract
On
[n]: the spec allows either a real checkpoint journal or dropping the count. There is no journal to hang[n]on, so this drops it. Passing an argument now prints what it used to do and refuses, rather than silently ignoring it./revert step-3likewise stops saying "coming soon" about something no code is working toward.On the restore target: the destructive scope is deliberately not widened. It would have been easy to make the message true by restoring from HEAD instead — but that destroys staged work the old command preserved. Making a destructive command destroy more is not a bug fix. So the behaviour stands and the message tells the truth:
The confirmation prompt names the same target as the completion line, so neither can drift from the other.
Implementation
git checkout --→git restore --. Both restore from the index, butrestoreonly ever touches files, so it cannot be talked into switching branches by a crafted pathspec.git rev-parse --show-toplevel.GitToolDeps { cwd, git }defaulting to the real runner. It reuses the existingGitRunnerfromgit_commit_guard.tsrather than adding a third runner abstraction — this repo already has two, flagged as a drift hazard during recon. The three call sites inslash.tsare unchanged.Tests
12 tests, previously zero. All drive a scripted
GitRunner; none touches a real repository.checkout/restorein argvcleanin any argvrev-parse, not.gitprobing/revertpasses the path after--/reverton an untracked file refusesMutation-checked. Forcing
restoreTargetLabelto always answer"last commit"fails both truthfulness tests — 10 pass / 2 fail. Restoring gives 12 / 12.Gates at
8e15476:npm run typechecknpm testBaseline on clean
41a7e261, measured in the same session: 922 / 0.Security notes
--separator, withcore.literalPathspecs=true(inherited fromSpawnGitRunner). A leading-dash filename is used in a test to prove it.Known limits
git add -p, nogit apply --cached. That is the next slice.gh pr createis still only a printed hint string (repo.ts:91), never executed.stage_diff.tsstill regexes humangit diff --statoutput and still usesexecSyncwith a shell string. Untouched here to keep this diff to the destructive commands; it is the one remaining shell-string git caller in the tree./stage-diffremains uncovered — this PR tests only the two destructive commands.Dependency and merge order
Independent of SC-A0 (#72) and SC-A2 (#73) — branched from
origin/main, no shared files. Any order.Per the integration order this lands after SC-A2 and before SC-A4.