From 8efb33862781ed0a001f9a9df1988cd2d4fed9d2 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Thu, 1 Oct 2026 02:03:50 -0700 Subject: [PATCH 1/2] fix(gh-aw): read committed cast registry from the checkout when validating an isolated bootstrap candidate The materialize step copies the checkout into a .git-less candidate (#2120), but committedRegistry() ran git rev-parse/ls-tree/show in that candidate, so every bootstrap failed with 'registry base: committed HEAD is unavailable'. Thread an optional gitRoot (default root) through validateBootstrapPayload -> validateCastTree -> parseRegistry, and pass gitRoot: checkout from the materializer. Adds a regression test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/validate-gh-aw-cast.mjs | 8 +++---- test/gh-aw-bootstrap-workflow.test.ts | 22 +++++++++++++++++++ workflows/package/squad-bootstrap.md | 5 +++-- workflows/package/squad.md | 2 +- .../shared/squad-bootstrap-validator.mjs | 3 ++- workflows/shared/squad-cast-validator.mjs | 8 +++---- workflows/squad-bootstrap.md | 5 +++-- workflows/squad-workflows.manifest.json | 16 +++++++------- workflows/squad.md | 2 +- 9 files changed, 48 insertions(+), 23 deletions(-) diff --git a/scripts/validate-gh-aw-cast.mjs b/scripts/validate-gh-aw-cast.mjs index 0f6c3a326..eeee7b3c6 100644 --- a/scripts/validate-gh-aw-cast.mjs +++ b/scripts/validate-gh-aw-cast.mjs @@ -531,7 +531,7 @@ function committedRegistry(root, errors) { } } -function parseRegistry(root, errors) { +function parseRegistry(root, errors, gitRoot = root) { let registryRaw; let historyRaw; try { @@ -543,7 +543,7 @@ function parseRegistry(root, errors) { } const registry = parseCastingPair(registryRaw, historyRaw, 'registry', errors); if (!registry) return []; - const base = committedRegistry(root, errors); + const base = committedRegistry(gitRoot, errors); if (base) { const baseRevision = base.schema === 'squad-agent-provenance/v1' ? base.revision : 0; if (registry.revision <= baseRevision) { @@ -631,7 +631,7 @@ function validateCapabilities(coordinator, active, routingRows, errors) { } } -export function validateCastTree({ root, payloadPath }) { +export function validateCastTree({ root, payloadPath, gitRoot = root }) { const errors = []; let payloadValue; try { @@ -660,7 +660,7 @@ export function validateCastTree({ root, payloadPath }) { if (!payload.includes(required)) errors.push(`payload: missing required path ${required}`); } - const active = parseRegistry(root, errors); + const active = parseRegistry(root, errors, gitRoot); const activeNames = new Set(active.map(({ name }) => name)); const activeCharters = active.map(({ id }) => `.squad/agents/${id}/charter.md`); const expectedPayload = new Set([...CORE_PAYLOAD, ...activeCharters, ...REQUIRED_BUILTIN_CHARTERS]); diff --git a/test/gh-aw-bootstrap-workflow.test.ts b/test/gh-aw-bootstrap-workflow.test.ts index b88472327..7cb0dcb6c 100644 --- a/test/gh-aw-bootstrap-workflow.test.ts +++ b/test/gh-aw-bootstrap-workflow.test.ts @@ -31,6 +31,7 @@ import { findBootstrapResearchArtifacts, isBootstrapResearchSeed, reconstructBootstrapPayload, + validateBootstrapPayload, } from '../workflows/shared/squad-bootstrap-validator.mjs'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -612,6 +613,27 @@ describe('automatic Squad bootstrap workflow', () => { expect(resolvedResult.stdout).toBe('Squad bootstrap validation passed.\n'); }); + it('validates an isolated candidate without .git against the checkout committed registry', () => { + const fixture = createFixture(); + const candidate = mkdtempSync(join(tmpdir(), 'gh-aw-bootstrap-candidate-')); + workspaces.push(candidate); + cpSync(fixture.root, candidate, { + recursive: true, + filter: source => !/(^|[\\/])\.git([\\/]|$)/.test(source.slice(fixture.root.length)), + }); + expect(existsSync(join(candidate, '.git'))).toBe(false); + const options = { + root: candidate, + payloadPath: fixture.payloadPath, + repository: 'octo/example', + defaultBranch: 'main', + linkMode: 'placeholder', + }; + + expect(validateBootstrapPayload(options).join('\n')).toContain('registry base: committed HEAD is unavailable'); + expect(validateBootstrapPayload({ ...options, gitRoot: fixture.root })).toEqual([]); + }); + it('transports a shared payload larger than the old single-string limit byte-identically', () => { const fixture = createFixture(); const targetBytes = 34_843; diff --git a/workflows/package/squad-bootstrap.md b/workflows/package/squad-bootstrap.md index e112632c3..8322d3c48 100644 --- a/workflows/package/squad-bootstrap.md +++ b/workflows/package/squad-bootstrap.md @@ -153,8 +153,8 @@ pre-agent-steps: } # BEGIN GENERATED RESOURCE DIGESTS check_hash "$install_verifier" "40753edd5995c5f7ba058f57fbcc5aff0ccf3341c721b764fa0cf1dc70eba3f1" - check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685" - check_hash "$bootstrap_validator" "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7" + check_hash "$cast_validator" "c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada" + check_hash "$bootstrap_validator" "6f2ff60104a238c7c171031e737fd8dcb57dd3eeffccbf8c0aeaad016d85ee4e" # END GENERATED RESOURCE DIGESTS node "$bootstrap_validator" \ --encode-payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \ @@ -316,6 +316,7 @@ safe-outputs: writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`); const errors = validatorModule.validateBootstrapPayload({ root: candidate, + gitRoot: checkout, payloadPath: candidatePayloadPath, repository: context.repo.owner + '/' + context.repo.repo, defaultBranch: process.env.SQUAD_BOOTSTRAP_DEFAULT_BRANCH, diff --git a/workflows/package/squad.md b/workflows/package/squad.md index f38dbe3d7..91bed0441 100644 --- a/workflows/package/squad.md +++ b/workflows/package/squad.md @@ -840,7 +840,7 @@ pre-agent-steps: validator_script="$(cd "$(dirname "$validator_script")" && pwd -P)/$(basename "$validator_script")" # BEGIN GENERATED RESOURCE DIGEST - validator_expected_sha256="0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685" + validator_expected_sha256="c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada" # END GENERATED RESOURCE DIGEST : > "$stderr_file" validator_actual_sha256="$( diff --git a/workflows/shared/squad-bootstrap-validator.mjs b/workflows/shared/squad-bootstrap-validator.mjs index 6f04b8d27..aa7dfc797 100644 --- a/workflows/shared/squad-bootstrap-validator.mjs +++ b/workflows/shared/squad-bootstrap-validator.mjs @@ -532,6 +532,7 @@ export function createBootstrapResearchComment(issueBodyText, issueNumber) { export function validateBootstrapPayload({ root, + gitRoot = root, payloadPath, repository, defaultBranch, @@ -588,7 +589,7 @@ export function validateBootstrapPayload({ try { const castPayloadPath = join(tempDir, 'cast-paths.json'); writeFileSync(castPayloadPath, `${JSON.stringify(castPaths)}\n`); - errors.push(...validateCastTree({ root, payloadPath: castPayloadPath })); + errors.push(...validateCastTree({ root, gitRoot, payloadPath: castPayloadPath })); } finally { rmSync(tempDir, { recursive: true, force: true }); } diff --git a/workflows/shared/squad-cast-validator.mjs b/workflows/shared/squad-cast-validator.mjs index 0f6c3a326..eeee7b3c6 100644 --- a/workflows/shared/squad-cast-validator.mjs +++ b/workflows/shared/squad-cast-validator.mjs @@ -531,7 +531,7 @@ function committedRegistry(root, errors) { } } -function parseRegistry(root, errors) { +function parseRegistry(root, errors, gitRoot = root) { let registryRaw; let historyRaw; try { @@ -543,7 +543,7 @@ function parseRegistry(root, errors) { } const registry = parseCastingPair(registryRaw, historyRaw, 'registry', errors); if (!registry) return []; - const base = committedRegistry(root, errors); + const base = committedRegistry(gitRoot, errors); if (base) { const baseRevision = base.schema === 'squad-agent-provenance/v1' ? base.revision : 0; if (registry.revision <= baseRevision) { @@ -631,7 +631,7 @@ function validateCapabilities(coordinator, active, routingRows, errors) { } } -export function validateCastTree({ root, payloadPath }) { +export function validateCastTree({ root, payloadPath, gitRoot = root }) { const errors = []; let payloadValue; try { @@ -660,7 +660,7 @@ export function validateCastTree({ root, payloadPath }) { if (!payload.includes(required)) errors.push(`payload: missing required path ${required}`); } - const active = parseRegistry(root, errors); + const active = parseRegistry(root, errors, gitRoot); const activeNames = new Set(active.map(({ name }) => name)); const activeCharters = active.map(({ id }) => `.squad/agents/${id}/charter.md`); const expectedPayload = new Set([...CORE_PAYLOAD, ...activeCharters, ...REQUIRED_BUILTIN_CHARTERS]); diff --git a/workflows/squad-bootstrap.md b/workflows/squad-bootstrap.md index 3bc3e9f54..5166b5c7b 100644 --- a/workflows/squad-bootstrap.md +++ b/workflows/squad-bootstrap.md @@ -160,8 +160,8 @@ pre-agent-steps: } # BEGIN GENERATED RESOURCE DIGESTS check_hash "$install_verifier" "40753edd5995c5f7ba058f57fbcc5aff0ccf3341c721b764fa0cf1dc70eba3f1" - check_hash "$cast_validator" "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685" - check_hash "$bootstrap_validator" "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7" + check_hash "$cast_validator" "c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada" + check_hash "$bootstrap_validator" "6f2ff60104a238c7c171031e737fd8dcb57dd3eeffccbf8c0aeaad016d85ee4e" # END GENERATED RESOURCE DIGESTS node "$bootstrap_validator" \ --encode-payload "${GITHUB_WORKSPACE:?}/.github/workflows/squad-bootstrap-payload.json" \ @@ -307,6 +307,7 @@ safe-outputs: writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`); const errors = validatorModule.validateBootstrapPayload({ root: candidate, + gitRoot: checkout, payloadPath: candidatePayloadPath, repository: context.repo.owner + '/' + context.repo.repo, defaultBranch: process.env.SQUAD_BOOTSTRAP_DEFAULT_BRANCH, diff --git a/workflows/squad-workflows.manifest.json b/workflows/squad-workflows.manifest.json index f347970b8..cfde8b75e 100644 --- a/workflows/squad-workflows.manifest.json +++ b/workflows/squad-workflows.manifest.json @@ -14,9 +14,9 @@ "source": "workflows/package/squad.md", "destination": ".github/workflows/squad.md", "lock": ".github/workflows/squad.lock.yml", - "source_sha256": "0f292c576983ac99dace0661e34f900ddd91fcd50f28ff6726eff78704e283f2", - "lock_sha256": "32df53558611b35f809ea40a0de0b1b33e4f4dda955400b5387e77f96cb25277", - "package_lock_sha256": "a3336602334921327ca585f2321f84547ac2fde94f22178e25dcb029dd1370e6" + "source_sha256": "f86eb83a75c6de63edf520bf21ca2b50fcb9c6e7c5a60ecbd92a527a108a19af", + "lock_sha256": "cecc8fc0903d7de78c8c62756439722d9a347c8c0af1da317d26a5fd4708e929", + "package_lock_sha256": "4da03e3363714608f1a4901e49dee75ae02238c273a0bae15603aa280e4e347c" }, { "name": "squad-implement-worker", @@ -68,9 +68,9 @@ "source": "workflows/package/squad-bootstrap.md", "destination": ".github/workflows/squad-bootstrap.md", "lock": ".github/workflows/squad-bootstrap.lock.yml", - "source_sha256": "ca2981acbdee8ce1d4ce705b9132e3777ceb63574dd884d6c75af8a8dfe16240", - "lock_sha256": "fd1562548a1aace9c57bdf315266da413575ea07da1d4b64c4f6446afc6aefc5", - "package_lock_sha256": "8aa7f287d51079746d997e5c5f17e5807efd1dd53de1c403982fcd10de23dfc0" + "source_sha256": "be73bf3c95214de5dd4702405715d0118d8d23aebfd2d1a8822d5358e8afdf68", + "lock_sha256": "147d85f07eeb02cfde1222f46bdc1510cd1f2f1539d1407ab47b5d17484bb19f", + "package_lock_sha256": "95e403d96fd094def920f913e114a560d6bc6ef367797f5cb529ab757897d4de" }, { "name": "squad-command-router", @@ -113,7 +113,7 @@ "package_destination": ".github/workflows/shared/squad-cast-validator.mjs", "destination": ".github/workflows/shared/squad-cast-validator.mjs", "ownership": "manifest", - "sha256": "0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685" + "sha256": "c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada" }, { "path": "shared/squad-bootstrap-validator.mjs", @@ -121,7 +121,7 @@ "package_destination": ".github/workflows/shared/squad-bootstrap-validator.mjs", "destination": ".github/workflows/shared/squad-bootstrap-validator.mjs", "ownership": "manifest", - "sha256": "4fc1bcc79b887bb4f2f78e7eef8693d5c1eaf817d8715ce3c92169934aa225b7" + "sha256": "6f2ff60104a238c7c171031e737fd8dcb57dd3eeffccbf8c0aeaad016d85ee4e" }, { "path": "shared/squad-improvement-gate.mjs", diff --git a/workflows/squad.md b/workflows/squad.md index d7ab39723..284b4b267 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -152,7 +152,7 @@ pre-agent-steps: validator_script="$(cd "$(dirname "$validator_script")" && pwd -P)/$(basename "$validator_script")" # BEGIN GENERATED RESOURCE DIGEST - validator_expected_sha256="0988e04aeef316f4d7a0107c902bbbcf6538b8899b9fffba5f62150717967685" + validator_expected_sha256="c6d0b92aac71dc6f6d5727cac418a323b0bc9c12047400faa12d96150d548ada" # END GENERATED RESOURCE DIGEST : > "$stderr_file" validator_actual_sha256="$( From ce856b1b6c2d41328e67c3ac17b8cc9512dc8f96 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Thu, 1 Oct 2026 02:09:27 -0700 Subject: [PATCH 2/2] test(gh-aw): refresh pinned squad lock digests for the updated cast validator Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-quality.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index daa4dff6d..810c27cc2 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -3961,8 +3961,8 @@ describe('gh-aw: canonical package integrity contract', () => { expect(mutable).not.toContain(actionReference); expect(createHash('sha256').update(normalizeCompiledLock(mutable, revisionA)).digest('hex')) .toBe(sourceBinding === 'workflow' - ? '8456d8129241269c107cdf4b16ae079e89a98ef944152df36f53ec0decf3f30d' - : 'bac4d8b90269d827fc9e626dd02c1b536bccde7105e9037971740054cda4c19b'); + ? '264c154a950ba0e414a7410aca2f9d64f6472796b48d395bda98a03e5e8551f5' + : '2482e11b6489da9c02efa25e9a1d64c99a736fe39e6e8bf75b7999709d07445e'); expect(() => validateCompilerActionPins(mutable)).toThrow(/invalid immutable action pin/); writeFileSync(lockPath, mutable); expect(verifyInstall(root).failures.join('\n')) @@ -4016,7 +4016,7 @@ describe('gh-aw: canonical package integrity contract', () => { compile(); const unpinned = readText(lockPath); expect(createHash('sha256').update(normalizeCompiledLock(unpinned, revisionA)).digest('hex')) - .toBe('bac4d8b90269d827fc9e626dd02c1b536bccde7105e9037971740054cda4c19b'); + .toBe('2482e11b6489da9c02efa25e9a1d64c99a736fe39e6e8bf75b7999709d07445e'); expect(verifyInstall(root).failures.join('\n')).toContain('Installed digest mismatch'); const seedPins = () => spawnSync(process.execPath, ['--input-type=module', '-e', seed!], {