diff --git a/README.md b/README.md index 47a4ba3ce..8f63d99b4 100644 --- a/README.md +++ b/README.md @@ -535,9 +535,15 @@ If you use [GitHub Agentic Workflows](https://github.blog/changelog/2025-05-19-g ### Install ```bash -gh aw add bradygaster/squad/workflows/squad.md@dev +gh aw add \ + bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad.md@dev gh aw compile -git add .github/workflows/ && git commit -m "Add Squad workflow" && git push +git add -- \ + .github/workflows/ \ + .gitattributes +git commit -m "Add Squad workflow" +git push ``` > `@dev` pulls the latest modes and fixes; switch to `@main` once gh-aw support is stable. @@ -552,6 +558,7 @@ git add .github/workflows/ && git commit -m "Add Squad workflow" && git push | `/squad cast-member ` | Add a team member | | `/squad retire ` | Remove a team member | | `/squad status` | Check current team | +| `/squad implement` | Implement an issue or dispatch ready tasks from an epic | ### Casting brief tip diff --git a/docs/src/content/docs/guide/gh-aw.md b/docs/src/content/docs/guide/gh-aw.md index 02be692b7..d29b641b7 100644 --- a/docs/src/content/docs/guide/gh-aw.md +++ b/docs/src/content/docs/guide/gh-aw.md @@ -26,14 +26,25 @@ gh api --method PUT repos/{owner}/{repo}/actions/permissions/workflow \ -f default_workflow_permissions=read \ -F can_approve_pull_request_reviews=true -# 3. Add the Squad workflow to your repo -gh aw add bradygaster/squad/workflows/squad.md@dev +# 3. Add the Squad workflows to your repo +gh aw add \ + bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad.md@dev -# 4. Compile the workflow to a lock file +# 4. Compile the workflows to lock files gh aw compile -# 5. Commit and push the changes -git add . +# 5. Commit and push the workflow sources and generated files +git add -- \ + .gitattributes \ + .github/aw/actions-lock.json \ + .github/workflows/squad.md \ + .github/workflows/squad.lock.yml \ + .github/workflows/squad-implement-worker.md \ + .github/workflows/squad-implement-worker.lock.yml \ + .github/workflows/shared/squad.md \ + .github/workflows/shared/planning-ontology.md \ + .github/workflows/shared/planning-policy.md git commit -m "ci: add Squad agentic workflow" git push @@ -79,12 +90,17 @@ a link for you to create the pull request manually. A manually created pull request is authored by your account, and GitHub does not allow authors to approve their own pull requests. -### Install the workflow +### Install the workflows ```bash -gh aw add bradygaster/squad/workflows/squad.md@dev +gh aw add \ + bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad.md@dev ``` +The single command installs the dedicated worker first, then the main workflow +that dispatches it. + :::note[Branch note] `@dev` pulls from the latest development branch where new modes and fixes land first. Stay on `@dev` to get improvements as they ship. Once gh-aw support reaches stable, you can switch to `@main` or drop the ref entirely for the default branch. ::: @@ -92,15 +108,15 @@ gh aw add bradygaster/squad/workflows/squad.md@dev This registers the Squad workflow in your repository's agentic workflow configuration. -### Compile to a lock file +### Compile to lock files ```bash gh aw compile ``` -Compiling resolves the workflow definition (including the shared bootstrap -component) into a deterministic `.github/workflows/squad.lock.yml` file. This -lock file is what GitHub Actions actually executes. +Compiling resolves both workflow definitions and their shared imports into +deterministic `.lock.yml` files. These lock files are what GitHub Actions +actually executes. ### Commit the workflow files @@ -109,8 +125,12 @@ git add -- \ .gitattributes \ .github/aw/actions-lock.json \ .github/workflows/squad.md \ + .github/workflows/squad.lock.yml \ + .github/workflows/squad-implement-worker.md \ + .github/workflows/squad-implement-worker.lock.yml \ .github/workflows/shared/squad.md \ - .github/workflows/squad.lock.yml + .github/workflows/shared/planning-ontology.md \ + .github/workflows/shared/planning-policy.md git commit -m "ci: add Squad agentic workflow" git push ``` @@ -200,6 +220,7 @@ PR review comment. | `/squad plan activate` | Create GitHub issues from an accepted plan | | `/squad plan activate phase {N}` | Create GitHub issues for only Phase N of the accepted plan | | `/squad plan revise ` | Revise the current plan based on your feedback | +| `/squad implement` | Implement an issue, or start the next ready wave of an epic | ### Where you can use slash commands @@ -604,6 +625,76 @@ next — no need to memorize the command sequence. --- +## Implementing issues + +After `/squad plan activate` creates the implementation backlog, run: + +``` +/squad implement +``` + +### Regular issues + +On a regular issue, Squad: + +1. Dispatches an isolated implementation worker +2. Checks that every issue listed in `Depends on:` is closed +3. Routes the work using the issue's `squad:{member}` label +4. Implements and validates the acceptance criteria +5. Opens a focused pull request that closes the issue + +The main Squad workflow retains its narrow cast and planning permissions. Only +the private implementation worker can edit repository files, and it delivers +changes through gh-aw's guarded `create-pull-request` safe output. + +### Epics + +On an epic, Squad finds its open child issues through native sub-issue +relationships and `Parent: #N` metadata. It then: + +1. Excludes tasks with open dependencies +2. Excludes tasks that already have an implementation pull request +3. Keeps up to three implementation pull requests active +4. Comments with the dispatched, blocked, existing, and deferred tasks + +Each worker creates its own branch and pull request. When one of those pull +requests merges, the existing worker resolves the parent epic and dispatches +the main Squad workflow in implement mode. Squad then dispatches enough ready +children to refill the three active slots. This continues until no open +children remain, without installing a third workflow. + +Both workflows accept gh-aw's propagated `aw_context` and allow the repository's +`github-actions[bot]` to pass the workflow-dispatch activation gate. Human +slash commands retain the normal repository-role checks. The merge relay +targets the repository's default branch so deleting a merged implementation +branch cannot prevent the continuation dispatch. + +For example, an epic with ten independent tasks starts three workers. Each merge +automatically starts one replacement, keeping three implementation pull requests +active until the final task is dispatched. Dependencies may temporarily reduce +the active count when no additional child is ready. + +`/squad implement` remains available as a manual recovery command if a run is +cancelled or an external change requires the epic to be reevaluated. + +:::note[Repository setting] +Pull-request delivery requires **Settings → Actions → General → Workflow +permissions → Allow GitHub Actions to create and approve pull requests**. +::: + +:::note[Pull request CI] +Pull requests created with the default `GITHUB_TOKEN` do not trigger other +workflow runs. Set `GH_AW_CI_TRIGGER_TOKEN` to a suitable fine-grained PAT if +implementation pull requests must start repository CI automatically. +::: + +:::note[Manual runs] +From **Actions → Squad → Run workflow**, enter `implement` as the command and +provide the target issue number. +::: + +--- + ## Connect vs. Adopt Squad supports two ways to bring in a team from another repository. @@ -761,7 +852,9 @@ are used. State does not persist across runs. To update your compiled workflow after pulling upstream changes: ```bash -gh aw add bradygaster/squad/workflows/squad.md@dev +gh aw add \ + bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad.md@dev ``` This re-compiles the workflow from source. If you have local customizations in your compiled `.github/workflows/squad-*.lock.yml`, they will be overwritten — keep customizations in the source `.md` files instead. @@ -785,6 +878,8 @@ gh aw compile | Universe is full on cast-member | All character names in the universe are allocated | Retire an unused member first, or re-cast with `/squad cast` | | "No plan found" on plan accept | No `/squad plan` comment exists yet | Run `/squad plan` first to generate a plan for review | | Plan accept creates fewer issues than expected | `create-issue` safe-output has a max of 30 | Split into multiple plan/accept cycles for very large decompositions | +| `/squad implement` cannot create a PR | Actions is not allowed to create pull requests | Enable **Allow GitHub Actions to create and approve pull requests** in repository Actions settings | +| Epic implementation dispatches no workers | Every child is blocked or already has an open implementation PR | Merge dependency PRs, then run `/squad implement` on the epic again | --- @@ -792,4 +887,5 @@ gh aw compile - [Squad README](https://github.com/bradygaster/squad#readme) — project overview and local CLI usage - [Workflow definition](https://github.com/bradygaster/squad/blob/dev/workflows/squad.md) — full slash command specification +- [Implementation worker](https://github.com/bradygaster/squad/blob/dev/workflows/squad-implement-worker.md) — isolated issue implementation workflow - [Bootstrap component](https://github.com/bradygaster/squad/blob/dev/workflows/shared/squad.md) — activation and artifact lifecycle diff --git a/test/gh-aw-implement-workflow.test.ts b/test/gh-aw-implement-workflow.test.ts new file mode 100644 index 000000000..fae6839c2 --- /dev/null +++ b/test/gh-aw-implement-workflow.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +function read(relativePath: string): string { + return readFileSync(resolve(ROOT, relativePath), 'utf-8'); +} + +describe('gh-aw implement workflows', () => { + const dispatcher = read('workflows/squad.md'); + const worker = read('workflows/squad-implement-worker.md'); + const guide = read('docs/src/content/docs/guide/gh-aw.md'); + + it('keeps repository editing isolated to the dispatch-only worker', () => { + expect(dispatcher).not.toMatch(/^tools:\r?\n\s+edit:/m); + expect(worker).toContain('private: false'); + expect(worker).not.toContain('slash_command:'); + expect(worker).toMatch(/^tools:\r?\n\s+edit:/m); + expect(worker).toContain('protected-files: request_review'); + }); + + it('bounds dispatch and serializes workers for the same issue', () => { + expect(dispatcher).toContain('bots: ["github-actions[bot]"]'); + expect(worker).toContain('bots: ["github-actions[bot]"]'); + expect(dispatcher).toContain('aw_context:'); + expect(worker).toContain('aw_context:'); + expect(dispatcher).toContain('workflows: [squad-implement-worker]'); + expect(dispatcher).toMatch(/dispatch-workflow:\r?\n\s+workflows: \[squad-implement-worker\]\r?\n\s+max: 3/); + expect(dispatcher).toContain('Never call the generic `dispatch_workflow` tool'); + expect(dispatcher).toContain('Never emit a dispatch without a'); + expect(worker).toContain( + 'group: "squad-implement-${{ github.event.inputs.issue_number || github.event.pull_request.number }}"', + ); + expect(worker).toContain('cancel-in-progress: false'); + }); + + it('continues epic execution after implementation PRs merge', () => { + expect(dispatcher).not.toMatch(/pull_request:\r?\n\s+types: \[closed\]/); + expect(worker).toMatch(/pull_request:\r?\n\s+types: \[closed\]/); + expect(worker).toContain("startsWith(github.event.pull_request.head.ref, 'squad/implement-')"); + expect(worker).toContain('workflows: [squad]'); + expect(worker).toContain('target-ref: ${{ github.event.repository.default_branch }}'); + expect(worker).toContain('"command": "implement"'); + expect(worker).toContain('Never call the generic `dispatch_workflow` tool'); + expect(dispatcher).toContain('available-slots = max(0, 3 - active-implementation-count)'); + expect(dispatcher).toContain('fills newly available slots'); + }); + + it('guards implementation branches, files, dependencies, and duplicate PRs', () => { + expect(worker).toContain('- "squad/implement-*"'); + expect(worker).not.toMatch(/allowed-files:\r?\n\s+- "\*"/); + expect(worker).toContain('Do not change `.github/workflows/`, `.github/agents/`, `.github/aw/`, or'); + expect(worker).toContain('blocker comment if any dependency remains open'); + expect(worker).toContain('Check for an existing open pull request'); + }); + + it('documents one-command installation in dependency order', () => { + const workerIndex = guide.indexOf('bradygaster/squad/workflows/squad-implement-worker.md@dev'); + const dispatcherIndex = guide.indexOf('bradygaster/squad/workflows/squad.md@dev'); + + expect(workerIndex).toBeGreaterThan(-1); + expect(dispatcherIndex).toBeGreaterThan(workerIndex); + expect(guide).toContain('The single command installs the dedicated worker first'); + }); +}); diff --git a/workflows/squad-implement-worker.md b/workflows/squad-implement-worker.md new file mode 100644 index 000000000..0111a06e2 --- /dev/null +++ b/workflows/squad-implement-worker.md @@ -0,0 +1,177 @@ +--- +name: Squad Implement Worker +run-name: "Squad implement — ${{ github.event.inputs.issue_number || github.event.pull_request.head.ref }}" +description: Implement one Squad issue or continue its parent epic after merge +private: false +on: + bots: ["github-actions[bot]"] + workflow_dispatch: + inputs: + issue_number: + description: Issue number to implement + required: true + type: string + aw_context: + description: Originating agentic workflow context + required: false + type: string + pull_request: + types: [closed] +if: >- + github.event_name != 'pull_request' || + (github.event.pull_request.merged == true && + startsWith(github.event.pull_request.head.ref, 'squad/implement-')) +permissions: + contents: read + copilot-requests: write + issues: read + pull-requests: read +concurrency: + group: "squad-implement-${{ github.event.inputs.issue_number || github.event.pull_request.number }}" + cancel-in-progress: false +network: + allowed: + - defaults +imports: + - shared/squad.md +tools: + edit: + bash: true + github: + mode: gh-proxy + toolsets: [default] +safe-outputs: + create-pull-request: + title-prefix: "[squad] " + labels: [squad] + max: 1 + allowed-base-branches: + - "squad/*" + allowed-branches: + - "squad/implement-*" + allowed-files: + - "*.c" + - "*.cc" + - "*.cpp" + - "*.cs" + - "*.css" + - "*.fs" + - "*.go" + - "*.html" + - "*.java" + - "*.js" + - "*.jsx" + - "*.json" + - "*.md" + - "*.mjs" + - "*.py" + - "*.rb" + - "*.rs" + - "*.sh" + - "*.toml" + - "*.ts" + - "*.tsx" + - "*.yaml" + - "*.yml" + - "Dockerfile*" + - "LICENSE*" + - "Makefile" + - "api/**" + - "app/**" + - "bin/**" + - "client/**" + - "cmd/**" + - "config/**" + - "docs/**" + - "examples/**" + - "internal/**" + - "lib/**" + - "packages/**" + - "public/**" + - "samples/**" + - "scripts/**" + - "server/**" + - "services/**" + - "src/**" + - "test/**" + - "tests/**" + - "tools/**" + - "web/**" + protected-files: request_review + max-patch-files: 500 + expires: 14d + add-comment: + max: 3 + target: "*" + dispatch-workflow: + workflows: [squad] + max: 1 + target-ref: ${{ github.event.repository.default_branch }} +--- + +# Squad Implementation Worker + +This workflow has two modes: + +1. A `workflow_dispatch` implements issue + `${{ github.event.inputs.issue_number }}` and opens a focused pull request. +2. A merged `pull_request` continues the parent epic. + +## Continue Parent Epic After Merge + +For a merged pull request: + +1. Extract the child issue number from the + `squad/implement-{issue-number}-` head branch. +2. Read the child issue and resolve its parent epic using the native parent + relationship, falling back to its `Parent: #N` body line. +3. If no parent epic exists, emit `noop` and stop. +4. Call the workflow-specific `squad` safe-output tool exactly once: + +```json +{ + "command": "implement", + "issue_number": "{parent-epic-number}" +} +``` + +Never call the generic `dispatch_workflow` tool. Never edit files or create a +pull request in this mode. Stop after the `squad` workflow is dispatched. + +The remaining instructions apply only to `workflow_dispatch`. + +## Gather Context + +1. Read the issue title, body, labels, state, and relevant comments. +2. Stop with a comment if the issue is closed. +3. Parse its `Depends on:` line. Check every referenced issue and stop with a + blocker comment if any dependency remains open. +4. Check for an existing open pull request whose branch starts with + `squad/implement-${{ github.event.inputs.issue_number }}-` or whose body + closes this issue. If one exists, comment with its URL and stop. +5. Read `.squad/team.md` and `.squad/routing.md`. Route work to the member named + by the `squad:{member}` label, or let the Lead choose specialists. + +## Implement + +1. Inspect the repository and implement the smallest complete change satisfying + every acceptance criterion. +2. Use the routed Squad specialists for design, implementation, tests, and + review. Keep delegation bounded to this issue. +3. Do not change `.github/workflows/`, `.github/agents/`, `.github/aw/`, or + `.squad/`. +4. Run the smallest existing build, test, and lint commands covering the change. +5. Review the final diff against the issue acceptance criteria. + +## Open Pull Request + +Use the `create-pull-request` safe-output: + +- Branch: `squad/implement-${{ github.event.inputs.issue_number }}-{short-slug}` +- Title: `Implement #${{ github.event.inputs.issue_number }}: {issue-title}` +- Body: summarize implementation and validation, including + `Closes #${{ github.event.inputs.issue_number }}`. +- Files: include only files required for this issue. + +If the repository already satisfies the issue, comment with evidence and do not +create an empty pull request. diff --git a/workflows/squad.md b/workflows/squad.md index 36860d66f..7fe5e73b9 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -5,6 +5,7 @@ description: Cast, connect, or adopt a Squad AI team for your repository emoji: "🤖" private: false on: + bots: ["github-actions[bot]"] slash_command: name: squad events: @@ -17,6 +18,14 @@ on: description: 'Squad command (e.g., cast, connect org/repo, adopt org/repo, status)' required: false default: 'cast' + issue_number: + description: 'Issue number to implement when run manually' + required: false + type: string + aw_context: + description: 'Originating agentic workflow context' + required: false + type: string permissions: contents: read copilot-requests: write @@ -53,6 +62,10 @@ safe-outputs: max: 75 add-comment: max: 20 + target: "*" + dispatch-workflow: + workflows: [squad-implement-worker] + max: 3 --- ## Comment Search Rules (all modes) @@ -67,7 +80,19 @@ safe-outputs: Read slash command from: `github.event.comment.body` (issue/PR comment), `github.event.issue.body` (issue body), or `github.event.inputs.command` (workflow_dispatch, default: `cast`). -The activation job ran `squad init --preset default` producing generic `.squad/` scaffolding. Cast mode REPLACES this. This workflow never creates/modifies `.github/workflows/`. +- **Issue body:** `github.event.issue.body` — the full issue description +- **Issue comment:** `github.event.comment.body` — the full comment text +- **PR review comment:** `github.event.comment.body` — the full comment text +- **Workflow dispatch:** `github.event.inputs.command` — manual input (default: `cast`) +- **Manual issue target:** `github.event.inputs.issue_number` — issue number for + `/squad implement` runs started from the Actions tab + +The activation job already ran `squad init --preset default`, which produced a +generic 5-agent team (lead, reviewer, devrel, security, docs) in `.squad/`. Cast +mode REPLACES this scaffolding with a team tailored to the repository. + +This workflow does not create or modify files under `.github/workflows/`. +Repository owners must configure Copilot setup steps separately when needed. ## Modes @@ -95,6 +120,7 @@ The activation job ran `squad init --preset default` producing generic `.squad/` | `/squad plan accept implementation phase {N}` | Plan Accept Implementation | | `/squad plan activate` | Plan Activate | | `/squad plan activate phase {N}` | Plan Activate | +| `/squad implement` | Implement | | `/squad` (no args) | Cast | ## Parse Command @@ -104,7 +130,7 @@ The activation job ran `squad init --preset default` producing generic `.squad/` 3. Match **longest-prefix-first**: - `plan accept implementation` (3), `plan accept scope` (3), `plan program revise` (3) - `plan implementation` (2), `plan program` (2), `plan activate` (2), `plan validate` (2), `plan accept` (2), `plan revise` (2), `triage revise` (2) - - `cast-member` (1), `plan` (1), `cast`, `connect`, `adopt`, `retire`, `status`, `research`, `triage` + - `cast-member` (1), `plan` (1), `cast`, `connect`, `adopt`, `retire`, `status`, `research`, `triage`, `implement` 4. Default to `cast` if empty. 5. **Phase selector:** If remaining args contain `phase {N}`, extract N. @@ -285,6 +311,73 @@ Read-only team composition report. --- +#### Implement Mode + +Implement mode dispatches an isolated implementation worker for a regular issue. +When invoked on an epic, it dispatches workers for up to three currently +unblocked children. The worker relays merged implementation pull requests back +to this mode so it can automatically refill the parent epic's available slots. + +**Acknowledge:** Post `🤖 Squad is preparing implementation…` using the +`add-comment` safe-output. + +##### Step 1: Validate and Gather Context + +1. Resolve the target issue number from `github.event.inputs.issue_number` for a + workflow dispatch, otherwise from the triggering issue. If invoked from a + pull request review comment, explain that `/squad implement` must be run from + the target issue. +2. Read the target issue title, body, labels, state, and relevant comments. +3. Find open child issues using native GitHub sub-issue relationships. Also + include open issues whose body contains a + `Parent: #{target-issue-number}` line for compatibility with older plans. +4. If child issues exist, treat the target as an epic and follow the Epic + Dispatch procedure below. Do not implement the epic body directly. +5. If no child issues exist, call the workflow-specific + `squad_implement_worker` safe-output tool with `issue_number` set to the + target issue number. +6. Post a comment linking the dispatched worker run. The worker performs + dependency, duplicate pull request, routing, implementation, and validation + checks. + +##### Epic Dispatch + +For each open child issue: + +1. Parse its `Depends on:` line and check the state of every referenced issue. +2. Exclude children with any open dependency. +3. Find children that already have an open pull request whose branch starts + with `squad/implement-{child-number}-` or whose body closes that child. + These are active implementation children. +4. Calculate `available-slots = max(0, 3 - active-implementation-count)`. +5. Exclude active implementation children from the ready set. +6. Sort ready children by issue number and select at most `available-slots`. + +For each selected child, call the workflow-specific `squad_implement_worker` +safe-output tool with this input: + +```json +{ + "issue_number": "{child-issue-number}" +} +``` + +Never call the generic `dispatch_workflow` tool. Never emit a dispatch without a +non-empty numeric `issue_number`. Emit exactly one workflow-specific dispatch +per selected child, and only report a child as dispatched after the tool returns +success. + +Post a comment on the epic listing the dispatched children, blocked children, +children with existing implementation pull requests, and any ready children +deferred because all three slots are occupied. If no child is ready or no slot +is available, post the status summary and do not dispatch a workflow. + +After each implementation pull request merges, this workflow runs again and +fills newly available slots. Continue until the epic has no open children. +`/squad implement` remains available as a manual recovery command. + +--- + #### Research Mode Deep analysis → structured findings comment. Read-only + comment. Works on open/closed issues.