Skip to content

Close the --force flag-verb bypass - #344

Closed
blooop wants to merge 4 commits into
mainfrom
wayfinder/devlaunch-303
Closed

Close the --force flag-verb bypass#344
blooop wants to merge 4 commits into
mainfrom
wayfinder/devlaunch-303

Conversation

@blooop

@blooop blooop commented Aug 22, 2026

Copy link
Copy Markdown
Owner

dl --force <ws> --rm force-deleted. verb_command folded cli.force in wherever clap happened to find the flag (cli.rs:756), while workspace_command consults force_placement (cli.rs:847-882) and refuses the same misparse for the word verbs. The guard's own rationale — "clap-parsed Rust silently accepted the flag and deleted" — applies identically to both spellings.

Two slices, each red first:

  1. --force in the verb slot is refused, not obeyed. The flag-verb grammar now runs the same placement check as the word grammar, so dl rm --force <ws> and dl --rm --force <ws> earn the same refusal. The trailing slot keeps meaning force.
  2. A leading --force is a name, not a forced delete. dl --force <ws> --rm now reads slot 0 the way the word grammar does — as a workspace name — so the line targets a workspace called --force, earns the unknown-workspace refusal at runtime, and force-deletes nothing.

dl <ws> --rm --force keeps working throughout.

Verified locally, run alone: cargo test --workspace (1,578 pass, 20 suites), cargo clippy --locked --all-targets -- -D warnings, cargo fmt --check all clean.

Closes #303.

🤖 Generated with Claude Code

Summary by Sourcery

Align flag-based command parsing with positional force-placement rules to prevent ambiguous arguments from triggering forced workspace deletion.

Bug Fixes:

  • Prevent --force from being honored when it appears in workspace or verb positions for flag-based commands, eliminating unintended forced deletions.
  • Treat a leading --force as a workspace name and preserve the corresponding unknown-workspace behavior.
  • Keep valid trailing and selector-form --force usage working as intended.

Enhancements:

  • Unify force-placement validation across word and flag verb grammars.

Tests:

  • Add focused grammar and integration coverage for ambiguous --force placements and safe forced-delete behavior.

blooop added 2 commits August 22, 2026 11:21
dl --rm --force <ws> silently force-deleted: verb_command folded cli.force
in wherever clap found the flag, while the word grammar recovers --force's
position and refuses the verb slot. Route the flag-verb grammar through
the same placement check; dl rm --force <ws> and dl --rm --force <ws> now
earn the same refusal, and the trailing slot keeps meaning force.

Refs #303
…lete

dl --force <ws> --rm read the flag as force and deleted; the word grammar
reads slot 0 as a workspace *name*. The flag-verb grammar now gives the
same reading: the line targets a workspace called --force, earns the
unknown-workspace refusal at runtime, and force-deletes nothing.

Refs #303

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Aligns flag-verb grammar with word-verb grammar for --force placement so misparsed flag-based delete commands are refused or treated as workspace names, preventing unintended forced deletes.

File-Level Changes

Change Details Files
Apply the same --force positional/placement checks to flag-verb commands as are already used for word-verb commands, preventing bypassed forced deletes.
  • Update resolve() to pass argv into verb_command for flag-based verbs so placement logic has full argument context.
  • Extend verb_command to accept argv and run force_placement to determine whether --force is in the workspace, verb, or trailing slot.
  • In the WorkspaceSlot case, treat --force as the workspace name and route through normal workspace targeting with force disabled.
  • In the VerbSlot case, return retired-verb or unknown-verb errors consistent with word grammar behavior when --force occupies the verb position.
  • Allow the Trailing case to continue to interpret --force as the force modifier, preserving valid forced delete behavior.
rust/dl/src/cli.rs
Add tests that lock in the new flag-verb behavior for --force placement so regressions are caught.
  • Add a test ensuring --force in the verb slot for flag-verb spelling yields UnknownVerb and does not force-delete.
  • Add a test ensuring leading --force in the workspace slot is treated as a workspace name, with no forced delete, matching word grammar semantics.
rust/dl/src/cli.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#303 Route flag-verb handling through the same --force placement validation as word verbs, refusing --force when it appears in the verb slot, such as dl --rm --force <ws>.
#303 Prevent a leading --force from being interpreted as an effective force-delete flag; instead treat it as the workspace name and ensure no workspace is force-deleted for dl --force <ws> --rm.
#303 Preserve valid trailing-force behavior for dl <ws> --rm --force and equivalent flag-verb syntax.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.35099% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.03%. Comparing base (a1c6fe8) to head (df385b1).

Files with missing lines Patch % Lines
rust/dl/src/cli.rs 97.35% 4 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 95.40% <97.35%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 95.40% <97.35%> (+0.02%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This was generated by AI during review.

Two-axis review at fixed point a1c6fe8...12a5d8f (git diff a1c6fe8f38b51fdd75f79550a9ffbeb877ab911e...HEAD). Preflight: ref resolves, diff non-empty (1 file, +87/-3), all 14 checks green. Spec is #303. Axes were run in isolated contexts and are not merged or reranked; the third section is the review stage's own probe of the safety property, and is reported separately for the same reason.

Standards

Passes. No blocking findings. The change is in the house style — why-comments, a total function over Cli, sum-typed placement, sentence-named tests.

  • non-blockingDoc contradicts the new code. verb_command's doc-comment states the contract "The displaced words are named rather than dropped in silence — see [Overridden] — with one exception" (rust/dl/src/cli.rs:773-776). The new WorkspaceSlot arm returns .alone() (:816-821), so dl --force ws --rm silently drops ws — confirmed live, the only output is Unknown workspace '--force', with no mention that ws was ignored. That is a second exception, and the doc-comment is untouched by the diff. In a file where the doc-comments are the spec, this is the highest-value fix here.
  • non-blockingDuplicated VerbSlot arm. The force_placement match now appears in two places (:809-838 vs :913-947). Half-refuted: hoisting into resolve would break globals (dl --purge --force must keep ModifierNotAllowed, :703-708), and the two WorkspaceSlot arms genuinely differ (Verb::Attach { autorm } vs word.with(false)). But the VerbSlot arm is byte-identical in both (:823-835 = :929-941) and wants extracting — it is the piece a third caller would copy again.
  • nitTest does not pin the displacement. The module has an overridden() helper used elsewhere for exactly this question; the new tests (:1869-1885) assert parse() only, so the silent drop above is unasserted either way. Neighbouring tests carry failure messages (:1794); these two carry none.
  • nit — the bare "--force" literal count is doubled by this diff (:817, :833, alongside :920, :940); a named const would do. A payload on ForcePlace::WorkspaceSlot would not earn itself — the variant is only ever produced by matching the literal at :668.
  • refuted — the UnknownVerb suggestion wart is not new: at merge-base dl rm --force ws already renders "Use 'dl rm -- --force'". This PR extends an existing wart consistently rather than creating one.

Spec

Passes. All three acceptance criteria hold, and no documented example stops resolving.

Acceptance criteria, traced through resolveverb_commandforce_placement:

  • dl --rm --force <ws> — stream ["--rm","--force","ws"], index 1 → VerbSlotUnknownVerb (:831). Refuses, deletes nothing. ✅
  • dl --force <ws> --rm — index 0 → WorkspaceSlotWorkspace{target:"--force", verb: word.with(false)} (:816-821). Ticket: "a --force in the workspace slot became an unknown workspace". Force dropped, runtime lookup fails, nothing deleted. ✅
  • dl <ws> --rm --force and dl --rm <ws> --force — index 2 → Trailingverb = word.with(cli.force). Still forced remove. ✅

Documented examples, all still resolving: GRAMMAR:538 dl owner/repo 'review this pr' --rm --force; GRAMMAR:539 dl stop owner/repo --rm; README:153/:269 "a dl prune <ws> --force line recalled with --rm appended still removes <ws>"; GRAMMAR:530 "A verb with no workspace named picks interactively"; README:188/:1271 dl <ws> rm --force; and aid's main.rs:229 aid … fix the bug --rm --force, whose build_dl_args output is pinned at rust/aid/src/rewrite.rs:653 and lands Trailing.

Findings:

  1. non-blockingaid's leading verb-flag form loses force. rust/aid/src/rewrite.rs:343 documents "A verb flag can also arrive before the spec (aid --rm owner/repo)". aid --rm --force owner/repo emits ["--force","owner/repo","--rm"] (--force is not in SUFFIX_VERBS, so it lands in dl_options) → WorkspaceSlot → unknown workspace --force. Previously force-removed. Defensible — rewrite.rs:691-692 already anticipates it — but an unannounced change on a documented shape.
  2. non-blockingThe refusal advertises a line the same grammar refuses. rust/dl/src/lib.rs:336 renders UnknownVerb as "Use 'dl {target} -- {word}'", so dl --rm --force ws prints Use 'dl --rm -- --force' to run a shell command. — which verb_command then rejects with A shell command can only be run by 'dl <workspace> -- <command>', not with 'rm'. (cli.rs:787, verified live). The ticket asks it to "refuse the way dl rm --force <ws> already does"; it does, but with advice naming a flag as the workspace.
  3. non-blockingSelector + force is no longer spellable. GRAMMAR:520 "add --force to delete it anyway" composed with GRAMMAR:530-531 "A verb with no workspace named picks interactively … dl rm can clear five workspaces in one visit". dl --rm --force was the last working spelling (dl rm --force already refused on main) and now refuses. See B2 below.
  4. non-blocking scope note--stop is folded in too (:625) though VerbWord::with ignores force for Stop, so dl --stop --force ws goes from harmless to refused. Within the letter of "Route verb_command through the same placement check".
  5. nit — no README/GRAMMAR change (CLAUDE.md: "update the README.md to reflect the current tool behavior"). No doc line is falsified, but GRAMMAR:520 still states --force unconditionally while three of four positions now refuse it.

Safety probe (review stage's own, neither axis)

The ticket is a guard on a destructive operation, so the stage probed the whole placement space rather than the cases the tests name. Method: a temporary matrix test over resolve in a throwaway worktree (reverted, nothing pushed), then the same lines through a real debug/dl under a scratch XDG_CACHE_HOME.

B1 — blocking. A --force that is not trailing still force-deletes on a flag-verb line.

dl --rm stop  --force ws  =>  Workspace { target: "ws", verb: Remove { force: true } }
dl --rm code  --force ws  =>  Workspace { target: "ws", verb: Remove { force: true } }
dl --rm prune --force ws  =>  Workspace { target: "ws", verb: Remove { force: true } }   (no override notice at all)

In each, --force sits ahead of the workspace name it deletes — exactly the placement this ticket set out to stop honouring. Cause: force_placement classifies index ≥ 2 as Trailing (:674), and on a flag-verb line the verb flag itself consumes a stream slot, so index 2 can still have positional words after it. The word grammar cannot reach this — clap caps positionals at num_args = 0..=2 (:419), so its stream is at most [w1, w2, --force] and index ≥ 2 really is the end of the line (verified: dl a b --force c is refused by clap's cap).

This is the same off-by-one seen from the other side in the passing cases: on a flag-verb line index 0 is the verb flag and index 1 is the workspace, yet the check reads them as workspace and verb. dl --rm --force ws refuses with target: "--rm" — the flag named as the workspace — which the new test hard-codes (:1875-1877). The two grammars agree on the verdict for the 3-token cases the tests name; the slot model underneath is shifted by one, and that shift is what leaves B1 open.

Not a regression (these shapes force-deleted at merge-base too), but the invariant the diff's own comment asserts — "--force means force only in the trailing slot, the one place Python read it" (:804-805) — is not what the code delivers, and a forced delete is still reachable from a non-trailing --force. Suggested fix: on a flag-verb line, count the slot stream with the verb flag excluded (or require --force to be genuinely last), and add the 4-token shapes above as red tests.

B2 — blocking. dl --rm --force regressed, unguarded and undecided.

At merge-base it resolved to Select { verb: Remove { force: true } } — pick interactively, force-remove. It now refuses (UnknownVerb { target: "--rm", word: "--force" }). No test in the diff covers the bare two-token form, and no breadcrumb on #303 records the trade. The grammar is left incoherent about it: the natural spelling refuses while the retired word still works —

dl --rm --force        =>  REFUSED UnknownVerb { target: "--rm", word: "--force" }
dl --rm prune --force  =>  Select { verb: Remove { force: true } }

Either outcome is arguable, but it should be a decision with a test on it, not a side effect. Note this is the same loss Spec #3 names from the doc side.

Everything else in the space behaves: --devcontainer stripping is consistent in both =-form and separated form; --yes and --autorm are refused ahead of the placement check so they cannot shift the index; --json/--size require --ls and the what group forbids a second verb flag, so no other token can enter the stream; a doubled --force is refused by clap; and --force after -- stays with the command.

Verdict

Request changes (posted as a comment — GitHub refuses a change request on a PR authored by the same account).

Blocking:

  • B1 — a non-trailing --force still force-deletes on flag-verb lines of the form dl --rm <word> --force <ws>; the guard's stated "trailing slot only" invariant is not what the code implements.
  • B2dl --rm --force (selector + force) regressed with no test and no recorded decision, while the retired-word spelling dl --rm prune --force still works.

Non-blocking, worth folding in: the verb_command doc-comment now contradicts its own WorkspaceSlot arm (silent word drop), the duplicated VerbSlot arm, aid's leading --rm --force form, and the refusal that advertises a line the grammar refuses.

Both axes passed on their own terms; the blocking items come from the stage's safety probe of the placement space beyond the cases the tests name.

blooop and others added 2 commits August 22, 2026 16:34
…luded

The placement guard was counting the verb flag as a name slot, and a flag verb
is not a name — it is the verb. That shifted every slot by one, so on a
flag-verb line slot 2 was still mid-line rather than the end of it, and a
`--force` sitting *ahead of the workspace it deleted* read as the trailing
modifier and force-deleted: `dl --rm stop --force ws`, `dl ws --rm --force
other`. The word spelling of the same shape, `dl stop --force ws`, has always
refused it.

The rule is now asked of the stream the flag is not in, and stated as "no name
follows it" rather than "slot 2" — asking for slot 2 exactly would be the same
off-by-one from the other end, since the flag supplies the verb and so the
longest line left is a name, a name and a `--force`.

Proved by a matrix over the argv space rather than more named cases, since
named cases are what missed these: every ordering of up to four slot-carrying
tokens must satisfy "a forced delete implies nothing named follows --force". It
found `dl ws --rm --force other` on its own, which no report had named. The
destructive end is pinned at the binary boundary too, against the scenario
world's scratch cache, where the failing run really had removed the clone.

Also folded in from the review: the duplicated verb-slot refusal is one
function, `--force` is spelled once as a const, and `verb_command`'s
doc-comment names the second exception to its "displaced words are named"
contract instead of contradicting it.

Refs #303

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard refuses `--force` in a name slot because it is ambiguous there —
either the flag, or the workspace somebody meant to delete. A selector line
carries no name at all, so that ambiguity cannot arise, and a refusal there
guards nothing while costing a working invocation. The previous pass refused it
with no test on the trade either way; this pins it, in both token orders and in
the retired spelling `dl --rm prune --force` that says the same thing and has
always resolved this way, so that a future tightening of the rule cannot take
it back by accident.

Also pinned at the binary boundary, as far as a headless run can go: the line
has to reach the picker rather than the refusal. What the picker does with no
terminal to draw on is its own business and is not what the test asserts.

Refs #303

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blooop

blooop commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

This was generated by AI during a build stage.

Back from changes requested, both blockers reproduced red before being fixed. Two commits on top of 12a5d8f, no force-push: 40ce0fd and df385b1.

B1 — a non-trailing --force still force-deletes on flag-verb lines

Confirmed, and worse than reported. Reproduced first as a matrix over the argv space rather than as more named cases, since named cases are exactly what missed it. The matrix walks every ordering of up to four slot-carrying tokens (ws, other, stop, prune, --rm, --stop, --force) and holds one property: a forced delete implies nothing named follows --force. Its first red was a line no report had named —

dl ws --rm --force other  =>  Workspace { target: "ws", verb: Remove { force: true } }

— a three-token workspace-first line, so this was not only reachable through the 4-token shapes in the report. The destructive end was then pinned at the binary boundary in tests/grammar.rs, against the scenario world's scratch cache, beside the two Python-parity tests that already live there; the failing run's stderr really did say Removed workspace clone: ….

Root cause is the one the review named, taken at its word: the guard was counting the verb flag as a name slot, and a flag verb is not a name — it is the verb. That shifted every slot by one, in both directions at once, which is why the same defect showed as a mid-line --force reading as trailing and as dl --rm --force ws refusing with target: "--rm", the flag named as the workspace.

Fix: the rule is now asked of the stream the verb flag is not in (flag_verb_force_placement), and stated as "no name follows it" rather than "slot 2". Both halves matter — asking for slot 2 exactly would be the same off-by-one from the other end, since with the flag out the longest line left is a name, a name and a --force. Both grammars now give the same answer to the same positional shape:

dl     stop --force ws  =>  Unknown command '--force'. Use 'dl stop -- --force' …
dl --rm stop --force ws  =>  Unknown command '--force'. Use 'dl stop -- --force' …

Verified live against a scratch XDG_CACHE_HOME: --rm stop --force ws, --rm code --force ws, --rm prune --force ws (the retirement's sentence) and ws --rm --force other all refuse before anything is touched, while --rm ws --force and ws --rm --force reach the workspace lookup as forced removes.

This also lands the review's point that the refusal advertised a line the grammar refuses (Spec #2): the sentence now names stop, a real name from the line, rather than dl --rm -- --force.

B2 — dl --rm --force regressed, unguarded and undecided

Honoured as decided on the ticket: the selector form keeps working, and the "no name follows it" phrasing of the rule is what delivers it rather than a special case bolted beside it. A line naming no workspace has nothing for --force to have been mistaken for, so there is nothing there for the guard to protect.

df385b1 is the test the review asked for, and it pins the decision rather than the incident: both token orders (--rm --force, --force --rm) and the retired spelling dl --rm prune --force that says the same thing and has always resolved this way — so a future tightening of the rule cannot take it back by accident. The grammar no longer disagrees with itself. Pinned at the binary boundary as far as a headless run goes: the line has to reach the picker rather than the refusal; what the picker does with no terminal is its own business and is not asserted.

Non-blocking, folded in

  • Doc contradicting the codeverb_command's doc-comment now names the second exception to its "displaced words are named" contract (a --force in the workspace slot takes the target, so the whole line falls away with it) instead of claiming there is one.
  • Duplicated VerbSlot arm — extracted as force_in_the_verb_slot, one function, called from both grammars. The WorkspaceSlot arms stay separate, as the review found: they genuinely differ.
  • The bare "--force" literal — a FORCE const, spelled once. A typo in any of those four places was a silent forced delete.
  • Tests carry failure messages now, naming the line under test.

Non-blocking, deliberately not done

  • --force after -- and the --devcontainer stripping are untouched and still covered.
  • aid's leading aid --rm --force owner/repo (Spec Feature/update from template #1) is unchanged — still WorkspaceSlot, force dropped. Fixing it means changing where build_dl_args puts --force, which is rust/aid/src/rewrite.rs, outside this ticket's file scope.
  • README/GRAMMAR (Spec docs: Add PyPI badge to README #5, a nit) — no doc line is falsified, and a GRAMMAR edit inside cli.rs without the matching README edit would create exactly the drift CLAUDE.md forbids. Worth a follow-up that documents the placement rule in both places at once.

Suite green locally: cargo test --workspace 1,582 pass, 20 suites, zero failures; clippy --locked --all-targets -D warnings and fmt --check clean.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This was generated by AI during review.

Re-review of the two blocking findings at df385b1 (fast-forward over the reviewed 12a5d8f, 40ce0fd + df385b1, +297/-56 across cli.rs and tests/grammar.rs). I raised them, so this is the call on whether they are closed. Both resolved. I did not re-run the whole review; below is only my own findings plus what the new code newly introduces.

B1 — resolved, and by the right rule

flag_verb_force_placement (cli.rs:689-718) filters the verb flag out of the slot stream before asking the question, and reads --force as the modifier iff nothing follows it. Every shape from my report now refuses, with the word spelling's own sentence and the right word named:

dl --rm stop  --force ws   =>  Unknown command '--force'. Use 'dl stop -- --force' …
dl --rm code  --force ws   =>  Unknown command '--force'. Use 'dl code -- --force' …
dl --rm prune --force ws   =>  'prune' is no longer a workspace verb. …
dl --rm ws1   --force ws2  =>  Unknown command '--force'. Use 'dl ws1 -- --force' …

On whether "no name follows it" is the invariant at both ends — it is, and it is equivalent by construction rather than by luck. On any line reaching flag_verb_force_placement, the filtered stream is exactly the positional words plus --force: --yes (:860) and --autorm (:855) are refused ahead of it, --devcontainer and its value are stripped by slot_stream, the second verb flag is a clap what-group conflict, --json/--size need --ls (also in that group), a repeated flag is an ArgumentConflict, and everything past -- is outside the stream. So "--force is last in the stream" ⟺ "no positional word follows --force in argv" — the invariant I asked for. At the near end the builder's reasoning also holds: --force alone leaves cli.words empty, so the line resolves to Select and there is no name it could have been mistaken for. Asking for "slot 2 exactly" would indeed have been the same off-by-one mirrored, and would have cost dl --rm --force for nothing.

B2 — resolved

dl --rm --force, dl --force --rm and dl --rm prune --force all resolve to Select { verb: Remove { force: true } } (:1990-2016) — merge-base behaviour restored, the grammar no longer disagrees with itself about the retired spelling, and the trade is now a recorded decision with a test on it rather than a side effect.

The matrix claim, checked

The reported first red is real. At 12a5d8f, dl ws --rm --force otherWorkspace { target: "ws", verb: Remove { force: true } } — a forced delete of ws with other still after --force. Nobody had named it; my report's shapes were dl --rm <word> --force <ws>, with the flag leading rather than sitting between the names. Same root cause, a shape I missed.

The property is the right one. "If the line resolves to a forced Remove, then among the non-verb-flag tokens --force is last" states the safety rule with no reference to slot numbers — which is precisely what the off-by-one defeated. It quantifies over the outcome reached rather than over expected errors, so it cannot pass by refusing everything.

Coverage does span the reachable space, and I pushed on it. longest = 4 is exactly the maximum count of slot-carrying tokens a flag-verb line can hold (2 positionals + verb flag + --force). I re-ran the same property in a throwaway worktree over a wider alphabet — rm, up, code added — and to five tokens: 5370 resolving lines, 620 of them forced deletes, no counterexample. Non-vacuous. Sensitivity checked by porting the identical probe onto 12a5d8f, where it fails on dl ws other --rm --force rm. I also checked the nine placements of a --devcontainer pair around a flag-verb line (=-form and separated, before/inside/after): slot stripping is correct in all of them. Branch's own suite green locally, all suites, zero failures.

Still outstanding — all non-blocking, none should hold this PR

  1. The word grammar is still on the absolute-index phrasing. workspace_command keeps force_placement, where index ≥ 2 ⇒ Trailing (:684). That is equivalent to "no name follows" today only because clap caps positionals at num_args = 0..=2 (:419), making index 2 necessarily last. It is not equivalent by construction: widen that cap, or let one more unstripped flag into the stream, and the word half reproduces exactly the bug just fixed. And the new matrix would not catch it — its alphabet omits rm, the only token that makes the word grammar produce a forced delete, so the property never exercises the half of the code this PR did not change. Adding rm to the alphabet costs one word and closes that (I verified it still passes today). Latent fragility, not a live defect.
  2. aid --rm --force owner/repo still loses its force (rewrite.rs:343 documents the leading form; --force is not in SUFFIX_VERBS, so it lands in dl_options at slot 0). Unchanged from Spec #1.
  3. lib.rs:336 still advertises dl stop -- --force, which verb_command then refuses (:846). Much improved — it now names a real word instead of the flag --rm — but still advice that does not work.
  4. No README/GRAMMAR change; GRAMMAR:520 still states --force unconditionally.
  5. Globals have no placement rule at all: dl --force --prunePrune { yes: false, force: true }. Outside this ticket (clone directories, not workspaces, and it prints its plan and asks without -y) — noted so it is not mistaken for covered.

Also newly introduced and fine: force_in_the_verb_slot (:721-732) de-duplicates the VerbSlot arm and the FORCE const removes the doubled literal — both of my Standards notes; and the verb_command doc-comment now names its second exception (:833-839), closing the contradiction I flagged. dl --rm --force ws reads as workspace --force where the word spelling dl rm --force ws says UnknownVerb{target:"rm"} — a diagnostic-only divergence inherent to the flag holding no name slot; both refuse and neither deletes.

The tests/grammar.rs addition is the right kind of test: force_ahead_of_the_workspace_on_a_flag_verb_line_deletes_nothing asserts exit 1, the exact stderr, and world.clone_is_there() — it guards the destructive outcome end to end, not just the parse.

Verdict

Approve (posted as a comment — GitHub refuses an approval on a PR authored by the same account). B1 and B2 both resolved; the five items above are follow-up material, not blockers.

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.

Close the --force flag-verb bypass

1 participant