Goal: flip Corveil from a stub TaskBackend to a real one, using the same template JiraTaskBackend already established. Crow's Provider enum, Session.codeProvider pairing, and capability-flag gating are already in place — this ticket just fills the concrete backend slot.
Depends on: four upstream CLI gaps being closed in radiusmethod/corveil so the Swift implementation can be straightforward (no caching, no URL synthesis, no fanout, no client-side filtering):
| Corveil ticket |
What it unlocks for this implementation |
corveil#1362 — @me assignee shortcut |
listAssigned/assign/createTask pass @me directly; no whoami cache |
corveil#1363 — url field in task JSON |
fetchTask reads task.url straight from the CLI output; no URL synthesis |
corveil#1364 — bulk fetch via --ids |
Declare the .batchedQuery capability; one HTTP request per poll |
corveil#1365 — filter by --external-ref |
PR-to-task reverse lookup for auto-close on PR merge |
This ticket assumes those four have landed. If they slip, the implementation gets uglier but not blocked — just adds the workarounds documented in those tickets.
Deliverables
1. CorveilTaskBackend.swift — real backend, mirroring JiraTaskBackend
New file: Packages/CrowProvider/Sources/CrowProvider/Backends/CorveilTaskBackend.swift
Replaces StubCorveilTaskBackend.swift (which can be removed once this lands — keep until the factory swap to avoid breaking the build). Same structure as JiraTaskBackend.swift (178 LOC); expect ~200 LOC.
Method-by-method CLI mapping:
| Protocol method |
corveil CLI call |
fetchTask(url:) |
parse <id> from URL → corveil task get <id> |
listAssigned(includeClosed:) |
corveil task list --assignee @me --status open (and --status closed if includeClosed) |
setLabels(url:add:remove:) |
corveil task update <id> --add-label X --remove-label Y (per label, or batched via repeat flag) |
setTaskStatus(url:status:) |
map TicketStatus → open/in_progress/closed → corveil task update <id> --status X |
assign(url:to:) |
corveil task update <id> --assignee user:<uuid> (or @me for self) |
createTask(repo:title:body:labels:) |
corveil task create --title X --description Y --label … --assignee @me |
provider: .corveil. capabilities: [.batchedQuery, .projectBoardStatus] — .batchedQuery because corveil#1364 lets us; .projectBoardStatus because corveil exposes the in_progress intermediate state.
Constructor takes a shellRunner: any ShellRunner (matches every other backend) so FakeShellRunner can mock CLI invocations in tests.
Discover the corveil binary via the same PATH-walk + defaults.binaries.corveil mechanism shipped in #485/#487/#489 — no new discovery logic.
2. WorkspaceInfo schema extension
File: Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift:271-345 (alongside jiraSite / jiraProjectKey / jiraJQL)
Add:
public var corveilHost: String? // e.g. "corveil.acme.io" — only needed for URL routing; CLI authenticates against its own configured host independently
Single field. The corveil CLI already manages its own auth state and target host via corveil login / CORVEIL_URL — Crow doesn't need to pipe those through. corveilHost exists only so URL detection in ProviderManager can route self-hosted Corveil URLs (e.g. corveil.mycompany.com/dashboard/tasks/42) to the right backend.
If unset, the existing corveil.io default in ProviderManager.detect() still routes the public corveil domain. Most users won't need to set this.
3. ProviderManager factory + URL detection
File: Packages/CrowProvider/Sources/CrowProvider/ProviderManager.swift
Two small changes:
- Line 25 (
taskBackend(for:)): replace the .corveil case from StubCorveilTaskBackend() to CorveilTaskBackend(shellRunner: ...).
- Lines 60-76 (
detect(url:)): extend the existing corveil.io check to also match any host configured via WorkspaceInfo.corveilHost (pass the additional-host list in the same way additionalGitLabHosts is handled today).
4. Status mapping (the only deliberate design decision)
Corveil's vocabulary: open / in_progress / closed.
Crow's TicketStatus: backlog / ready / inProgress / inReview / done.
Recommended mapping:
| Corveil → Crow |
Crow → Corveil |
open → .ready |
.backlog → open |
in_progress → .inProgress |
.ready → open |
closed → .done |
.inProgress → in_progress |
|
.inReview → in_progress (no Corveil intermediate distinguishes "in review") |
|
.done → closed |
Document this in a comment + ADR update so the asymmetry is captured. .inReview clamping to in_progress is the only lossy direction; Crow's setTaskStatus for .inReview on a Corveil task moves it to in_progress and the UI surfacing (project-board status capability) handles the visual distinction.
5. Workspace Settings UI
File: Packages/CrowUI/Sources/CrowUI/SettingsView.swift
Add a single field row for corveilHost in the workspace editor, mirroring the existing Jira workspace UI rows (jiraSite, etc.). Label: "Corveil host". Tooltip: "Only needed for self-hosted Corveil instances. Public corveil.io is auto-detected."
No Verify button — the existing Corveil CLI picker (defaults.binaries.corveil from #482/#483) handles binary verification, and corveil whoami is the auth probe (user runs it from a terminal).
6. ADR 0005 update
File: docs/adr/0005-task-and-code-backend-protocols.md
In the Decision table around line 56-60 (the "Concrete implementations" table), flip the Corveil row:
| Backend |
Provider |
Capabilities |
CorveilTaskBackend |
.corveil |
[batchedQuery, projectBoardStatus] |
Remove StubCorveilTaskBackend from the table. Note in the Consequences section that Corveil is now a real provider alongside Jira (both isTaskOnly: true, both paired with a CodeBackend via Session.codeProvider).
Bump the ADR's status if it's still Proposed (it shouldn't be — #411 merged), but verify and clean up if needed.
7. Tests
New file: Packages/CrowProvider/Tests/CrowProviderTests/CorveilTaskBackendTests.swift
Mirror JiraTaskBackendTests.swift structure. Each protocol method gets a test using FakeShellRunner that records the command vector and returns canned JSON.
Coverage:
- Each method's happy path (asserts the right
corveil task X command was built).
- Status mapping (each
TicketStatus ↔ corveil status round-trip).
- URL detection routing —
corveil.io/dashboard/tasks/42 and a configured self-hosted host both route to .corveil.
- Capability flags declared correctly.
Out of scope
- Corveil-side CLI improvements — those are the four
radiusmethod/corveil tickets (#1362-1365). Land them there.
- A new
crow-create-corveil-task skill. The existing /crow-create-ticket skill should grow a corveil branch (parallel to its existing github/gitlab/jira branches in setup.sh and SKILL.md). That's a sibling ticket against the crow/skills/crow-create-ticket/ source, not this one. Worth filing as a follow-up once this backend ships.
- Real-time webhook subscription for task state changes. Polling via Crow's existing
IssueTracker is sufficient for v1; webhooks are a separate ticket if/when needed.
- PR-merge → task-close auto-link. The mechanism (corveil#1365 + Crow's existing PR-watch path in
IssueTracker.swift) will be in place; wiring them is a small follow-up ticket once both sides land.
Acceptance
- A workspace configured with a Corveil task and any code provider (github/gitlab) opens a Crow coding session that tracks the Corveil task end-to-end:
- Session card displays the corveil task title/number/URL.
listAssigned on app launch fetches the user's open Corveil tasks in one batched --ids call (post-corveil#1364), populating the assigned-issue panel.
- Marking the session in-review via the project-board affordance calls
setTaskStatus(.inReview) which translates to corveil task update <id> --status in_progress.
- Completing the session calls
setTaskStatus(.done) → corveil task update <id> --status closed (or corveil task close <id> if that's preferred by the implementation).
- URL routing: pasting
https://corveil.io/dashboard/tasks/42 (or a self-hosted Corveil URL whose host matches workspace config) into the ticket-attach flow routes to CorveilTaskBackend.fetchTask and pulls the task metadata.
StubCorveilTaskBackend.swift is removed; references to it in ProviderManager.taskBackend(for: .corveil) flip to CorveilTaskBackend.
- ADR 0005's Corveil row reads "real backend with
[batchedQuery, projectBoardStatus] capabilities" rather than "stub that throws".
FakeShellRunner-based tests covering every method pass.
- No regressions to GitHub / GitLab / Jira backends — the changes are purely additive at the factory and
Provider enum boundary.
Critical files
| Purpose |
Path |
| New real backend |
Packages/CrowProvider/Sources/CrowProvider/Backends/CorveilTaskBackend.swift (new) |
| Stub to remove after flip |
Packages/CrowProvider/Sources/CrowProvider/Backends/StubCorveilTaskBackend.swift (delete after factory swap) |
| Factory + URL detection |
Packages/CrowProvider/Sources/CrowProvider/ProviderManager.swift:25,60-76 |
| Workspace schema |
Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift:271-345 |
| Template to mirror |
Packages/CrowProvider/Sources/CrowProvider/Backends/JiraTaskBackend.swift |
| Settings UI |
Packages/CrowUI/Sources/CrowUI/SettingsView.swift (Jira workspace row, around the jiraSite field) |
| ADR |
docs/adr/0005-task-and-code-backend-protocols.md |
| Tests |
Packages/CrowProvider/Tests/CrowProviderTests/CorveilTaskBackendTests.swift (new) |
Related
🐦⬛ Created with Crow via Claude Code
Goal: flip Corveil from a stub
TaskBackendto a real one, using the same templateJiraTaskBackendalready established. Crow'sProviderenum,Session.codeProviderpairing, and capability-flag gating are already in place — this ticket just fills the concrete backend slot.Depends on: four upstream CLI gaps being closed in radiusmethod/corveil so the Swift implementation can be straightforward (no caching, no URL synthesis, no fanout, no client-side filtering):
@meassignee shortcutlistAssigned/assign/createTaskpass@medirectly; nowhoamicacheurlfield in task JSONfetchTaskreadstask.urlstraight from the CLI output; no URL synthesis--ids.batchedQuerycapability; one HTTP request per poll--external-refThis ticket assumes those four have landed. If they slip, the implementation gets uglier but not blocked — just adds the workarounds documented in those tickets.
Deliverables
1.
CorveilTaskBackend.swift— real backend, mirroringJiraTaskBackendNew file:
Packages/CrowProvider/Sources/CrowProvider/Backends/CorveilTaskBackend.swiftReplaces
StubCorveilTaskBackend.swift(which can be removed once this lands — keep until the factory swap to avoid breaking the build). Same structure asJiraTaskBackend.swift(178 LOC); expect ~200 LOC.Method-by-method CLI mapping:
fetchTask(url:)<id>from URL →corveil task get <id>listAssigned(includeClosed:)corveil task list --assignee @me --status open(and--status closedifincludeClosed)setLabels(url:add:remove:)corveil task update <id> --add-label X --remove-label Y(per label, or batched via repeat flag)setTaskStatus(url:status:)TicketStatus→open/in_progress/closed→corveil task update <id> --status Xassign(url:to:)corveil task update <id> --assignee user:<uuid>(or@mefor self)createTask(repo:title:body:labels:)corveil task create --title X --description Y --label … --assignee @meprovider:.corveil.capabilities:[.batchedQuery, .projectBoardStatus]—.batchedQuerybecause corveil#1364 lets us;.projectBoardStatusbecause corveil exposes thein_progressintermediate state.Constructor takes a
shellRunner: any ShellRunner(matches every other backend) soFakeShellRunnercan mock CLI invocations in tests.Discover the
corveilbinary via the same PATH-walk +defaults.binaries.corveilmechanism shipped in #485/#487/#489 — no new discovery logic.2.
WorkspaceInfoschema extensionFile:
Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift:271-345(alongsidejiraSite/jiraProjectKey/jiraJQL)Add:
Single field. The corveil CLI already manages its own auth state and target host via
corveil login/CORVEIL_URL— Crow doesn't need to pipe those through.corveilHostexists only so URL detection inProviderManagercan route self-hosted Corveil URLs (e.g.corveil.mycompany.com/dashboard/tasks/42) to the right backend.If unset, the existing
corveil.iodefault inProviderManager.detect()still routes the public corveil domain. Most users won't need to set this.3.
ProviderManagerfactory + URL detectionFile:
Packages/CrowProvider/Sources/CrowProvider/ProviderManager.swiftTwo small changes:
taskBackend(for:)): replace the.corveilcase fromStubCorveilTaskBackend()toCorveilTaskBackend(shellRunner: ...).detect(url:)): extend the existingcorveil.iocheck to also match any host configured viaWorkspaceInfo.corveilHost(pass the additional-host list in the same wayadditionalGitLabHostsis handled today).4. Status mapping (the only deliberate design decision)
Corveil's vocabulary:
open/in_progress/closed.Crow's
TicketStatus:backlog/ready/inProgress/inReview/done.Recommended mapping:
open→.ready.backlog→openin_progress→.inProgress.ready→openclosed→.done.inProgress→in_progress.inReview→in_progress(no Corveil intermediate distinguishes "in review").done→closedDocument this in a comment + ADR update so the asymmetry is captured.
.inReviewclamping toin_progressis the only lossy direction; Crow'ssetTaskStatusfor.inReviewon a Corveil task moves it toin_progressand the UI surfacing (project-board status capability) handles the visual distinction.5. Workspace Settings UI
File:
Packages/CrowUI/Sources/CrowUI/SettingsView.swiftAdd a single field row for
corveilHostin the workspace editor, mirroring the existing Jira workspace UI rows (jiraSite, etc.). Label: "Corveil host". Tooltip: "Only needed for self-hosted Corveil instances. Public corveil.io is auto-detected."No Verify button — the existing Corveil CLI picker (
defaults.binaries.corveilfrom #482/#483) handles binary verification, andcorveil whoamiis the auth probe (user runs it from a terminal).6. ADR 0005 update
File:
docs/adr/0005-task-and-code-backend-protocols.mdIn the Decision table around line 56-60 (the "Concrete implementations" table), flip the Corveil row:
CorveilTaskBackend.corveil[batchedQuery, projectBoardStatus]Remove
StubCorveilTaskBackendfrom the table. Note in the Consequences section that Corveil is now a real provider alongside Jira (bothisTaskOnly: true, both paired with aCodeBackendviaSession.codeProvider).Bump the ADR's status if it's still
Proposed(it shouldn't be — #411 merged), but verify and clean up if needed.7. Tests
New file:
Packages/CrowProvider/Tests/CrowProviderTests/CorveilTaskBackendTests.swiftMirror
JiraTaskBackendTests.swiftstructure. Each protocol method gets a test usingFakeShellRunnerthat records the command vector and returns canned JSON.Coverage:
corveil task Xcommand was built).TicketStatus↔ corveil status round-trip).corveil.io/dashboard/tasks/42and a configured self-hosted host both route to.corveil.Out of scope
radiusmethod/corveiltickets (#1362-1365). Land them there.crow-create-corveil-taskskill. The existing/crow-create-ticketskill should grow acorveilbranch (parallel to its existing github/gitlab/jira branches in setup.sh and SKILL.md). That's a sibling ticket against thecrow/skills/crow-create-ticket/source, not this one. Worth filing as a follow-up once this backend ships.IssueTrackeris sufficient for v1; webhooks are a separate ticket if/when needed.IssueTracker.swift) will be in place; wiring them is a small follow-up ticket once both sides land.Acceptance
listAssignedon app launch fetches the user's open Corveil tasks in one batched--idscall (post-corveil#1364), populating the assigned-issue panel.setTaskStatus(.inReview)which translates tocorveil task update <id> --status in_progress.setTaskStatus(.done)→corveil task update <id> --status closed(orcorveil task close <id>if that's preferred by the implementation).https://corveil.io/dashboard/tasks/42(or a self-hosted Corveil URL whose host matches workspace config) into the ticket-attach flow routes toCorveilTaskBackend.fetchTaskand pulls the task metadata.StubCorveilTaskBackend.swiftis removed; references to it inProviderManager.taskBackend(for: .corveil)flip toCorveilTaskBackend.[batchedQuery, projectBoardStatus]capabilities" rather than "stub that throws".FakeShellRunner-based tests covering every method pass.Providerenum boundary.Critical files
Packages/CrowProvider/Sources/CrowProvider/Backends/CorveilTaskBackend.swift(new)Packages/CrowProvider/Sources/CrowProvider/Backends/StubCorveilTaskBackend.swift(delete after factory swap)Packages/CrowProvider/Sources/CrowProvider/ProviderManager.swift:25,60-76Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift:271-345Packages/CrowProvider/Sources/CrowProvider/Backends/JiraTaskBackend.swiftPackages/CrowUI/Sources/CrowUI/SettingsView.swift(Jira workspace row, around thejiraSitefield)docs/adr/0005-task-and-code-backend-protocols.mdPackages/CrowProvider/Tests/CrowProviderTests/CorveilTaskBackendTests.swift(new)Related
TaskBackendfoundation that made this swap possible.Complete TaskBackend/CodeBackend migrationticket. This work closes the "Corveil is still a stub" item from its acceptance list.🐦⬛ Created with Crow via Claude Code