Skip to content

Settings UI to map Crow ticket states ↔ Jira workflow statuses (#523)#525

Merged
dhilgaertner merged 2 commits into
mainfrom
feature/crow-523-jira-status-mapping
Jun 18, 2026
Merged

Settings UI to map Crow ticket states ↔ Jira workflow statuses (#523)#525
dhilgaertner merged 2 commits into
mainfrom
feature/crow-523-jira-status-mapping

Conversation

@dhilgaertner

Copy link
Copy Markdown
Contributor

Closes #523. Follow-up to #522/#524 (the Atlassian MCP migration), rebased onto it.

Problem

Crow's Crow→Jira status mapping was hardcoded in JiraTaskBackend.jiraStatusName(for:) (.ready → "To Do"; everything else used the TicketStatus raw value). Jira workflow status names are configurable per project, so a project that renames a status (e.g. "In Development" instead of "In Progress") made Crow's transitions silently fail.

What

A per-workspace jiraStatusMap (Backlog / Ready / In Progress / In Review / Done → concrete Jira status names) persisted in config.json, edited in Settings → Workspaces → Jira Status Mapping, consulted by both status surfaces with a fallback to today's hardcoded defaults when unset.

How

  • ModelWorkspaceInfo.jiraStatusMap + JiraConfig.statusMap. The default table moves to TicketStatus.defaultJiraStatusName (CrowCore) as one source of truth shared by the UI (placeholders) and the backend (fallback).
  • In-app acli pathJiraTaskBackend.setTaskStatus consults the map; IssueTracker.markInReview resolves the matching workspace's map from the ticket's project key / site (acli is single-site, so only the map needs threading).
  • Agent MCP pathClaudeLauncher embeds a ## Jira Status Mapping block in Jira-session prompts, and the /crow-workspace skill (+ synced template) tells the agent to consult jiraStatusMap from config.json before transitionJiraIssue.
  • Settings UI — one field per pipeline status (placeholder = current default; blank = use default). Bonus: a "Fetch from Jira" button populates per-row dropdowns from the live workflow via GET /rest/api/3/project/{key}/statuses (new JiraStatusFetcher, reusing Replace acli with the official Atlassian Remote MCP Server for Jira (#522) #524's Atlassian email+token Basic auth). Degrades to free-text when no credential / site / project key is set. (acli has no list-transitions command and the in-app UI can't reach the agent MCP, so REST is the only viable in-app fetch.)
  • Docsdocs/configuration.md Jira section documents jiraStatusMap shape, fallback behavior, and the fetch affordance.

Tests

  • JiraTaskBackendTests — default table, map override, blank-entry fallback, setTaskStatus uses the mapped --status.
  • AppConfigTestsjiraStatusMap round-trip + nil-when-absent (back-compat).
  • ClaudeLauncherTests — mapping block present with a map, absent without one, never leaks on non-Jira sessions.
  • New JiraStatusFetcherTests — URL build, parse/dedupe, Basic-auth header, HTTP-error surface.
  • Green: CrowCore 295+, CrowProvider 50, CrowClaude 48, root 145, skill byte-identity guards 20. make app clean.

🤖 Generated with Claude Code

Jira workflow status names are configurable per project, so Crow's
hardcoded Crow→Jira mapping (.ready → "To Do", everything else verbatim)
made transitions silently fail for projects that renamed a status. Add a
per-workspace `jiraStatusMap` (Backlog/Ready/In Progress/In Review/Done →
concrete Jira status names) persisted in config.json, edited in
Settings → Workspaces, consulted by both status surfaces with a fallback
to the built-in defaults when unset.

- Model: `WorkspaceInfo.jiraStatusMap` + `JiraConfig.statusMap`; the
  default table moves to `TicketStatus.defaultJiraStatusName` (CrowCore)
  as the single source of truth shared by the UI and the backend.
- In-app acli path: `JiraTaskBackend.setTaskStatus` consults the map;
  `IssueTracker.markInReview` resolves the matching workspace's map by
  the ticket's project key / site.
- Agent MCP path: `ClaudeLauncher` embeds a "Jira Status Mapping" block
  in Jira-session prompts, and the `/crow-workspace` skill instructs the
  agent to consult `jiraStatusMap` before `transitionJiraIssue`.
- Settings UI: a per-status field (placeholder = current default) plus a
  bonus "Fetch from Jira" button that populates dropdowns from the live
  workflow via `GET /rest/api/3/project/{key}/statuses` (new
  `JiraStatusFetcher`, reusing #524's Atlassian credentials); degrades to
  free-text when no credential/site is configured.
- Tests (CrowCore/CrowProvider/CrowClaude) + docs/configuration.md.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: ACF93A34-A697-4DEF-B282-2734892C8AD2
@dhilgaertner dhilgaertner requested a review from dgershman as a code owner June 18, 2026 18:11
@dhilgaertner dhilgaertner added the crow:merge Crow auto-merge on green label Jun 18, 2026

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code & Security Review

Build: not run locally (swift build blocked on missing Frameworks/GhosttyKit.xcframework, env issue unrelated to this PR). Tests run: JiraStatusFetcherTests (8), JiraTaskBackendTests (20), ClaudeLauncherTests (18) all green. CI's Build & Test was still in progress at review time.

Critical Issues

None.

Security Review

Strengths:

  • JiraStatusFetcher keeps the Basic-auth header out of logs and uses the same resolver as the MCP path, so the secret never lands at rest. Test coverage confirms the header is set and HTTP errors surface as .http(code) instead of leaking response bodies.
  • Token resolution is delegated to AtlassianMCPResolver (which already gates on op availability) — no new secret-handling code paths.
  • Status-name input is user-controlled but consumed only as an acli --status arg / MCP transition arg; no shell composition or SQL.

Concerns (Yellow):

  • JiraStatusFetcher.statusesURL accepts http:// origins (Packages/CrowCore/Sources/CrowCore/JiraStatusFetcher.swift:27). The hasPrefix(\"http\") test treats anything starting with http as a full origin, so a workspace jiraSite typed as http://acme.atlassian.net will send the Atlassian Basic credential in cleartext. Operator-controlled input, but a one-line hasPrefix(\"https://\") guard (or auto-upgrade) closes the cleartext-credential foot-gun for free.

Code Quality

Yellow — should fix:

  1. ClaudeLauncher.jiraStatusMappingBlock is dead in production (Packages/CrowClaude/Sources/CrowClaude/ClaudeLauncher.swift:23,88,108). The new jiraStatusMap: parameter on ClaudeLauncher.generatePrompt is exercised only by unit tests. The protocol-facing wrapper ClaudeCodeAgent.generatePrompt (Packages/CrowClaude/Sources/CrowClaude/ClaudeCodeAgent.swift:89-104) does not pass jiraStatusMap (nor customInstructions) through to the launcher, and the CodingAgent protocol method doesn't include it either. So no production Jira session will see the ## Jira Status Mapping block — only the SKILL-driven path is live. This contradicts the PR description's "ClaudeLauncher embeds a ## Jira Status Mapping block in Jira-session prompts." Either thread jiraStatusMap through CodingAgent.generatePrompt + ClaudeCodeAgent (matches the design intent and gives belt-and-suspenders coverage), or drop the launcher-side block in favor of the already-shipping SKILL path (matches today's customInstructions story). Either way, the current state ships testable code that nothing actually calls.

  2. Prompt block doesn't restate the fallback rule (Packages/CrowClaude/Sources/CrowClaude/ClaudeLauncher.swift:107-122). The rendered block lists only mapped states (In Progress → In Development); an agent reading just the prompt — without consulting the SKILL — could read that as "these are the only valid transitions." SKILL.md covers the default-fallback rule, but the prompt block is the inline cue. One sentence (e.g., "Unlisted states use Crow's defaults: ReadyTo Do; others use the state name verbatim.") makes the block self-contained. (Moot if Yellow #1 is resolved by removing the launcher block.)

  3. IssueTracker.jiraStatusMap(forTicket:) host match is loose substring (Sources/Crow/App/IssueTracker.swift:2399-2403). ticketURL.contains(site) matches acme.atlassian.net against a workspace configured for dev.acme.atlassian.net (or vice versa, depending on enumeration order). The project-key path runs first and is exact, so this only bites when two Jira workspaces share an overlapping site fragment and the ticket isn't keyed to either project — but the workspace order is config-define, not URL-aware. Safer to parse the URL host (URL(string:)?.host) and compare host-equality (or anchor with ://\\(site)/ substring) rather than free substring.

Green — consider

  • Stale fetchedStatuses after edits (Packages/CrowUI/Sources/CrowUI/WorkspaceFormView.swift:33-35). The dropdown keeps the last-fetched names even after the operator edits jiraSite or jiraProjectKey, so a stale list can be silently applied to a different project. .onChange(of:) on those two bindings to clear fetchedStatuses + fetchStatusesError tightens the UX.
  • Dedup the trim/empty dance. JiraTaskBackend.jiraStatusName, WorkspaceFormView.statusMapForSave, and ClaudeLauncher.jiraStatusMappingBlock each implement the same "trim, drop blanks" rule. A tiny String?.nonBlank (or one normalizer on the model) would dedupe and make the contract single-sourced.
  • No request timeout on JiraStatusFetcher. Default URLSession.shared has a 60s resource timeout; for a click-driven UI affordance, an explicit 10–15s URLRequest.timeoutInterval would surface slow responses as a clear error instead of a long spinner. The work is in a detached task so it can't hang the UI, but the button can stay disabled a while.

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, 4 Yellow, 3 Green] findings. Yellow #1 is the biggest one: the PR description and the new tests both imply the launcher embeds the mapping in Jira-session prompts, but it doesn't reach production. Worth either wiring it through ClaudeCodeAgent (and the CodingAgent protocol) so the prompt block actually fires, or removing the launcher-side block and leaning entirely on the SKILL path — current state is half-implemented.


🐦‍⬛ Reviewed by Crow via Claude Code

…on (#523)

Review feedback from #525 (dgershman):

- Yellow #1/#2: the `ClaudeLauncher` `## Jira Status Mapping` prompt block
  was never wired through `CodingAgent`/`ClaudeCodeAgent`, so it was dead
  in production (same story as `customInstructions`). Drop the launcher
  block + its `jiraStatusMap` param and lean entirely on the live
  `/crow-workspace` SKILL path (which reads `jiraStatusMap` from
  config.json). Removed the now-stale SKILL/docs cross-reference to the
  prompt block.
- Yellow (security): `JiraStatusFetcher.statusesURL` now strips any
  user-supplied scheme and forces https, so a `jiraSite` typed as
  `http://…` can't send the Atlassian Basic credential in cleartext.
- Yellow #3: `IssueTracker.jiraStatusMap(forTicket:)` matches the
  workspace site by parsed-host equality instead of a loose substring, so
  `acme.atlassian.net` no longer matches `dev.acme.atlassian.net`.
- Green: clear stale `fetchedStatuses` on jiraSite/projectKey change; add
  a 15s request timeout to the fetcher; single-source the "trim, drop
  blanks" rule via `String.nonBlank` (CrowCore).

Tests updated (launcher block tests removed; http→https upgrade test
added). make app clean; CrowCore/CrowProvider/CrowClaude + root 165 green.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: ACF93A34-A697-4DEF-B282-2734892C8AD2
@dhilgaertner dhilgaertner requested a review from dgershman June 18, 2026 18:28

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code & Security Review

Second pass after the f2cd4b0 hardening commit. The earlier Yellow findings (dead ClaudeLauncher block, http→cleartext credential leak, loose substring host matching) are all addressed, and the new behavior is well-tested. No blockers.

Security Review

Strengths

  • JiraStatusFetcher.statusesURL now strips any user-supplied scheme and forces https://, with a regression test (statusesURLForcesHTTPSOnCleartextOrigin). Auth header is set on URLRequest (not embedded in URL) and constrained to a Basic value resolved from the already-vetted AtlassianMCPResolver.
  • IssueTracker.jiraStatusMap(forTicket:) now compares parsed hosts via caseInsensitiveCompare, so acme.atlassian.net no longer accidentally matches dev.acme.atlassian.net.
  • Click-driven JiraStatusFetcher.fetchStatusNames pins a 15s timeoutInterval and tunnels failures through a typed FetchError, so a slow workflow doesn't leave the button spinning indefinitely.
  • The off-main-actor Task.detached in SettingsView.fetchJiraStatuses is the right call — AtlassianMCPResolver.resolve may shell out to op read.

Concerns

  • None blocking. Consider items below.

Code Quality

  • Tests are thorough — URL build/encoding/HTTPS upgrade, parse/dedupe/empty/malformed, basic-auth header, HTTP error surface, JiraConfig.statusMap override + blank-fallback + setTaskStatus argv assertion, and JSON round-trip + missing-key back-compat. swift test --filter JiraStatusFetcherTests is green locally.
  • Default-table single-source-of-truth via TicketStatus.defaultJiraStatusName (CrowCore) is nice — the Settings placeholders, the JiraTaskBackend fallback, and the docs all converge on one definition.
  • String.nonBlank extension consolidates the "treat blank as unset" rule, which previously would have been re-implemented in statusMapForSave, jiraStatusName(for:), etc.
  • WorkspaceFormView clears fetchedStatuses on jiraSite / jiraProjectKey change so a stale list can't be applied to a different project — good.
  • WorkspaceFormView.fetchStatuses is optional with nil default, so the existing Setup Wizard call site stays source-compatible.

Considerations (Green)

  1. JiraStatusFetcher.statusesURL — userinfo edge case. A site typed as acme.atlassian.net@evil.com would build https://acme.atlassian.net@evil.com/…, whose parsed host is evil.com and userinfo is acme.atlassian.net. Hitting "Fetch from Jira" would then POST the Basic credential to the attacker host. Threat model is admin self-config (the user types their own site value), but the source comment already calls out credential-safety intent — a bareHost.contains("@") || bareHost.contains("/") reject would close the door cheaply and match that intent. (Packages/CrowCore/Sources/CrowCore/JiraStatusFetcher.swift:27)
  2. Project key encoding. addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) leaves / unencoded, so a stray project key with path separators could traverse to a different REST endpoint. Same admin-self-config caveat. Tightening to .alphanumerics plus - / _ would be safer-by-default. (JiraStatusFetcher.swift:32)
  3. Multi-Jira fallback heuristic. IssueTracker.jiraStatusMap(forTicket:) final clause returns the lone map when exactly one Jira workspace has a jiraStatusMap — even if other Jira workspaces exist without one and the ticket is from one of them. With multiple Jira sites this could apply the wrong project's renames. Consider returning nil whenever the ticket doesn't match by project-key or host and multiple Jira workspaces are configured (regardless of which have maps). (Sources/Crow/App/IssueTracker.swift:2410)
  4. Stale PR description. Description still says ClaudeLauncher embeds a ## Jira Status Mapping prompt block, but f2cd4b0 dropped that block in favor of the live /crow-workspace SKILL path. Worth updating so future readers don't hunt for code that isn't there.

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, 4 Green findings.

🐦‍⬛ Reviewed by Crow via Claude Code

@dhilgaertner dhilgaertner merged commit 31d01e5 into main Jun 18, 2026
2 checks passed
@dhilgaertner dhilgaertner deleted the feature/crow-523-jira-status-mapping branch June 18, 2026 18:37
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 UI to map Crow ticket states ↔ Jira workflow statuses

2 participants