Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions Packages/CrowClaude/Sources/CrowClaude/ClaudeLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,64 @@ public actor ClaudeLauncher {
lines.append("## Instructions")
lines.append("1. Study the ticket thoroughly — use dangerouslyDisableSandbox: true for ALL gh/glab commands")
lines.append("2. Create an implementation plan")
lines.append("3. Implement the plan")
lines.append("4. Commit the changes with a descriptive message")
lines.append("5. Push the branch to origin")

let ticketIsPR = ticketURL.map(Self.isPullRequestURL) ?? false
if ticketIsPR {
lines.append("6. The ticket is itself a pull/merge request — pushing the branch updates it; do not open a new one")
} else {
appendOpenPRStep(
to: &lines,
provider: provider,
ticketNumber: session.ticketNumber,
hasTicket: ticketURL != nil
)
}

return lines.joined(separator: "\n")
}

/// Append the final "open a PR/MR" instruction, branching on provider.
private func appendOpenPRStep(
to lines: inout [String],
provider: Provider?,
ticketNumber: Int?,
hasTicket: Bool
) {
let suffix = hasTicket ? " linked to the ticket" : ""
switch provider {
case .github:
lines.append("6. Open a pull request\(suffix):")
lines.append("")
lines.append("```bash")
if let n = ticketNumber {
lines.append("gh pr create --title \"<summary>\" --body \"Closes #\(n)\" --base main")
} else {
lines.append("gh pr create --fill --base main")
}
lines.append("```")
case .gitlab:
lines.append("6. Open a merge request\(suffix):")
lines.append("")
lines.append("```bash")
if let n = ticketNumber {
lines.append("glab mr create --title \"<summary>\" --description \"Closes #\(n)\" --target-branch main")
} else {
lines.append("glab mr create --fill --target-branch main")
}
lines.append("```")
case nil:
lines.append("6. Open a pull request\(suffix)")
}
}

/// True when the URL points at a pull/merge request rather than an issue.
private static func isPullRequestURL(_ url: String) -> Bool {
url.contains("/pull/") || url.contains("/merge_requests/")
}

/// Write prompt to temp file and return the launch command.
public func launchCommand(sessionID: UUID, worktreePath: String, prompt: String) throws -> String {
let tmpDir = FileManager.default.temporaryDirectory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Testing

@Test func generatePromptWithGitHubProvider() async {
let launcher = ClaudeLauncher()
let session = Session(name: "test-session")
let session = Session(name: "test-session", ticketNumber: 42)
let worktree = SessionWorktree(
sessionID: session.id,
repoName: "my-repo",
Expand All @@ -28,11 +28,17 @@ import Testing
#expect(prompt.contains("feature/42-cool"))
#expect(prompt.contains("gh issue view"))
#expect(prompt.contains("dangerouslyDisableSandbox"))
// Completion instructions: commit / push / open PR with ticket linkage
#expect(prompt.contains("Commit the changes"))
#expect(prompt.contains("Push the branch"))
#expect(prompt.contains("gh pr create"))
#expect(prompt.contains("Closes #42"))
#expect(!prompt.contains("glab mr create"))
}

@Test func generatePromptWithGitLabProvider() async {
let launcher = ClaudeLauncher()
let session = Session(name: "test-session")
let session = Session(name: "test-session", ticketNumber: 10)

let prompt = await launcher.generatePrompt(
session: session,
Expand All @@ -43,6 +49,10 @@ import Testing

#expect(prompt.contains("glab issue view"))
#expect(prompt.contains("dangerouslyDisableSandbox"))
#expect(prompt.contains("glab mr create"))
#expect(prompt.contains("Closes #10"))
#expect(prompt.contains("merge request"))
#expect(!prompt.contains("gh pr create"))
}

@Test func generatePromptWithNilProvider() async {
Expand All @@ -59,6 +69,10 @@ import Testing
#expect(prompt.contains("URL: https://example.com/ticket/1"))
#expect(!prompt.contains("gh issue"))
#expect(!prompt.contains("glab issue"))
// With an unknown provider, we must not emit a provider-specific CLI command
#expect(!prompt.contains("gh pr create"))
#expect(!prompt.contains("glab mr create"))
#expect(prompt.contains("Open a pull request"))
}

@Test func generatePromptWithNoTicket() async {
Expand All @@ -82,6 +96,10 @@ import Testing
#expect(prompt.hasPrefix("/plan"))
#expect(prompt.contains("| repo |"))
#expect(!prompt.contains("## Ticket"))
// Without a ticket, the PR step is still present (generic form)
#expect(prompt.contains("Push the branch"))
#expect(prompt.contains("Open a pull request"))
#expect(!prompt.contains("Closes #"))
}

@Test func generatePromptWithEmptyWorktrees() async {
Expand All @@ -99,6 +117,40 @@ import Testing
#expect(prompt.contains("## Instructions"))
}

@Test func generatePromptWhenTicketIsPullRequestGitHub() async {
let launcher = ClaudeLauncher()
let session = Session(name: "test-session", ticketNumber: 77)

let prompt = await launcher.generatePrompt(
session: session,
worktrees: [],
ticketURL: "https://github.com/org/repo/pull/77",
provider: .github
)

// When the ticket is already a PR, we must not instruct the agent to open a new one
#expect(!prompt.contains("gh pr create"))
#expect(!prompt.contains("Closes #"))
#expect(prompt.contains("Push the branch"))
#expect(prompt.contains("pushing the branch updates it"))
}

@Test func generatePromptWhenTicketIsMergeRequestGitLab() async {
let launcher = ClaudeLauncher()
let session = Session(name: "test-session", ticketNumber: 77)

let prompt = await launcher.generatePrompt(
session: session,
worktrees: [],
ticketURL: "https://gitlab.com/org/repo/-/merge_requests/77",
provider: .gitlab
)

#expect(!prompt.contains("glab mr create"))
#expect(!prompt.contains("Closes #"))
#expect(prompt.contains("pushing the branch updates it"))
}

// MARK: - launchCommand()

@Test func launchCommandWritesTempFile() async throws {
Expand Down
15 changes: 14 additions & 1 deletion skills/crow-workspace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,18 @@ gh issue view https://github.com/org/repo/issues/123 --comments
## Instructions
1. Study the ticket thoroughly — use dangerouslyDisableSandbox: true for ALL gh/glab commands
2. Create an implementation plan
3. Implement the plan
4. Commit the changes with a descriptive message
5. Push the branch to origin
6. Open a pull request linked to the ticket:

```bash
gh pr create --title "<summary>" --body "Closes #123" --base main
```
~~~

For GitLab tickets, substitute `glab mr create --title "<summary>" --description "Closes #{number}" --target-branch main` on step 6 (use "merge request" instead of "pull request"). When no ticket number is available, drop the body/description and fall back to `gh pr create --fill` / `glab mr create --fill`.

**When an existing PR was detected**, add this section to the prompt between `## Ticket` and `## Instructions`:

~~~markdown
Expand All @@ -346,9 +356,12 @@ And update the Instructions section to:
1. Review the existing PR and its changes — use dangerouslyDisableSandbox: true for ALL gh/glab commands
2. Study the ticket thoroughly
3. Create an implementation plan that builds on the existing work
4. Implement the plan
5. Commit the changes with a descriptive message
6. Push the branch — this updates the existing PR automatically; do NOT open a new one
~~~

For MyGitLab, add: `4. If any changes to my-project are required, create a new worktree with a feature branch before making modifications`
For MyGitLab, add: `7. If any changes to my-project are required, create a new worktree with a feature branch before making modifications`

### CLI Commands for Fetching Issues

Expand Down
Loading