Add Reinstall skill button to Settings → Corveil CLI (CROW-491)#498
Conversation
dhilgaertner
left a comment
There was a problem hiding this comment.
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 viaexecutableURL+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-688 — await 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.
…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
dhilgaertner
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
- No new attack surface. Reinstall reuses the existing per-launch
installCorveilSkillflow — the user-configuredcorveilbinary is invoked with an argument array (["skill", "install", "--path", target]), so there is no shell/command-injection vector. isExecutableFileis still gated before launch; the failure path returns a sanitized, user-facing diagnostic and logs viaNSLog(no secrets, no network).targetis derived fromdevRoot, not from untrusted input.
Concerns: None.
Code Quality
Strong, idiomatic work — the threading claims in the description check out:
- Concurrency is race-free.
SettingsViewis main-actor-isolated (confirmed by the pre-existing synchronous@Statemutation atSettingsView.swift:648, which only compiles under swift-tools 6.0 if the method is@MainActor). The newTask { }inreinstallCorveilSkill(SettingsView.swift:679) therefore inherits main-actor isolation, making the direct@State/appStatemutations safe, while the blockingProcess.waitUntilExitis correctly offloaded toTask.detachedinside theAppDelegateclosure (AppDelegate.swift:438). - Live
devRootread (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. private→internaloninstallCorveilSkillis module-scoped and well documented.
Green considerations (non-blocking):
- In-flight path edit staleness (
SettingsView.swift:671): the pathTextFieldstays editable while a reinstall is running (only the buttons disable). If the user edits the path during the install window, the completingTaskwrites✓ Skill reinstallednext 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): ifself(AppDelegate) were deallocated, the closure returnsnil, 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 extractedrunCorveilVersionas 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.
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
e657caf to
155fefa
Compare
Summary
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.✓ 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
Scaffolderlives. The call therefore goes through a newAppState.onReinstallCorveilSkillasync closure wired inAppDelegate.launchMainApp— same callback-injection pattern asonCreateManager,onRestartManager, etc. The closure offloads the synchronousProcess.waitUntilExitwork to a detached task internally, so awaiting it from the main actor doesn't pin the UI for the 5-second install timeout.Scaffolder.installCorveilSkillaccess is loosened fromprivatetointernalsoAppDelegatecan call it on a freshScaffolderinstance (this is the "function-accessibility refactor" the ticket references — #490 has not yet landed in this branch).Test plan
corveilpath → click Reinstall skill → expect transient✓ Skill reinstalled;{devRoot}/.claude/commands/query-corveil.mdbyte-matches<corveilPath> skill show✗ <diagnostic>inline AND the orange warning banner at the top of General (existingcorveilSkillInstallWarningsurface)os.WriteFile+os.Chmod)Closes #491
🤖 Generated with Claude Code