Files
Archduino/docs/superpowers/plans/2026-04-14-force-reapply.md
2026-04-14 21:42:52 +02:00

727 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Force Reapply Implementation Plan (Plan 4b)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add `archduino install --force` that reapplies every selected module unconditionally — new `PlanForce` method on `module.Module`, new `Reinstall` helpers on `platform.Pacman`/`platform.Yay`, engine + wizard + CLI wiring.
**Architecture:** Interface-level `PlanForce(ctx) (Plan, error)` gives each module an explicit "redo everything" path. Engine branches on `RunOptions.Force` to skip `Detect` and call `PlanForce`. Wizard mirrors that branch in its preview cache. Package-only modules reinstall without `--needed`; config-writing modules rewrite managed files.
**Tech Stack:** Go, existing cobra + bubbletea stack. No new dependencies.
**Spec:** `docs/superpowers/specs/2026-04-14-force-reapply-design.md`
---
## File Structure
**Modified (no new files):**
- `internal/platform/pacman.go` + `_test.go` — add `Reinstall`
- `internal/platform/aur.go` + `_test.go` — add `Reinstall`
- `internal/module/module.go` — add `PlanForce` to interface
- `internal/engine/engine.go` + `_test.go` — add `RunOptions.Force`
- `internal/tui/state.go` — add `WizardState.Force`
- `internal/tui/wizard/wizard.go` + `_test.go` — force-aware preview
- `cmd/archduino/main.go` — add `--force` flag
- 13 module files + their tests — implement `PlanForce`
---
## Task 1: Platform layer — `Reinstall` on Pacman and Yay
**Files:**
- Modify: `internal/platform/pacman.go`
- Modify: `internal/platform/pacman_test.go`
- Modify: `internal/platform/aur.go`
- Modify: `internal/platform/aur_test.go`
- [ ] **Step 1: Inspect existing Install methods**
Read the existing `Install` methods in `pacman.go` and `aur.go` to know their exact command construction. The new `Reinstall` must be identical except for the `--needed` flag being absent.
```bash
grep -n -A 15 "func.*Install" internal/platform/pacman.go internal/platform/aur.go
```
Read the test files to understand how commands are asserted (likely via `FakeRunner` recording executed commands).
- [ ] **Step 2: Write failing test for `Pacman.Reinstall`**
Add to `internal/platform/pacman_test.go`:
```go
func TestPacmanReinstall(t *testing.T) {
fr := &FakeRunner{}
p := NewPacman(fr)
if err := p.Reinstall(context.Background(), []string{"git", "curl"}); err != nil {
t.Fatalf("Reinstall: %v", err)
}
if len(fr.Calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(fr.Calls))
}
got := fr.Calls[0]
// Must include -S and --noconfirm but NOT --needed.
if !containsAll(got.Args, "-S", "--noconfirm") {
t.Errorf("expected -S and --noconfirm in args; got %v", got.Args)
}
for _, a := range got.Args {
if a == "--needed" {
t.Errorf("Reinstall must not pass --needed; got args %v", got.Args)
}
}
if !containsAll(got.Args, "git", "curl") {
t.Errorf("expected packages git, curl in args; got %v", got.Args)
}
}
```
Adjust `containsAll` / `FakeRunner` / `Calls` field names to match what the existing tests already use. Read `pacman_test.go` first to confirm the exact helper names — do NOT invent names.
- [ ] **Step 3: Run test, verify it fails**
```bash
cd /home/marvin/git-clones/ArduinoArchSetup
go test ./internal/platform/ -run TestPacmanReinstall -v
```
Expected: FAIL with `undefined: Pacman.Reinstall` or similar.
- [ ] **Step 4: Implement `Pacman.Reinstall`**
Add to `internal/platform/pacman.go`, mirroring the existing `Install`:
```go
// Reinstall runs `pacman -S --noconfirm pkg...` without --needed so pacman
// unconditionally reinstalls each package. Used by force-reapply.
func (p Pacman) Reinstall(ctx context.Context, pkgs []string) error {
if len(pkgs) == 0 {
return nil
}
args := append([]string{"pacman", "-S", "--noconfirm"}, pkgs...)
return p.runner.RunSudo(ctx, args[0], args[1:]...)
}
```
Adjust the runner method (`RunSudo` / `Run`) to whatever the existing `Install` uses. Read `Install` first.
- [ ] **Step 5: Run test, verify it passes**
```bash
go test ./internal/platform/ -run TestPacmanReinstall -v
```
Expected: PASS.
- [ ] **Step 6: Repeat Steps 2–5 for `Yay.Reinstall`**
Add `TestYayReinstall` to `internal/platform/aur_test.go` (parallel structure; `yay` command instead of `pacman`). Then implement `Yay.Reinstall` in `aur.go`. Run `go test ./internal/platform/ -run TestYayReinstall -v`, verify it passes.
- [ ] **Step 7: Commit**
```bash
git add internal/platform/pacman.go internal/platform/pacman_test.go \
internal/platform/aur.go internal/platform/aur_test.go
git commit -m "feat(platform): add Reinstall helpers to Pacman and Yay"
```
No Co-Authored-By trailer.
---
## Task 2: Extend Module interface + stub all 13 modules
Adding `PlanForce` to the interface breaks compilation until every module implements it. This task adds the interface method and a *stub* implementation on every module that returns `module.Plan{}, errors.New("PlanForce not implemented")`. Compilation stays green, engine is not wired yet, and each module's real implementation lands in a later task with its own test.
**Files:**
- Modify: `internal/module/module.go`
- Modify: 13 module files listed below
- [ ] **Step 1: Add `PlanForce` to the interface**
Edit `internal/module/module.go`. In the `Module` interface block:
```go
type Module interface {
Meta() Meta
Detect(ctx context.Context) (State, error)
Plan(ctx context.Context, current State) (Plan, error)
PlanForce(ctx context.Context) (Plan, error)
Apply(ctx context.Context, plan Plan, r Reporter) error
Verify(ctx context.Context) error
}
```
Add a doc comment above `PlanForce`:
```go
// PlanForce returns a plan that reapplies the module unconditionally,
// ignoring current detected state. Used by `archduino install --force`.
// Implementations should produce a plan equivalent to a from-scratch
// install — reinstall packages without pacman's --needed guard and
// rewrite managed config files.
```
- [ ] **Step 2: Verify compile fails**
```bash
cd /home/marvin/git-clones/ArduinoArchSetup
go build ./... 2>&1 | head -20
```
Expected: compile errors like `*starship.Module does not implement module.Module (missing method PlanForce)` for every module.
- [ ] **Step 3: Add stub `PlanForce` to every module**
For each of the 13 module files below, append this stub at the bottom of the file (replace `Module` receiver type if the file uses something different — read each file first to confirm):
```go
func (m Module) PlanForce(ctx context.Context) (module.Plan, error) {
return module.Plan{}, errors.New("PlanForce not implemented")
}
```
Ensure `errors` is imported. The module files:
1. `internal/modules/packages/packages.go`
2. `internal/modules/gittools/gittools.go`
3. `internal/modules/fonts/fonts.go`
4. `internal/modules/renode/renode.go`
5. `internal/modules/hello/hello.go`
6. `internal/modules/starship/starship.go`
7. `internal/modules/yazi/yazi.go`
8. `internal/modules/zsh/zsh.go`
9. `internal/modules/zellij/zellij.go`
10. `internal/modules/shellconfig/shellconfig.go`
11. `internal/modules/themes/themes.go`
12. `internal/modules/neovim/neovim.go`
13. `internal/modules/arduinocli/arduinocli.go`
- [ ] **Step 4: Verify compile + existing tests pass**
```bash
go build ./...
go test ./...
```
Expected: compile clean, all existing tests green. The new stubs aren't exercised yet.
- [ ] **Step 5: Commit**
```bash
git add internal/module/module.go internal/modules/
git commit -m "feat(module): add PlanForce to Module interface with stubs"
```
---
## Task 3: Engine — `RunOptions.Force` + branch
**Files:**
- Modify: `internal/engine/engine.go`
- Modify: `internal/engine/engine_test.go`
- [ ] **Step 1: Write failing test**
In `internal/engine/engine_test.go`, add:
```go
func TestRunForceSkipsDetectAndCallsPlanForce(t *testing.T) {
fake := &fakeModule{
id: "fake",
planForce: module.Plan{Actions: []module.Action{{
Description: "forced action",
Exec: func(ctx context.Context) error { return nil },
}}},
}
reg := module.NewRegistry()
if err := reg.Register(fake); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
sum, err := engine.Run(context.Background(), reg, engine.RunOptions{
Selected: []string{"fake"},
Force: true,
Reporter: engine.NewPlainReporter(&buf),
})
if err != nil {
t.Fatalf("Run: %v", err)
}
if fake.detectCalls != 0 {
t.Errorf("Detect should not be called under Force; got %d calls", fake.detectCalls)
}
if fake.planForceCalls != 1 {
t.Errorf("PlanForce should be called once; got %d", fake.planForceCalls)
}
if fake.planCalls != 0 {
t.Errorf("Plan should not be called under Force; got %d", fake.planCalls)
}
if sum.Applied != 1 {
t.Errorf("expected Applied=1, got %d", sum.Applied)
}
}
```
`fakeModule` already exists in `engine_test.go`; extend it with:
- `planForce module.Plan`
- `planForceCalls int`
- Implement `PlanForce(ctx) (module.Plan, error)` that increments the counter and returns `planForce`.
- If `detectCalls`/`planCalls` don't already exist on the fake, add them too (they may already be there — read the file first).
Read `engine_test.go` before editing to confirm the fake's field layout.
- [ ] **Step 2: Run test, verify it fails**
```bash
go test ./internal/engine/ -run TestRunForceSkipsDetectAndCallsPlanForce -v
```
Expected: FAIL — likely `unknown field Force in RunOptions` at compile time.
- [ ] **Step 3: Add `Force` to `RunOptions`**
In `internal/engine/engine.go`, find the `RunOptions` struct and add:
```go
// Force, when true, skips Detect and calls PlanForce on every module
// so each one produces a from-scratch plan regardless of current state.
Force bool
```
- [ ] **Step 4: Branch `Run` on `Force`**
Find the per-module loop inside `Run`. The current shape is roughly:
```go
cur, err := mod.Detect(ctx)
if err != nil { /* handle */ }
plan, err := mod.Plan(ctx, cur)
```
Replace with:
```go
var (
plan module.Plan
err error
)
if opts.Force {
plan, err = mod.PlanForce(ctx)
} else {
cur, derr := mod.Detect(ctx)
if derr != nil {
// preserve existing Detect error handling verbatim
err = derr
} else {
plan, err = mod.Plan(ctx, cur)
}
}
```
Read the current block first and preserve its error-handling shape (reporter calls, halt-on-error, summary bookkeeping). Do not inline a different handling style.
- [ ] **Step 5: Run test, verify it passes**
```bash
go test ./internal/engine/ -run TestRunForceSkipsDetectAndCallsPlanForce -v
go test ./internal/engine/ -v
```
Expected: both green.
- [ ] **Step 6: Commit**
```bash
git add internal/engine/engine.go internal/engine/engine_test.go
git commit -m "feat(engine): add RunOptions.Force to skip Detect and call PlanForce"
```
---
## Task 4: Package-only modules — real `PlanForce`
Package-only modules' `PlanForce` produces a plan that calls `Reinstall` on the full package list.
**Modules covered:** `packages`, `gittools`, `fonts`, `renode`, `hello`.
**Files:** for each module, both `<name>.go` and `<name>_test.go`.
For the four real modules (packages, gittools, fonts, renode), follow the same pattern. Do this **one module at a time** (5 cycles of write-test → verify-fail → implement → verify-pass → next). For `hello`, since it's a placeholder, make `PlanForce` return a fixed non-empty plan (see below).
### Pattern (use for each of packages / gittools / fonts / renode)
- [ ] **Step 1: Write failing `TestPlanForce` in the module's `_test.go`**
Template (adapt field names to the module under test — read the existing `TestPlan` test for each module to see how the fake pacman/yay is wired):
```go
func TestPlanForceReinstallsAllPackages(t *testing.T) {
fp := &FakePacman{installed: map[string]bool{"git": true, "curl": true}}
m := New([]string{"git", "curl"}, fp)
plan, err := m.PlanForce(context.Background())
if err != nil {
t.Fatalf("PlanForce: %v", err)
}
if plan.IsEmpty() {
t.Fatal("force plan must not be empty even when packages are installed")
}
// Execute to confirm it calls Reinstall, not Install.
for _, a := range plan.Actions {
if err := a.Exec(context.Background()); err != nil {
t.Fatalf("Exec: %v", err)
}
}
if fp.ReinstallCalls != 1 {
t.Errorf("expected 1 Reinstall call, got %d", fp.ReinstallCalls)
}
}
```
If `FakePacman` doesn't already have a `ReinstallCalls` counter and a `Reinstall` method, add them to the fake first (read the fake in the same test file — most modules share a local fake or use a common one in `internal/platform`).
- [ ] **Step 2: Run test, verify it fails**
```bash
go test ./internal/modules/<name>/ -run TestPlanForce -v
```
Expected: FAIL — the stub returns an error.
- [ ] **Step 3: Replace stub with real `PlanForce`**
In `<name>.go`, replace the stub with (adjust receiver/field names):
```go
func (m Module) PlanForce(ctx context.Context) (module.Plan, error) {
if len(m.pkgs) == 0 {
return module.Plan{}, nil
}
pkgs := append([]string{}, m.pkgs...)
return module.Plan{Actions: []module.Action{{
Description: "pacman -S (force) " + strings.Join(pkgs, ", "),
Kind: module.ActionPackageInstall,
Exec: func(ctx context.Context) error { return m.pm.Reinstall(ctx, pkgs) },
}}}, nil
}
```
Ensure the module's stored `pm` field (or equivalent) has the `Reinstall` method available — it will, because Task 1 added it to both `Pacman` and `Yay`, and the fake was extended in Step 1.
- [ ] **Step 4: Run test, verify it passes**
```bash
go test ./internal/modules/<name>/ -v
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add internal/modules/<name>/
git commit -m "feat(<name>): implement PlanForce to reinstall all packages"
```
### `hello` module
The `hello` module is a placeholder. Implement `PlanForce` as:
```go
func (m Module) PlanForce(ctx context.Context) (module.Plan, error) {
return module.Plan{Actions: []module.Action{{
Description: "hello force reapply",
Exec: func(ctx context.Context) error { return nil },
}}}, nil
}
```
Add a test that just asserts the plan is non-empty. Same commit cadence as above.
- [ ] **Step 6: Complete all 5 modules before moving on**
After each of packages / gittools / fonts / renode / hello has its own commit, move to Task 5. Do NOT batch them into one commit.
---
## Task 5: Hybrid modules — `starship`, `yazi`
These modules install packages AND (optionally) write configs. Their existing `Plan` already produces a plan that may include both; `PlanForce` should produce both unconditionally.
For each of `starship` and `yazi`:
- [ ] **Step 1: Read the existing `Plan` to see what it does**
```bash
grep -n -A 30 "func.*Module.*Plan\b" internal/modules/starship/starship.go
grep -n -A 30 "func.*Module.*Plan\b" internal/modules/yazi/yazi.go
```
Identify the branches: package install vs. config write (if any).
- [ ] **Step 2: Write failing `TestPlanForce`**
The test must arrange a "fully satisfied" state (packages installed, any config files already present) and assert the force plan is non-empty AND its actions reinstall the packages (via Reinstall call counter) AND rewrite the config files (via a writefile call counter or file presence assertion).
Use the same pattern as Task 4 for the package side. For the config side, use whatever the module's existing tests use to assert config writes (likely `WriteFile` mock or a tempdir).
- [ ] **Step 3: Run test, verify it fails**
```bash
go test ./internal/modules/<name>/ -run TestPlanForce -v
```
- [ ] **Step 4: Replace stub with real `PlanForce`**
```go
func (m Module) PlanForce(ctx context.Context) (module.Plan, error) {
var actions []module.Action
if len(m.pkgs) > 0 {
pkgs := append([]string{}, m.pkgs...)
actions = append(actions, module.Action{
Description: "pacman -S (force) " + strings.Join(pkgs, ", "),
Kind: module.ActionPackageInstall,
Exec: func(ctx context.Context) error { return m.pm.Reinstall(ctx, pkgs) },
})
}
// Any config-write actions the module normally conditions on Detect:
actions = append(actions, /* unconditional config-write actions */ ...)
return module.Plan{Actions: actions}, nil
}
```
Fill in the config-write actions by lifting them from `Plan` but stripping the "already exists" short-circuit. Reuse the same helper functions (e.g. `writeConfig`) that `Plan` uses.
- [ ] **Step 5: Run test, verify it passes, commit**
```bash
go test ./internal/modules/<name>/ -v
git add internal/modules/<name>/
git commit -m "feat(<name>): implement PlanForce (reinstall + rewrite config)"
```
Repeat for both modules, one commit each.
---
## Task 6: Config-writing simple modules — `zsh`, `zellij`, `shellconfig`, `themes`
Same pattern as Task 5 — read existing `Plan`, lift its actions, strip conditionals. Do one module per commit.
For each of `zsh`, `zellij`, `shellconfig`, `themes`:
- [ ] **Step 1: Write failing `TestPlanForce`** — satisfied-system fixture → assert non-empty plan with all the module's write/install actions.
- [ ] **Step 2: Run test, verify it fails.**
- [ ] **Step 3: Replace stub with real implementation.**
- [ ] **Step 4: Run test, verify it passes.**
- [ ] **Step 5: Commit** with message `feat(<name>): implement PlanForce`.
Module-specific notes:
- **`shellconfig`** has no packages; `PlanForce` only produces file-write actions.
- **`themes`** writes under `~/.config/archduino/overlay.yaml` — force rewrites the overlay's theme fields unconditionally.
- **`zsh`** clones plugins (oh-my-zsh, etc.) via `git`; `PlanForce` should clone even if the target already exists — add a pre-step that removes the directory, or use the `git` helper's `ForceClone` if one exists (grep `internal/platform/git.go`; if missing, note it as a concern and fall back to rm-then-clone via an action that uses `os.RemoveAll` before the clone).
---
## Task 7: Config-writing complex modules — `neovim`, `arduinocli`
Same pattern as Task 6, but these modules have multiple sub-steps (neovim: clone NvChad, write chadrc, run post-install; arduinocli: install binary, update index, install cores, install libraries).
For each module:
- [ ] **Step 1: Enumerate the existing `Plan`'s sub-actions.** Every conditional "already done" branch in `Plan` becomes an unconditional branch in `PlanForce`.
- [ ] **Step 2: Write failing `TestPlanForce`** — satisfied-system fixture → assert ALL sub-actions are present (count-based is fine if exact equality is fragile).
- [ ] **Step 3: Run test, verify it fails.**
- [ ] **Step 4: Replace stub with real implementation.**
- [ ] **Step 5: Run test, verify it passes.**
- [ ] **Step 6: Commit** with message `feat(<name>): implement PlanForce`.
**Neovim specifics:**
- If `NvChad` is already cloned, `PlanForce` should either `git pull` + rewrite `chadrc.lua` OR `rm -rf && git clone`. Pick the simpler: rewrite `chadrc.lua` unconditionally, do NOT delete the existing clone. Users' local NvChad modifications shouldn't be wiped by `--force`. Document this in a code comment.
**arduinocli specifics:**
- `PlanForce` should re-download the binary tarball, re-run `arduino-cli core update-index`, and re-install every configured core/library regardless of current installation. Cores install is idempotent at the arduino-cli layer; no issue.
---
## Task 8: TUI wizard — force-aware preview
**Files:**
- Modify: `internal/tui/state.go`
- Modify: `internal/tui/wizard/wizard.go`
- Modify: `internal/tui/wizard/wizard_test.go`
- [ ] **Step 1: Add `Force bool` to `WizardState`**
In `internal/tui/state.go`, find the `WizardState` struct (or wherever the wizard's global state lives) and add a `Force bool` field with a doc comment:
```go
// Force mirrors the --force CLI flag: when true, the wizard's per-module
// plan preview uses PlanForce instead of Detect + Plan, and the header
// shows a "Force mode" banner.
Force bool
```
If `WizardState` is constructed in a factory (e.g., `NewWizardState(cfg, reg)`), leave the constructor signature alone — the flag is set by the CLI after construction.
- [ ] **Step 2: Write failing test for wizard preview under force**
In `internal/tui/wizard/wizard_test.go`, add a test that dispatches the wizard's plan-preview code path with `Force: true` on a fake module whose `Detect` panics (proving it's not called) and whose `PlanForce` returns a non-empty plan.
The exact test shape depends on how the wizard currently tests previews — read the existing preview tests before writing. Bare minimum:
```go
func TestWizardPreviewUsesPlanForceWhenForceSet(t *testing.T) {
mod := &fakeModule{
planForce: module.Plan{Actions: []module.Action{{Description: "forced"}}},
detect: func() (module.State, error) { t.Fatal("Detect should not be called"); return nil, nil },
}
// ... drive the preview path and assert the displayed plan is the PlanForce output
}
```
- [ ] **Step 3: Run test, verify it fails**
```bash
go test ./internal/tui/wizard/ -run TestWizardPreviewUsesPlanForceWhenForceSet -v
```
- [ ] **Step 4: Branch the preview cache on `Force`**
In `internal/tui/wizard/wizard.go`, find the `planCache` / preview logic (Plan 3a Task 1 added this — search for `planCache` or `Detect` call sites inside the wizard). Add a branch:
```go
if state.Force {
plan, err = mod.PlanForce(ctx)
} else {
cur, derr := mod.Detect(ctx)
if derr != nil { /* existing error path */ }
plan, err = mod.Plan(ctx, cur)
}
```
Also: the cache key must include `force` so a force run doesn't reuse a non-force cached plan. If the cache is keyed by module ID only today, extend it to `(moduleID, force)`.
- [ ] **Step 5: Add a header banner**
Find the wizard's view/render code (likely a `View()` method). When `state.Force`, prepend a banner line to the rendered output:
```go
if state.Force {
sb.WriteString(styles.Warning.Render("⚠ Force mode: all selected modules will be reapplied"))
sb.WriteString("\n\n")
}
```
Use whatever Lipgloss style the codebase already has for warnings (grep for `Warning`, `warnStyle`, or similar). If none exists, use a plain bold-red or bold string — do NOT create a new style module just for this.
- [ ] **Step 6: Run test, verify it passes + rest of wizard tests**
```bash
go test ./internal/tui/wizard/ -v
go test ./internal/tui/ -v
```
Expected: all green.
- [ ] **Step 7: Commit**
```bash
git add internal/tui/state.go internal/tui/wizard/
git commit -m "feat(tui): wizard uses PlanForce preview when Force is set"
```
---
## Task 9: CLI — `--force` flag
**Files:**
- Modify: `cmd/archduino/main.go`
- [ ] **Step 1: Add the flag**
In `newInstallCmd`, alongside the other flag declarations (`noTUI`, `dryRun`, `halt`, `noSudo`, `only`, `skip`, `overlayPath`), add:
```go
var force bool
// ...
cmd.Flags().BoolVar(&force, "force", false,
"Reapply every selected module, ignoring current state")
```
- [ ] **Step 2: Propagate to engine and wizard**
In the `RunE` body:
- Where `engine.RunOptions{…}` is constructed (there are currently two call sites: no-TUI path and TUI path — both must set `Force: force`).
- Where `tui.NewWizardState(cfg, reg)` is called: after construction, set `initial.Force = force`.
- [ ] **Step 3: Smoke-test**
```bash
cd /home/marvin/git-clones/ArduinoArchSetup
go build ./cmd/archduino
./archduino install --help | grep -A 2 force
./archduino install --force --dry-run --no-tui --no-sudo 2>&1 | head -20
```
Expected: `--help` shows the new flag; a dry-run with `--force` completes without error and prints per-module force plans.
- [ ] **Step 4: Commit**
```bash
git add cmd/archduino/main.go
git commit -m "feat(cmd): add --force flag to install command"
```
---
## Task 10: End-to-end validation
**Files:** none (validation only)
- [ ] **Step 1: Full test suite with race detector**
```bash
cd /home/marvin/git-clones/ArduinoArchSetup
go test -race ./...
```
Expected: every package `ok`. Failures here = STOP and fix.
- [ ] **Step 2: Full build**
```bash
go build ./...
```
Expected: exit 0.
- [ ] **Step 3: gofmt + vet**
```bash
diff=$(gofmt -l .); [ -z "$diff" ] && echo "gofmt clean" || { echo "$diff"; exit 1; }
go vet ./...
```
Expected: both clean.
- [ ] **Step 4: Dry-run smoke (without and with force)**
```bash
./archduino install --dry-run --no-tui --no-sudo 2>&1 | tail -10
./archduino install --dry-run --no-tui --no-sudo --force 2>&1 | tail -10
```
Expected: second output's per-module plan is non-empty for every selected module (even ones the first output reports as satisfied).
- [ ] **Step 5: No commit — validation is informational**
---
## Notes for the executing agent
- **Interface change order matters.** Task 2's stubs MUST land before any per-module `PlanForce` test runs (Task 3 onward), otherwise the whole package is red.
- **Read each module's existing `Plan` before writing `PlanForce`.** The force version is almost always "strip the conditionals from Plan". Don't invent new actions.
- **Do not delete users' cloned repos during force.** Especially neovim's NvChad clone. Rewriting `chadrc.lua` is enough; use `git pull` if the user wants updates, not `rm -rf && git clone`.
- **If a module's test file doesn't already have a fake pacman/yay with a `Reinstall` method,** extend the fake in that file (or, if it lives in `internal/platform/testing.go` or similar, extend it once there). Do NOT duplicate fakes — reuse.
- **Conventional Commits:** every module's force implementation gets its own `feat(<name>):` commit. This keeps the changelog readable and svu happy.
- **CI is now live** (Plan 4d merged into this branch). Expect the workflow to run on every push of this plan — if it goes red, the task isn't done.