From ea62e91f667c615c7390dcf9aa3fb4b5e0ee368b Mon Sep 17 00:00:00 2001 From: brady gaster Date: Tue, 25 Aug 2026 16:54:19 -0700 Subject: [PATCH 1/2] feat(workflows): route dependency tasks to deps worker Refs #1748 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/fixtures/gh-aw-deps-routing.json | 50 +++++ test/gh-aw-deps-routing.test.ts | 250 ++++++++++++++++++++++++ test/gh-aw-deps-worker-workflow.test.ts | 4 +- workflows/squad-deps-worker.md | 36 +++- workflows/squad.md | 84 ++++++-- 5 files changed, 392 insertions(+), 32 deletions(-) create mode 100644 test/fixtures/gh-aw-deps-routing.json create mode 100644 test/gh-aw-deps-routing.test.ts diff --git a/test/fixtures/gh-aw-deps-routing.json b/test/fixtures/gh-aw-deps-routing.json new file mode 100644 index 000000000..3cfcd7883 --- /dev/null +++ b/test/fixtures/gh-aw-deps-routing.json @@ -0,0 +1,50 @@ +[ + { + "name": "explicit npm dependency addition", + "title": "Add lodash as a runtime dependency", + "body": "Add lodash 4.17.21 and regenerate the npm lockfile.", + "requiredFiles": ["package.json", "package-lock.json"], + "config": "{\"version\":1}", + "expectedWorkflow": "squad-deps-worker" + }, + { + "name": "ordinary source task with dependency relationship", + "title": "Fix login validation", + "body": "Depends on: #41. Correct the validator without changing packages.", + "requiredFiles": ["src/login.ts", "test/login.test.ts"], + "config": "{\"version\":1}", + "expectedWorkflow": "squad-implement-worker" + }, + { + "name": "mixed dependency and source task", + "title": "Add Serilog and wire request logging", + "body": "Add the package, configure the request pipeline, and add tests.", + "requiredFiles": ["Directory.Packages.props", "src/App/Program.cs", "test/App.Tests.cs"], + "config": "{\"squadDeps\":\"allow\"}", + "expectedWorkflow": "squad-implement-worker" + }, + { + "name": "unsupported dependency ecosystem", + "title": "Add pytest", + "body": "Add pytest to the Python development dependencies.", + "requiredFiles": ["pyproject.toml"], + "config": "{\"squadDeps\":\"allow\"}", + "expectedWorkflow": "squad-implement-worker" + }, + { + "name": "dependency addition denied by repository policy", + "title": "Add zod", + "body": "Add zod and regenerate package-lock.json.", + "requiredFiles": ["package.json", "package-lock.json"], + "config": "{\"squadDeps\":\"deny\"}", + "expectedWorkflow": "denied" + }, + { + "name": "dependency addition denied by malformed config", + "title": "Add testify", + "body": "Add testify to the Go module.", + "requiredFiles": ["go.mod", "go.sum"], + "config": "{\"squadDeps\":", + "expectedWorkflow": "denied" + } +] diff --git a/test/gh-aw-deps-routing.test.ts b/test/gh-aw-deps-routing.test.ts new file mode 100644 index 000000000..efebdbab2 --- /dev/null +++ b/test/gh-aw-deps-routing.test.ts @@ -0,0 +1,250 @@ +import { afterAll, describe, expect, it } from 'vitest'; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import { basename, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const WAVE_1_BASENAMES = new Set([ + 'package.json', + 'package-lock.json', + 'npm-shrinkwrap.json', + 'yarn.lock', + 'pnpm-lock.yaml', + 'Directory.Packages.props', + 'go.mod', + 'go.sum', +]); + +interface RoutingFixture { + name: string; + title: string; + body: string; + requiredFiles: string[]; + config: string; + expectedWorkflow: 'squad-deps-worker' | 'squad-implement-worker' | 'denied'; +} + +function read(relativePath: string): string { + return readFileSync(resolve(ROOT, relativePath), 'utf8'); +} + +function frontmatter(markdown: string): string { + return markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? ''; +} + +function yamlBlock(yaml: string, key: string): string { + const lines = yaml.split(/\r?\n/); + const keyPattern = new RegExp( + `^(\\s*)${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.*)$`, + ); + const start = lines.findIndex(line => keyPattern.test(line)); + if (start === -1) return ''; + + const indent = lines[start].match(keyPattern)![1].length; + const block = [lines[start]]; + for (let index = start + 1; index < lines.length; index++) { + const line = lines[index]; + if (line.trim() !== '' && line.search(/\S/) <= indent) break; + block.push(line); + } + return block.join('\n'); +} + +function listInBlock(block: string, key: string): string[] { + const inline = block.match( + new RegExp( + `^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*\\[(.*)\\]\\s*$`, + 'm', + ), + ); + if (inline) { + return inline[1] + .split(',') + .map(item => item.trim().replace(/^['"]|['"]$/g, '')) + .filter(Boolean); + } + return []; +} + +function explicitDependencyIntent(title: string, body: string): boolean { + const task = `${title}\n${body}`.toLowerCase(); + return ( + /\b(add|install|remove|uninstall|update|upgrade|bump)\b[\s\S]*\b(package|dependency|dependencies|module|lockfile)\b/.test( + task, + ) || + /\bregenerate\b[\s\S]*\blockfile\b/.test(task) || + /\badd\b[\s\S]*\b(package\.json|package-lock\.json|go\.mod|go\.sum|directory\.packages\.props)\b/.test( + task, + ) + ); +} + +function dependencyConfigAllows(configText: string): boolean { + let config: unknown; + try { + config = JSON.parse(configText); + } catch { + return false; + } + if (config === null || typeof config !== 'object' || Array.isArray(config)) return false; + const object = config as Record; + if (!Object.hasOwn(object, 'squadDeps')) return true; + return object.squadDeps === 'allow'; +} + +function routeFixture(fixture: RoutingFixture): RoutingFixture['expectedWorkflow'] { + const dependencyOnly = + fixture.requiredFiles.length > 0 && + fixture.requiredFiles.every(file => WAVE_1_BASENAMES.has(basename(file))); + if (!explicitDependencyIntent(fixture.title, fixture.body) || !dependencyOnly) { + return 'squad-implement-worker'; + } + return dependencyConfigAllows(fixture.config) ? 'squad-deps-worker' : 'denied'; +} + +const compileWorkspaces: string[] = []; + +afterAll(() => { + for (const workspace of compileWorkspaces) { + rmSync(workspace, { recursive: true, force: true }); + } +}); + +function compileSafeOutputs(workflowId: string): Record> { + const workspace = mkdtempSync(resolve(tmpdir(), `${workflowId}-routing-contract-`)); + compileWorkspaces.push(workspace); + const workflowDir = resolve(workspace, '.github', 'workflows'); + mkdirSync(workflowDir, { recursive: true }); + cpSync(resolve(ROOT, 'workflows'), workflowDir, { recursive: true }); + execFileSync('git', ['init', '--quiet'], { cwd: workspace }); + execFileSync( + 'gh', + ['aw', 'compile', workflowId, '--strict', '--no-check-update'], + { cwd: workspace, encoding: 'utf8', stdio: 'pipe' }, + ); + + const compiled = readFileSync(resolve(workflowDir, `${workflowId}.lock.yml`), 'utf8'); + const lines = compiled.split(/\r?\n/); + const configStart = lines.findIndex( + line => line.includes('/safeoutputs/config.json') && line.includes('<<'), + ); + const delimiter = lines[configStart]?.match(/<< '([^']+)'/)?.[1]; + const configEnd = delimiter + ? lines.findIndex((line, index) => index > configStart && line.trim() === delimiter) + : -1; + + expect(configStart, `${workflowId} must compile safe-output config`).toBeGreaterThanOrEqual(0); + expect(delimiter, `${workflowId} safe-output delimiter must be present`).toBeDefined(); + expect(configEnd, `${workflowId} safe-output config must terminate`).toBeGreaterThan(configStart); + return JSON.parse(lines.slice(configStart + 1, configEnd).join('\n')) as Record< + string, + Record + >; +} + +describe('gh-aw dependency dispatcher routing (#1748 S3)', () => { + const dispatcher = read('workflows/squad.md'); + const depsWorker = read('workflows/squad-deps-worker.md'); + const generalWorker = read('workflows/squad-implement-worker.md'); + const fixtures = JSON.parse( + read('test/fixtures/gh-aw-deps-routing.json'), + ) as RoutingFixture[]; + const dispatcherTargets = listInBlock( + yamlBlock(frontmatter(dispatcher), 'dispatch-workflow'), + 'workflows', + ); + + it('wires both workers and keeps the dependency route conservative', () => { + expect(dispatcherTargets).toEqual( + expect.arrayContaining(['squad-implement-worker', 'squad-deps-worker']), + ); + expect(dispatcher).toContain('Choose `squad_deps_worker` only when **all**'); + expect(dispatcher).toContain( + 'Choose `squad_implement_worker` for every other task.', + ); + expect(dispatcher).toContain('Never call both workers for one issue.'); + expect(dispatcher).toContain( + 'do not reroute it to the general worker', + ); + }); + + it.each(fixtures)('fixture: $name -> $expectedWorkflow', fixture => { + const routed = routeFixture(fixture); + expect(routed).toBe(fixture.expectedWorkflow); + if (routed !== 'denied') { + expect(dispatcherTargets).toContain(routed); + const toolName = routed.replaceAll('-', '_'); + expect(dispatcher).toContain(`\`${toolName}\``); + } + }); + + it('fails closed for every unrecognized squadDeps value', () => { + for (const denied of [ + '{"squadDeps":"ALLOW"}', + '{"squadDeps":"unexpected"}', + '{"squadDeps":true}', + '{"squadDeps":1}', + '{"squadDeps":null}', + '{"squadDeps":[]}', + '{"squadDeps":{}}', + '[]', + 'not-json', + ]) { + expect(dependencyConfigAllows(denied), denied).toBe(false); + } + expect(dependencyConfigAllows('{}')).toBe(true); + expect(dependencyConfigAllows('{"squadDeps":"allow"}')).toBe(true); + expect(dependencyConfigAllows('{"squadDeps":"deny"}')).toBe(false); + expect(dispatcher).toContain('Every other value, including any other string'); + expect(depsWorker).toContain('Any other value -- including another string'); + }); + + it( + 'strict-compiles dispatcher targets and preserves dependency/general file boundaries', + () => { + const dispatcherSafeOutputs = compileSafeOutputs('squad'); + const depsSafeOutputs = compileSafeOutputs('squad-deps-worker'); + const generalSafeOutputs = compileSafeOutputs('squad-implement-worker'); + const dispatch = dispatcherSafeOutputs.dispatch_workflow; + const depsPullRequest = depsSafeOutputs.create_pull_request; + const generalPullRequest = generalSafeOutputs.create_pull_request; + + expect(dispatch.aw_context_workflows).toEqual( + expect.arrayContaining([ + 'squad-implement-worker', + 'squad-deps-worker', + ]), + ); + + const depsAllowed = depsPullRequest.allowed_files as string[]; + const depsExcluded = depsPullRequest.excluded_files as string[]; + const depsProtected = depsPullRequest.protected_files as string[]; + const generalProtected = generalPullRequest.protected_files as string[]; + expect(depsAllowed).toEqual( + expect.arrayContaining(['package.json', 'package-lock.json', 'go.mod', 'go.sum']), + ); + expect(depsExcluded).toEqual( + expect.arrayContaining([ + 'node_modules/**', + 'vendor/**', + '.github/workflows/**', + '.squad/**', + ]), + ); + expect(depsProtected).not.toContain('package.json'); + expect(depsProtected).toContain('NuGet.Config'); + expect(generalProtected).toContain('package.json'); + expect(generalProtected).toContain('go.mod'); + }, + 30000, + ); + + it('keeps the general worker structurally unchanged', () => { + const protectedFiles = yamlBlock(frontmatter(generalWorker), 'protected-files'); + expect(protectedFiles).toContain('- README.md'); + expect(protectedFiles).not.toContain('- package.json'); + expect(protectedFiles).not.toContain('- go.mod'); + }); +}); diff --git a/test/gh-aw-deps-worker-workflow.test.ts b/test/gh-aw-deps-worker-workflow.test.ts index 4d1243154..a64c07bcd 100644 --- a/test/gh-aw-deps-worker-workflow.test.ts +++ b/test/gh-aw-deps-worker-workflow.test.ts @@ -353,7 +353,7 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', ( }); // ── S1 guards preserved (existing behavior) ─────────────────────────────── - it('is a standalone workflow_dispatch worker, not yet wired into the dispatcher', () => { + it('is a standalone workflow_dispatch worker wired only through the dispatcher', () => { expect(depsWorkerFrontmatter).toMatch(/^on:\r?\n\s+bots: \["github-actions\[bot\]"\]\r?\n\s+workflow_dispatch:/m); expect(depsWorker).not.toContain('slash_command:'); expect(depsWorkerFrontmatter).toContain('issue_number:'); @@ -361,7 +361,7 @@ describe('gh-aw squad-deps-worker S2: Wave 1 protected-files.exclude (#1748)', ( expect(depsWorker).toMatch(/^tools:\r?\n\s+edit:/m); const dispatcherDispatch = yamlBlock(dispatcherFrontmatter, 'dispatch-workflow'); - expect(listInBlock(dispatcherDispatch, 'workflows')).not.toContain('squad-deps-worker'); + expect(listInBlock(dispatcherDispatch, 'workflows')).toContain('squad-deps-worker'); }); it('declares Wave 1 extensionless manifest/lockfile basenames in allowed-files', () => { diff --git a/workflows/squad-deps-worker.md b/workflows/squad-deps-worker.md index 59542942a..730f29e3c 100644 --- a/workflows/squad-deps-worker.md +++ b/workflows/squad-deps-worker.md @@ -140,23 +140,39 @@ so that dependency-manifest authority never leaks into the general `squad-implement-worker` path: that worker's `protected-files` carries no manifest exclusions and is unchanged by this workflow's existence. -This slice (S2) adds Wave 1 `protected-files.exclude` entries to the -dependency worker. The Wave 1 basenames (`package.json`, `package-lock.json`, -`yarn.lock`, `pnpm-lock.yaml`, `npm-shrinkwrap.json`, -`Directory.Packages.props`, `go.mod`, `go.sum`) are now excluded from -`protected-files`, so the agent can produce a signed PR for those files. -Registry/install config, SDK/tool pins, and governance docs remain protected. -The `squadDeps` opt-out guard and `dependency-change` PR presentation rules -are separate follow-up slices (S3+). +The Wave 1 basenames (`package.json`, `package-lock.json`, `yarn.lock`, +`pnpm-lock.yaml`, `npm-shrinkwrap.json`, `Directory.Packages.props`, `go.mod`, +`go.sum`) are excluded from `protected-files`, so the agent can produce a +signed PR for those files. Registry/install config, SDK/tool pins, and +governance docs remain protected. The dispatcher routes only explicit, +dependency-only Wave 1 work here, and this worker independently enforces the +`squadDeps` opt-out guard before editing. ## Gather Context 1. Read the issue title, body, labels, state, and relevant comments. 2. Stop with a comment if the issue is closed. -3. Check for an existing open pull request whose branch starts with +3. DEPENDENCY CHANGE GUARD. Before editing any file, read + `.squad/config.json` and apply this exact schema: + - The file must be readable, valid JSON, and a top-level object. If it is + missing, unreadable, malformed, or not an object, post a comment stating + that dependency changes are denied because the config is unreadable or + invalid, then stop. + - If the `squadDeps` key is absent, allow (default-on). + - If `squadDeps` is the exact string `"allow"`, allow. + - If `squadDeps` is the exact string `"deny"`, post a comment citing + `.squad/config.json squadDeps: "deny"`, then stop. + - Any other value -- including another string, boolean, number, `null`, + array, or object -- is unrecognized. Post a comment stating that + dependency changes are denied because `squadDeps` is unrecognized, then + stop. + Never infer this setting from the issue body or comments. This prompt guard + does not alter the compiled exclusions; it prevents both dispatcher-launched + and direct human `workflow_dispatch` runs from proceeding when denied. +4. Check for an existing open pull request whose branch starts with `squad/deps-${{ github.event.inputs.issue_number }}-` or whose body closes this issue. If one exists, comment with its URL and stop. -4. Read `.squad/team.md` and `.squad/routing.md`. Route work to the member +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 diff --git a/workflows/squad.md b/workflows/squad.md index da5d0375d..95ae9cbc6 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -106,7 +106,7 @@ safe-outputs: max: 20 target: "*" dispatch-workflow: - workflows: [squad-implement-worker, squad-review] + workflows: [squad-implement-worker, squad-deps-worker, squad-review] max: 3 --- @@ -855,14 +855,17 @@ Read-only team composition report. ## skill: `squad-implement` --- -description: Dispatch implementation work to the squad-implement-worker workflow. +description: Dispatch implementation work to the dependency or general worker. --- -Implement mode dispatches an isolated implementation worker for a regular issue. -When invoked on a parent (initiative or epic), it descends the sub-issue +Implement mode dispatches an isolated worker for a regular issue. Explicit, +dependency-only Wave 1 work routes to `squad-deps-worker`; every other task +routes to `squad-implement-worker`, whose manifest protection remains unchanged. +When invoked on a parent (initiative or epic), this mode descends the sub-issue hierarchy to the **leaf tasks** and dispatches workers for up to three currently -unblocked leaf tasks. The worker relays merged implementation pull requests back -to this mode so it can automatically refill the parent's available slots. +unblocked leaf tasks. The general worker relays merged implementation pull +requests back to this mode so it can automatically refill the parent's available +slots. **Acknowledge:** Post `🤖 Squad is preparing implementation…` using the `add-comment` safe-output. @@ -892,12 +895,50 @@ to this mode so it can automatically refill the parent's available slots. 5. If the target has one or more open leaf descendants, treat the target as a parent and follow the Epic Dispatch procedure below over the leaf-task set. Do not implement the parent body directly. -6. If the target has no open descendants (it is itself a leaf), call the - workflow-specific `squad_implement_worker` safe-output tool with `issue_number` - set to the target issue number. -7. Post a comment linking the dispatched worker run. The worker performs - dependency, duplicate pull request, routing, implementation, and validation - checks. +6. Classify every leaf with the **Dependency Route Decision** below. +7. If the target has no open descendants (it is itself a leaf), call exactly the + workflow-specific tool selected by that decision with `issue_number` set to + the target issue number. +8. Post a comment linking the dispatched worker run and naming the selected + worker. The worker performs dependency, duplicate pull request, routing, + implementation, and validation checks. + +##### Dependency Route Decision [MANDATORY — fail closed] + +Choose `squad_deps_worker` only when **all** of these statements are true: + +1. The issue explicitly asks to add, remove, or update package dependencies, or + to regenerate a dependency lockfile. +2. Every repository edit required to complete the issue is limited to the Wave + 1 dependency basenames authorized by `squad-deps-worker`: + `package.json`, `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`, + `pnpm-lock.yaml`, `Directory.Packages.props`, `go.mod`, and `go.sum`. +3. The task does not require registry/install configuration, SDK/tool pins, + governance files, source code, tests, documentation, workflows, agent + instructions, or vendored/generated dependency content. + +Choose `squad_implement_worker` for every other task. This includes mixed-scope +tasks, ambiguous dependency intent, unsupported ecosystems, ordinary source +imports, issue bodies that only contain a `Depends on:` relationship, and prose +that merely mentions a dependency. Never broaden or guess dependency intent. +The general worker's compiled `fallback-to-issue` manifest protection is the +fail-closed destination for any misclassified or mixed task. + +Before any `squad_deps_worker` dispatch, read `.squad/config.json` and apply this +exact guard: + +- The file must be readable, valid JSON, and a top-level object. Otherwise post + a denial comment and do not dispatch. +- Missing `squadDeps` key or exact string `"allow"` means allow. +- Exact string `"deny"` means deny; cite + `.squad/config.json squadDeps: "deny"` in the comment. +- Every other value, including any other string, boolean, number, `null`, array, + or object, means deny as unrecognized. + +Do not apply this config guard to `squad_implement_worker`; non-dependency tasks +must continue to route through the general path even when dependency work is +denied. The dependency worker repeats the guard so a direct human +`workflow_dispatch` cannot bypass this dispatcher check. ##### Epic Dispatch @@ -906,14 +947,14 @@ For each open leaf task in the target's descendant set: 1. Parse its `Depends on:` line and check the state of every referenced issue. 2. Exclude leaf tasks with any open dependency. 3. Find leaf tasks that already have an open pull request whose branch starts - with `squad/implement-{leaf-number}-` or whose body closes that leaf task. - These are active implementation tasks. + with `squad/implement-{leaf-number}-` or `squad/deps-{leaf-number}-`, or whose + body closes that leaf task. These are active implementation tasks. 4. Calculate `available-slots = max(0, 3 - active-implementation-count)`. 5. Exclude active implementation tasks from the ready set. 6. Sort ready leaf tasks by issue number and select at most `available-slots`. -For each selected leaf task, call the workflow-specific `squad_implement_worker` -safe-output tool with this input: +For each selected leaf task, apply the **Dependency Route Decision**, then call +exactly one selected workflow-specific safe-output tool with this input: ```json { @@ -924,12 +965,15 @@ safe-output tool with this input: 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 leaf task, and only report a leaf task as dispatched after the tool -returns success. +returns success. Never call both workers for one issue. If the dependency config +guard denies a selected dependency task, leave that slot unused and report the +denial; do not reroute it to the general worker. Post a comment on the target listing the dispatched leaf tasks, blocked leaf -tasks, leaf tasks with existing implementation pull requests, and any ready leaf -tasks deferred because all three slots are occupied. If no leaf task is ready or -no slot is available, post the status summary and do not dispatch a workflow. +tasks, the worker selected for each dispatch, dependency tasks denied by config, +leaf tasks with existing implementation pull requests, and any ready leaf tasks +deferred because all three slots are occupied. If no leaf task is ready or no +slot is available, post the status summary and do not dispatch a workflow. **Always leave a visible next step.** Every Implement run against a parent ends with a comment on that parent — never a silent exit. Cover each terminal case: From 7e1326a4d8729f52ea2b2f57ded70562b9cb47b1 Mon Sep 17 00:00:00 2001 From: brady gaster Date: Tue, 25 Aug 2026 17:03:46 -0700 Subject: [PATCH 2/2] fix: install dependency worker with dispatcher Refs #1748 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/agents.md | 3 ++- README.md | 1 + docs/src/content/docs/guide/gh-aw.md | 10 +++++++--- test/gh-aw-implement-workflow.test.ts | 6 ++++-- test/gh-aw-review-workflow.test.ts | 6 +++++- workflows/shared/squad.md | 1 + 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/agents.md b/.github/agents.md index 920175397..92de691b5 100644 --- a/.github/agents.md +++ b/.github/agents.md @@ -23,12 +23,13 @@ GitHub Agentic Workflows (`gh-aw`) are composable AI workflows triggered by slas gh aw add \ bradygaster/squad/workflows/squad.md@dev \ bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad-deps-worker.md@dev \ bradygaster/squad/workflows/squad-review.md@dev ``` This command: -1. Fetches the Squad dispatcher, implementation worker, and advisory reviewer +1. Fetches the Squad dispatcher, general and dependency workers, and advisory reviewer 2. Compiles them into GitHub Actions–compatible workflows 3. Adds the workflow sources and generated files to your repository's `.github/` directory diff --git a/README.md b/README.md index 6381a11c5..cce251c21 100644 --- a/README.md +++ b/README.md @@ -539,6 +539,7 @@ If you use [GitHub Agentic Workflows](https://github.blog/changelog/2025-05-19-g gh aw add \ bradygaster/squad/workflows/squad.md@dev \ bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad-deps-worker.md@dev \ bradygaster/squad/workflows/squad-review.md@dev git add -- \ .github/aw/ \ diff --git a/docs/src/content/docs/guide/gh-aw.md b/docs/src/content/docs/guide/gh-aw.md index 7230929f5..7a95fac3a 100644 --- a/docs/src/content/docs/guide/gh-aw.md +++ b/docs/src/content/docs/guide/gh-aw.md @@ -30,6 +30,7 @@ gh api --method PUT repos/{owner}/{repo}/actions/permissions/workflow \ gh aw add \ bradygaster/squad/workflows/squad.md@dev \ bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad-deps-worker.md@dev \ bradygaster/squad/workflows/squad-review.md@dev # 4. Commit and push the workflow sources and generated files @@ -85,12 +86,14 @@ approve their own pull requests. gh aw add \ bradygaster/squad/workflows/squad.md@dev \ bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad-deps-worker.md@dev \ bradygaster/squad/workflows/squad-review.md@dev ``` -Keep the dispatcher first. `gh aw add` discovers its implementation-worker and -reviewer dependencies while compiling it; the explicit worker and reviewer -entries then confirm the complete install surface without creating duplicates. +Keep the dispatcher first. `gh aw add` discovers its general worker, dependency +worker, and reviewer dependencies while compiling it; the explicit worker and +reviewer entries then confirm the complete install surface without creating +duplicates. The installed top-level workflow set is: - `squad.md` and `squad.lock.yml` @@ -978,6 +981,7 @@ To update your compiled workflow after pulling upstream changes: gh aw add \ bradygaster/squad/workflows/squad.md@dev \ bradygaster/squad/workflows/squad-implement-worker.md@dev \ + bradygaster/squad/workflows/squad-deps-worker.md@dev \ bradygaster/squad/workflows/squad-review.md@dev ``` diff --git a/test/gh-aw-implement-workflow.test.ts b/test/gh-aw-implement-workflow.test.ts index beda23c35..9aa5e0565 100644 --- a/test/gh-aw-implement-workflow.test.ts +++ b/test/gh-aw-implement-workflow.test.ts @@ -240,13 +240,15 @@ describe('gh-aw implement workflows', () => { const paths = [ 'bradygaster/squad/workflows/squad.md@dev', 'bradygaster/squad/workflows/squad-implement-worker.md@dev', + 'bradygaster/squad/workflows/squad-deps-worker.md@dev', 'bradygaster/squad/workflows/squad-review.md@dev', ]; const orderedInstallCommand = [ 'gh aw add \\', ` ${paths[0]} \\`, ` ${paths[1]} \\`, - ` ${paths[2]}`, + ` ${paths[2]} \\`, + ` ${paths[3]}`, ].join('\n'); const normalizedGuide = guide.replace(/\r\n/g, '\n'); const hasOrderedInstallCommand = (markdown: string): boolean => @@ -261,7 +263,7 @@ describe('gh-aw implement workflows', () => { ); expect(hasOrderedInstallCommand(reorderedGuide)).toBe(false); expect(guide).toMatch( - /Keep the dispatcher first\. `gh aw add` discovers its implementation-worker and\s+reviewer dependencies while compiling it; the explicit worker and reviewer\s+entries then confirm the complete install surface without creating duplicates\./, + /Keep the dispatcher first\. `gh aw add` discovers its general worker, dependency\s+worker, and reviewer dependencies while compiling it; the explicit worker and\s+reviewer entries then confirm the complete install surface without creating\s+duplicates\./, ); }); }); diff --git a/test/gh-aw-review-workflow.test.ts b/test/gh-aw-review-workflow.test.ts index 38b34bc9f..2fd65bee0 100644 --- a/test/gh-aw-review-workflow.test.ts +++ b/test/gh-aw-review-workflow.test.ts @@ -189,6 +189,7 @@ describe('gh-aw advisory Squad reviewer', () => { expect(installOrder).toEqual([ 'squad.md', 'squad-implement-worker.md', + 'squad-deps-worker.md', 'squad-review.md', ]); @@ -214,6 +215,8 @@ describe('gh-aw advisory Squad reviewer', () => { .map(entry => entry.name) .sort(); expect(installed).toEqual([ + 'squad-deps-worker.lock.yml', + 'squad-deps-worker.md', 'squad-implement-worker.lock.yml', 'squad-implement-worker.md', 'squad-review.lock.yml', @@ -223,7 +226,7 @@ describe('gh-aw advisory Squad reviewer', () => { ]); }, 30000); - it('keeps all consumer install surfaces on the coherent three-workflow order', () => { + it('keeps all consumer install surfaces on the coherent four-workflow order', () => { for (const surface of [GUIDE, README, AGENT_GUIDE, SHARED_BOOTSTRAP]) { const orders = installOrders(surface); expect(orders.length).toBeGreaterThan(0); @@ -231,6 +234,7 @@ describe('gh-aw advisory Squad reviewer', () => { expect(order).toEqual([ 'squad.md', 'squad-implement-worker.md', + 'squad-deps-worker.md', 'squad-review.md', ]); } diff --git a/workflows/shared/squad.md b/workflows/shared/squad.md index 5aa539000..9a75c565e 100644 --- a/workflows/shared/squad.md +++ b/workflows/shared/squad.md @@ -8,6 +8,7 @@ # gh aw add \ # bradygaster/squad/workflows/squad.md@dev \ # bradygaster/squad/workflows/squad-implement-worker.md@dev \ +# bradygaster/squad/workflows/squad-deps-worker.md@dev \ # bradygaster/squad/workflows/squad-review.md@dev # # Design credit: adapted from Peli de Halleux's proven gh-aw integration in