Skip to content

Add Reinstall skill button to Settings → Corveil CLI (CROW-491)#498

Merged
dgershman merged 3 commits into
mainfrom
feature/crow-491-reinstall-skill-button
Jun 12, 2026
Merged

Add Reinstall skill button to Settings → Corveil CLI (CROW-491)#498
dgershman merged 3 commits into
mainfrom
feature/crow-491-reinstall-skill-button

Conversation

@dgershman

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a Reinstall skill button next to the existing Verify button in Settings → General → Corveil CLI.
  • Runs the same corveil skill install --path … flow as the per-launch path (Add Corveil CLI path picker + auto-install /query-corveil on launch #483), on demand — no restart, workspace switch, or path re-pick required. Serves the common "I just rebuilt corveil locally; install the new embedded skill" loop.
  • Disabled when the picker is unset; shows ✓ Skill reinstalled / ✗ <diagnostic> inline (coalesces with Verify's result line — only one runs at a time). A successful manual reinstall also clears the launch-time warning banner; a failed one updates it, so "is corveil broken?" stays single-sourced.

Plumbing

SettingsView lives in the CrowUI package, which cannot import the Crow app target where Scaffolder lives. The call therefore goes through a new AppState.onReinstallCorveilSkill async closure wired in AppDelegate.launchMainApp — same callback-injection pattern as onCreateManager, onRestartManager, etc. The closure offloads the synchronous Process.waitUntilExit work to a detached task internally, so awaiting it from the main actor doesn't pin the UI for the 5-second install timeout.

Scaffolder.installCorveilSkill access is loosened from private to internal so AppDelegate can call it on a fresh Scaffolder instance (this is the "function-accessibility refactor" the ticket references — #490 has not yet landed in this branch).

Test plan

  • Configure a valid corveil path → click Reinstall skill → expect transient ✓ Skill reinstalled; {devRoot}/.claude/commands/query-corveil.md byte-matches <corveilPath> skill show
  • Rebuild corveil locally with a tweaked embedded skill string → click Reinstall skill again without restarting Crow → expect file content to reflect the new build
  • Set path to a non-executable junk value → click → expect ✗ <diagnostic> inline AND the orange warning banner at the top of General (existing corveilSkillInstallWarning surface)
  • Successful reinstall after a failure clears the orange banner
  • Clear the path → button is disabled with tooltip "Set the Corveil CLI path first."
  • Click repeatedly with a valid path → no errors (install is idempotent via os.WriteFile + os.Chmod)
  • Verify button and Reinstall button mutually disable while either is running

Closes #491

🤖 Generated with Claude Code

@dgershman dgershman requested a review from dhilgaertner as a code owner June 11, 2026 23:31
@dgershman dgershman added the crow:merge Crow auto-merge on green label Jun 11, 2026

@dhilgaertner dhilgaertner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code & Security Review

Tidy, well-documented feature. The callback-injection pattern matches the existing onCreateManager/onListWorkspaceRepos precedent, the loosening of installCorveilSkill to internal is appropriately scoped, the single coalesced result line with mutual disabled is a clean way to avoid two competing status lines, and the threading story (async closure offloading Process.waitUntilExit to a detached task) keeps the main actor unpinned for the 5s timeout. CrowCore builds clean; CrowUI/app target couldn't be built locally (missing GhosttyKit.xcframework artifact — pre-existing infra, unrelated to this PR), so those two files were reviewed by inspection.

Security Review

Strengths:

  • No new external input surface. The reinstall path reuses Scaffolder.installCorveilSkill, which already validates the binary is executable (isExecutableFile), bounds runtime with a SIGTERM watchdog, discards stdout to a null device to avoid pipe-write deadlock, and routes the binary via executableURL + arguments (no shell, no injection vector).
  • Failure diagnostics surfaced to the user come from corveil's own stderr, already truncated/trimmed; nothing sensitive added.

No security concerns.

Code Quality

Yellow — stale devRoot capture: reinstall can target the wrong directory and still report success.
Sources/Crow/App/AppDelegate.swift:433-438 — the closure captures the launch-time let devRoot (line 330) and builds Scaffolder(devRoot: devRoot) from it. But saveSettings(devRoot:config:) (AppDelegate.swift:1153-1154) reassigns self.devRoot when the user edits Development Root — which lives in the same Settings window as the Reinstall button. So the sequence "change devRoot → scroll down → click Reinstall skill" installs query-corveil.md into the previous devRoot's .claude/commands/, while the UI still reports ✓ Skill reinstalled. This diverges from the sibling closure onListWorkspaceRepos (AppDelegate.swift:847-848), which deliberately uses [weak self] + live self.devRoot for exactly this reason. Since the whole selling point of the button is "no restart required," silently honoring a stale devRoot is surprising. Suggest { [weak self] path in ... read self?.devRoot on the main actor, then pass into Task.detached }, mirroring onListWorkspaceRepos.

Green — false ✓ Skill reinstalled when the closure is unwired.
SettingsView.swift:675-688await appState.onReinstallCorveilSkill?(path) returns nil both on real success and when the closure isn't set (previews/unit tests). nil falls into the success branch, printing ✓ Skill reinstalled and clearing the warning banner for an install that never ran. Harmless in production (always wired), but the "degrade gracefully" comment oversells it — it degrades to a false positive. Consider a sentinel/explicit nil-closure branch if previews ever exercise this.

Green — result line not cleared on manual path edits.
The corveilReinstallResult doc comment says it's cleared "on path edits so stale results don't outlive a binary swap," but only the Browse button (SettingsView.swift:285-287) clears it. Typing a new path into the TextField + onSubmit { save() } (SettingsView.swift:274-276) leaves a stale ✓ Skill reinstalled visible against the new path. Functionality is unaffected (disabled/run logic reads the live binding); only the displayed line is briefly stale. Consider clearing both results in an .onChange of the binding, or soften the comment.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Request Changes — driven by [0 Red, 1 Yellow, 2 Green] findings. The Yellow (stale devRoot) is the only blocker; addressing it in the same round avoids a follow-up.


🐦‍⬛ Reviewed by Crow via Claude Code

dgershman added a commit that referenced this pull request Jun 11, 2026
…edit

1. Read self.devRoot live on each Reinstall click (Yellow blocker):
   the launch-time `let devRoot` capture went stale when the user edits
   Development Root in the same Settings window, silently installing
   into the previous devRoot while reporting success. Now mirrors the
   `onListWorkspaceRepos` pattern — `[weak self]` and reads
   `self.devRoot` from the main actor before dispatching to the
   detached task.

2. Guard against unwired closure to prevent false ✓ in previews
   (Green): when AppDelegate hasn't wired `onReinstallCorveilSkill`,
   `await callback?(path)` returns nil and falls into the success
   branch, printing "Skill reinstalled" for an install that never
   ran. Explicit `guard let callback` inside the Task now reports
   "Reinstall unavailable in this build" instead.

3. Clear inline results when the path binding changes (Green): the
   doc comment promised path-edit clearing but only Browse did it.
   Typed/pasted edits to the TextField left a stale ✓/✗ visible
   against the new path. `.onChange` of the binding's
   source-of-truth (the config dict slot) now clears both results
   for any edit path; Browse's redundant manual clears are removed.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <[email protected]>
Crow-Session: F2C4CB9B-B99D-4C06-AD44-E04F5F5E11A4
@dgershman dgershman requested a review from dhilgaertner June 11, 2026 23:40

@dhilgaertner dhilgaertner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code & Security Review

Critical Issues

None.

Security Review

Strengths:

  • No new attack surface. Reinstall reuses the existing per-launch installCorveilSkill flow — the user-configured corveil binary is invoked with an argument array (["skill", "install", "--path", target]), so there is no shell/command-injection vector.
  • isExecutableFile is still gated before launch; the failure path returns a sanitized, user-facing diagnostic and logs via NSLog (no secrets, no network).
  • target is derived from devRoot, not from untrusted input.

Concerns: None.

Code Quality

Strong, idiomatic work — the threading claims in the description check out:

  • Concurrency is race-free. SettingsView is main-actor-isolated (confirmed by the pre-existing synchronous @State mutation at SettingsView.swift:648, which only compiles under swift-tools 6.0 if the method is @MainActor). The new Task { } in reinstallCorveilSkill (SettingsView.swift:679) therefore inherits main-actor isolation, making the direct @State/appState mutations safe, while the blocking Process.waitUntilExit is correctly offloaded to Task.detached inside the AppDelegate closure (AppDelegate.swift:438).
  • Live devRoot read (AppDelegate.swift:437) correctly avoids installing into a stale launch-time devRoot after an in-window Settings change.
  • Unwired-callback guard (SettingsView.swift:685) properly distinguishes "real success (nil)" from "never ran," preventing a false .
  • Single source of truth for the warning banner is preserved — success clears corveilSkillInstallWarning, failure updates it.
  • privateinternal on installCorveilSkill is module-scoped and well documented.

Green considerations (non-blocking):

  • In-flight path edit staleness (SettingsView.swift:671): the path TextField stays editable while a reinstall is running (only the buttons disable). If the user edits the path during the install window, the completing Task writes ✓ Skill reinstalled next to the newly-typed path. The install itself is correct (it captured the path at click time + reads devRoot live); this is purely cosmetic and self-corrects on the next action.
  • Theoretical false-success (AppDelegate.swift:437): if self (AppDelegate) were deallocated, the closure returns nil, which the caller reads as success. AppDelegate lives for the app lifetime, so this is not reachable in practice.
  • No automated tests for the new glue. The success/failure/unwired branching is thin async UI glue, but unlike verifyCorveil (which extracted runCorveilVersion as a pure, testable helper) nothing here is unit-tested. Acceptable given the manual test plan.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by [0 Red, 0 Yellow, 3 Green] findings.


🐦‍⬛ Reviewed by Crow via Claude Code

dgershman and others added 3 commits June 11, 2026 19:47
The launch-time path (#483) installs the embedded /query-corveil skill into
the devroot via `corveil skill install`. After rebuilding corveil locally,
the embedded skill version drifts from what's installed in the devroot, and
the only ways to fix it were heavy: restart Crow (closes every session),
switch workspaces, or re-pick the same path in Settings.

Add a dedicated "Reinstall skill" button next to the existing Verify button
that runs the same install flow on demand. Disabled when the picker is
unset; surfaces ✓/✗ inline (coalesced with Verify's result line — only one
operation runs at a time); also updates the launch-time warning banner so
"is corveil broken?" has a single source of truth.

Plumbing: SettingsView lives in the CrowUI package which can't import the
Crow app target where Scaffolder lives, so the call goes through a new
`AppState.onReinstallCorveilSkill` async closure wired in AppDelegate
(matches the existing on* callback pattern). The closure offloads the
synchronous Process.waitUntilExit work to a detached task internally so
awaiting it from the main actor doesn't pin the UI for the install timeout.
`Scaffolder.installCorveilSkill` access is loosened from private to
internal so AppDelegate can call it on a fresh Scaffolder instance.

Closes #491

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <[email protected]>
Crow-Session: F2C4CB9B-B99D-4C06-AD44-E04F5F5E11A4
…edit

1. Read self.devRoot live on each Reinstall click (Yellow blocker):
   the launch-time `let devRoot` capture went stale when the user edits
   Development Root in the same Settings window, silently installing
   into the previous devRoot while reporting success. Now mirrors the
   `onListWorkspaceRepos` pattern — `[weak self]` and reads
   `self.devRoot` from the main actor before dispatching to the
   detached task.

2. Guard against unwired closure to prevent false ✓ in previews
   (Green): when AppDelegate hasn't wired `onReinstallCorveilSkill`,
   `await callback?(path)` returns nil and falls into the success
   branch, printing "Skill reinstalled" for an install that never
   ran. Explicit `guard let callback` inside the Task now reports
   "Reinstall unavailable in this build" instead.

3. Clear inline results when the path binding changes (Green): the
   doc comment promised path-edit clearing but only Browse did it.
   Typed/pasted edits to the TextField left a stale ✓/✗ visible
   against the new path. `.onChange` of the binding's
   source-of-truth (the config dict slot) now clears both results
   for any edit path; Browse's redundant manual clears are removed.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <[email protected]>
Crow-Session: F2C4CB9B-B99D-4C06-AD44-E04F5F5E11A4
After rebasing onto #497 (CROW-490 picker-change hot-trigger), CROW-491
had two parallel install hooks pointing at the same Scaffolder call:
the new AppState.onReinstallCorveilSkill and #490's
SettingsView.onCorveilReinstall. Drop the duplicate and route the
Reinstall button through #490's existing infrastructure:

- Widen `SettingsView.onCorveilReinstall` from
  `((String?) -> Void)?` → `((String?) async -> String?)?`. Picker
  commits fire-and-forget the return; the Reinstall button awaits it
  for inline ✓/✗ feedback.
- Refactor `AppDelegate.enqueueCorveilInstall` to return the warning
  per call (each task reports its own result; serialization through
  `corveilInstallTail` is preserved). `corveilInstallTail` becomes
  `Task<String?, Never>?`.
- Read `self.devRoot` live inside the `onCorveilReinstall` wiring
  closure — fixes the same stale-capture bug both #490 and the v1
  CROW-491 implementation had (saveSettings mutates devRoot in the
  same Settings window).
- Drop `AppState.onReinstallCorveilSkill` and its launch-time wiring.

The Reinstall button now naturally serializes behind any in-flight
picker-driven install, which the original AppState-callback path
bypassed.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <[email protected]>
Crow-Session: F2C4CB9B-B99D-4C06-AD44-E04F5F5E11A4
@dgershman dgershman force-pushed the feature/crow-491-reinstall-skill-button branch from e657caf to 155fefa Compare June 11, 2026 23:54
@dgershman dgershman merged commit ef0f861 into main Jun 12, 2026
2 checks passed
@dgershman dgershman deleted the feature/crow-491-reinstall-skill-button branch June 12, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crow:merge Crow auto-merge on green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings → Corveil CLI: add 'Reinstall skill' button next to Verify (manual rebuild-pickup without restart)

2 participants