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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ jobs:
- name: setup.sh set-ticket / worktree launch gate (CROW-1166 / CROW-1218)
run: bash skills/crow-workspace/setup_session_test.sh

- name: crow-cascade-epic wave planner / merge-gate watcher (CROW-1312)
run: bash skills/crow-cascade-epic/cascade_test.sh

# jq 1.6 is still what some Macs resolve first (Homebrew), and it rejects
# 1.7-only syntax such as a keyword used as an object key — which once
# made `watch` release an unmerged wave. The runner's jq is newer, so run
# the suite again under a pinned, checksum-verified 1.6.
- name: crow-cascade-epic under jq 1.6 (CROW-1312)
run: |
mkdir -p "$RUNNER_TEMP/jq16"
curl -fsSL -o "$RUNNER_TEMP/jq16/jq" https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64
echo "af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 $RUNNER_TEMP/jq16/jq" | sha256sum -c -
chmod +x "$RUNNER_TEMP/jq16/jq"
PATH="$RUNNER_TEMP/jq16:$PATH" jq --version
PATH="$RUNNER_TEMP/jq16:$PATH" bash skills/crow-cascade-epic/cascade_test.sh

- name: NSLog ban (CROW-874)
run: bash scripts/check-no-nslog.sh

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- CROW-1312 — **`/crow-cascade-epic <epic-url>` runs an epic as a dependency-gated cascade.** The new bundled skill reads the epic's sub-tickets and their dependencies (GitHub sub-issues and `blocked_by` links, the epic's task list and wave list, `Depends on` lines), layers them into waves, and starts one Crow work session per ticket of the current wave through `crow-workspace/setup.sh`. Sessions are created one at a time, each checked with `crow list-worktrees` (the #1301 race). Every coder prompt carries the cascade contract: one PR scoped to its ticket with `Closes #N`, a human reviewer requested (`--reviewer`, default `dgershman`), no self-approval, and the gate's label rule. With `--gate human`, the coder leaves `crow:merge` off and the reviewer merges by hand. With `--gate auto`, the coder adds `crow:merge` so Crow's auto-merge watcher merges on green. When `--gate` is omitted, the skill picks `auto` only if the base branch requires an approving review and the watcher is on. A background watcher (`cascade.sh watch`) releases the next wave only once every PR in the current wave has merged. It also reports PRs that break the contract, such as `crow:merge` added before a human approval. Re-running the skill resumes from live GitHub and Crow state without launching a ticket twice. `--dry-run` prints the wave map only, `--explore` runs a mapping pass over one wave, and `--wave <n>` starts at a later wave.
- CROW-1294 — **Drag extra Managers in the left sidebar to reorder them.** A grip on each extra Manager row (when there are two or more) drops that row above or below another extra Manager. The order is written into the stored session array, so a reload shows it. The nav pill stays the primary Manager (`is_primary_manager` / the well-known id), and a new extra Manager still appears at the end until it is moved. Jobs, Active, Reviews, In Review, and Completed are not part of the drag. Sidebar order in the session switcher lists Managers the same way. CLI: `crow reorder-manager --session <uuid> --before <uuid>` or `--after <uuid>`.

- CROW-1293 — **Delete on the Manager session header.** An extra Manager's header shows a red **Delete** immediately left of Reload, and immediately right of **Mark Scratch Done** when a linked Scratch item is still open. The click is the same confirm dialog and `delete-session` path as the row menu. The primary Manager still offers Delete; the daemon rejects it with "Cannot delete manager session" and the header surfaces that failure. Work and review headers are unchanged.
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ The crow CLI is safe for concurrent use. Multiple `crow` commands can run simult

Use `/crow-batch-workspace` to set up multiple workspaces in parallel.

Use `/crow-cascade-epic <epic-url>` to run an epic's sub-tickets as a dependency-gated cascade: one session per ticket, wave by wave, and the next wave starts only after the current wave's PRs merge. It creates each wave's sessions one at a time, not in a parallel batch.

## Fetching Ticket / PR Data

Claude Code permission allow-rules (`Bash(gh issue view:*)`, `Bash(gh api:*)`, `Bash(gh pr view:*)`, `Bash(git -C:*)`, …) are **prefix matches against the whole Bash command**. A compound invocation auto-approves only if **every** segment matches a rule — so one un-allowlisted segment (a `cd`, a `find`, an `echo` banner, a pipe into `head`) forces a permission prompt even though the `gh`/`git` part is allowlisted on its own.
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ parity:
@./scripts/check-no-nslog.sh
@echo "==> setup.sh set-ticket / worktree launch gate (CROW-1166 / CROW-1218)..."
@bash skills/crow-workspace/setup_session_test.sh
@echo "==> crow-cascade-epic wave planner / merge-gate watcher (CROW-1312)..."
@bash skills/crow-cascade-epic/cascade_test.sh
@echo "==> macos-sign-notarize helpers (CROW-1150)..."
@./scripts/macos-sign-notarize_test.sh
@echo "==> desktop sidecar helpers (CROW-1189)..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ struct LaunchScaffoldTests {
"skills/crow-review-pr/SKILL.md",
"skills/crow-batch-workspace/SKILL.md",
"skills/crow-create-ticket/SKILL.md",
"skills/crow-cascade-epic/SKILL.md",
"skills/crow-cascade-epic/cascade.sh",
"skills/crow-show-image/SKILL.md",
"skills/crow-attribution/FOOTER.md",
]
Expand Down Expand Up @@ -76,6 +78,22 @@ struct LaunchScaffoldTests {
#expect(refreshed.contains("crow-workspace"))
}

/// The Manager runs both skill scripts directly as well as via `bash`
/// (`settings.json` pre-approves both forms), so each lands executable —
/// including `cascade.sh`, the `/crow-cascade-epic` helper (CROW-1312).
@Test func skillScriptsAreExecutable() throws {
let devRoot = try Self.makeTempDevRoot()
defer { try? FileManager.default.removeItem(atPath: devRoot) }

LaunchScaffold.run(devRoot: devRoot, configured: true)

let claudeDir = (devRoot as NSString).appendingPathComponent(".claude")
for relative in ["skills/crow-workspace/setup.sh", "skills/crow-cascade-epic/cascade.sh"] {
let path = (claudeDir as NSString).appendingPathComponent(relative)
#expect(FileManager.default.isExecutableFile(atPath: path), "not executable: \(relative)")
}
}

/// `DaemonOptions.parse` falls back to the current working directory when
/// nothing is configured. Scaffolding that would scatter `.claude/skills/`
/// into whatever directory `crowd` was started from, so the unconfigured
Expand Down
52 changes: 52 additions & 0 deletions Packages/CrowEngine/Sources/CrowEngine/Scaffolder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public struct Scaffolder {
let createTicketSkillsDir = (claudeDir as NSString).appendingPathComponent("skills/crow-create-ticket")
try fm.createDirectory(atPath: createTicketSkillsDir, withIntermediateDirectories: true)

let cascadeSkillsDir = (claudeDir as NSString).appendingPathComponent("skills/crow-cascade-epic")
try fm.createDirectory(atPath: cascadeSkillsDir, withIntermediateDirectories: true)

let attributionSkillsDir = (claudeDir as NSString).appendingPathComponent("skills/crow-attribution")
try fm.createDirectory(atPath: attributionSkillsDir, withIntermediateDirectories: true)

Expand Down Expand Up @@ -138,6 +141,16 @@ public struct Scaffolder {
try CrowAttribution.expandSkillBody(createTicketSkillTemplate, agentKind: managerAgentKind)
.write(toFile: createTicketSkillPath, atomically: true, encoding: .utf8)

// Always overwrite the cascade-epic skill and its helper script
// (CROW-1312). The skill orchestrates crow-workspace/setup.sh; the
// script is its deterministic half (wave layering, merge-gate watch).
let cascadeSkillPath = (cascadeSkillsDir as NSString).appendingPathComponent("SKILL.md")
try CrowAttribution.expandSkillBody(Self.bundledCascadeSkill(), agentKind: managerAgentKind)
.write(toFile: cascadeSkillPath, atomically: true, encoding: .utf8)
let cascadeScriptPath = (cascadeSkillsDir as NSString).appendingPathComponent("cascade.sh")
try Self.bundledCascadeScript().write(toFile: cascadeScriptPath, atomically: true, encoding: .utf8)
try fm.setAttributes([.posixPermissions: 0o755], ofItemAtPath: cascadeScriptPath)

// Always overwrite the show-image skill (surfaces generated images in
// Crow's Images panel). No attribution expansion — it has no agent
// placeholders (CROW-593).
Expand Down Expand Up @@ -763,6 +776,43 @@ public struct Scaffolder {
"""
}

/// The crow-cascade-epic SKILL.md template bundled with the app (CROW-1312).
static func bundledCascadeSkill() -> String {
if let content = loadFromRepo("skills/crow-cascade-epic/SKILL.md") {
return content
}
if let url = Bundle.main.url(forResource: "crow-cascade-epic-SKILL.md", withExtension: "template"),
let content = try? String(contentsOf: url) {
return content
}
return """
# Crow Cascade Epic

## Activation
This skill activates when user invokes `/crow-cascade-epic <epic-url>` command.

## Important
Drives a dependency-gated epic cascade on top of `/crow-workspace`. All `crow`,
`gh`, `glab`, and `git worktree` commands require `dangerouslyDisableSandbox: true`.
"""
}

/// The crow-cascade-epic cascade.sh helper bundled with the app (CROW-1312).
static func bundledCascadeScript() -> String {
if let content = loadFromRepo("skills/crow-cascade-epic/cascade.sh") {
return content
}
if let url = Bundle.main.url(forResource: "crow-cascade-epic-cascade.sh", withExtension: "template"),
let content = try? String(contentsOf: url) {
return content
}
return """
#!/bin/bash
echo '{"status":"error","message":"cascade.sh not bundled"}'
exit 2
"""
}

/// Shared attribution footer instructions (issue #443).
static func bundledAttributionFooter() -> String {
if let content = loadFromRepo("skills/crow-attribution/FOOTER.md") {
Expand Down Expand Up @@ -791,6 +841,8 @@ public struct Scaffolder {
"Bash(crow *)",
"Bash(bash .claude/skills/crow-workspace/setup.sh *)",
"Bash(.claude/skills/crow-workspace/setup.sh *)",
"Bash(bash .claude/skills/crow-cascade-epic/cascade.sh *)",
"Bash(.claude/skills/crow-cascade-epic/cascade.sh *)",
"Bash(gh issue view:*)",
"Bash(gh issue create:*)",
"Bash(gh issue edit:*)",
Expand Down
2 changes: 2 additions & 0 deletions Resources/CLAUDE.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ The crow CLI is safe for concurrent use. Multiple `crow` commands can run simult

Use `/crow-batch-workspace` to set up multiple workspaces in parallel.

Use `/crow-cascade-epic <epic-url>` to run an epic's sub-tickets as a dependency-gated cascade: one session per ticket, wave by wave, and the next wave starts only after the current wave's PRs merge. It creates each wave's sessions one at a time, not in a parallel batch.

## Fetching Ticket / PR Data

Claude Code permission allow-rules (`Bash(gh issue view:*)`, `Bash(gh api:*)`, `Bash(gh pr view:*)`, `Bash(git -C:*)`, …) are **prefix matches against the whole Bash command**. A compound invocation auto-approves only if **every** segment matches a rule — so one un-allowlisted segment (a `cd`, a `find`, an `echo` banner, a pipe into `head`) forces a permission prompt even though the `gh`/`git` part is allowlisted on its own.
Expand Down
Loading
Loading