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
75 changes: 74 additions & 1 deletion docs/src/content/docs/guide/gh-aw.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ for workflow in squad squad-implement-worker squad-review squad-deps-worker squa
done

# Verify every local runtime module referenced by those workflows was installed
for runtime_module in squad-cast-validator squad-bootstrap-validator squad-improvement-gate squad-retro-evidence squad-retro-provenance; do
for runtime_module in squad-cast-validator squad-bootstrap-validator squad-improvement-gate squad-retro-evidence squad-retro-provenance squad-implementation-provenance; do
test -f ".github/workflows/shared/${runtime_module}.mjs" || {
echo "MISSING shared/${runtime_module}.mjs"
exit 1
}
done

test -f ".github/workflows/shared/implementation-provenance-v1.schema.json" || {
echo "MISSING shared/implementation-provenance-v1.schema.json"
exit 1
}

# Strict compilation validates gh-aw's source contract; also reject JSON-escaped
# operators inside emitted GitHub expressions, which GitHub rejects before jobs start.
if grep -nE '\$\{\{[^}]*\\u00(26|3[cCeE])' .github/workflows/*.lock.yml; then
Expand Down Expand Up @@ -440,6 +445,74 @@ wins: `/squad plan accept scope` is not treated as `/squad plan`.
| Governance | `/squad approve-improvement` | Request implementation of an exact retrospective proposal revision | Human write/maintain/admin permission, `Approved-Revision:` hash and exact `Approved-Path:` lines; dispatcher relays nested `issue_number` and `approval_comment_id`, never approval authority |
| Governance | `/squad revoke-improvement` | Withdraw a prior `/squad approve-improvement` | Reserved, read-only command available to any actor; emits no output of any kind — the comment itself is the record that later runs re-check |

### Implementation provenance

Every pull request created by the general implementation worker or dependency
worker receives a `Squad implementation provenance:` comment using schema
`https://bradygaster.github.io/squad/schemas/implementation-provenance/v1`.
The pre-creation gate validates one strict provenance-record request and all
replacement references. After the PR handler returns, a compiled safe-output
script resolves the handler's temporary ID to the actual PR number, re-fetches
the created PR and replacement evidence, constructs the authoritative payload
from GitHub runtime context, and posts the comment as `github-actions[bot]`.
Consumers and merge-continuation/replacement checks accept provenance only
from that bot-authored boundary. No `"self"` or unresolved temporary identifier
is durable evidence. The general worker also keeps the existing standalone
`<!-- squad:implement issue={issue} run={run} -->` marker unchanged for
backward compatibility.

The dispatcher posts a bot-authored receipt before each worker dispatch. The
worker verifies that receipt, the dispatcher Actions run, repository, run
attempt, origin issue, selected worker, and deterministic session identifier
before the agent starts. The `implementation_session_id` is minted by the
dispatching `squad` or `squad-retro` run as
`squad-implementation-session/v1/{repository-id}/{dispatcher-run-id}`. Treat it
as opaque:

- **Lifetime:** one scheduling wave. All implementation and dependency workers
dispatched by that run share the identifier.
- **Retries and reruns:** rerunning the same dispatcher run or worker preserves
the identifier; `workflow_run.run_attempt` identifies the concrete rerun.
A new `/squad implement`, retrospective reconciliation, or merge-refill
dispatcher run starts a new session.
- **Multiple pull requests:** several PRs may share one session when a parent
issue dispatches several ready leaf tasks. Session ID is not a PR ID.
- **Replacement pull requests:** `replaces` explicitly lists verified earlier
PRs. Each referenced PR must exist in the same repository and carry one valid
provenance comment for the same origin issue and session. Duplicate,
malformed, nonexistent, or unrelated references fail closed.
- **Multiple goals:** `origin_issue` is the primary scheduling goal. `goals`
lists every explicitly referenced issue and whether the PR closes or merely
relates to it. A PR may have multiple goals, but it has one origin issue.
- **Pull request reference:** `pull_request.number` is always the actual
positive integer returned by the PR handler.

Do not derive a missing session identifier from branch names, actors,
timestamps, closing text, or textual similarity. A worker started directly by
a human cannot mint an identity; it must have the exact bot-authored dispatcher
receipt and matching Actions run. Missing
provenance means the session is unknown. Consumers may continue to use the
legacy implementation marker and closing references as their own explicit
correlation sources, and may label branch correlation as inferred, but those
sources do not manufacture a session ID.

#### Compatibility and cache behavior

| Producer evidence | Consumer treatment |
|---|---|
| Valid v1 payload | Authoritative for repository, origin, session, run, PR, goals, and replacements |
| No v1 payload | Compatible legacy record; session remains unknown |
| Malformed, duplicated, partial, or runtime-mismatched v1 payload | Invalid producer evidence; do not silently downgrade it to a valid v1 record |
| Cached valid payload whose source cannot be refreshed | Retain only with an explicit stale/source-unavailable status |
| Fresh source says the payload was removed or changed | Replace the cached source result; do not merge old fields into the new payload |

Repositories upgrading from an older Squad workflow do not need to rewrite old
pull requests. Recompile and commit the updated dispatcher, implementation
worker, dependency worker, retrospective, shared validator, and schema
together. Direct manual dispatches of either worker fail closed; use
`/squad implement` or the retrospective relay so the dispatcher can create the
authoritative receipt and bound session inputs.

### Where you can use slash commands

| Surface | How it works |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"schema": "https://bradygaster.github.io/squad/schemas/implementation-provenance/v1",
"schema_version": "1",
"producer": "squad",
"repository": "octo/example",
"origin_issue": 42,
"implementation_session_id": "squad-implementation-session/v1/12345/67890",
"session_origin": {
"repository": "octo/example",
"workflow": ".github/workflows/squad.lock.yml",
"run_id": 67890,
"run_attempt": 1
},
"workflow_run": {
"repository": "octo/example",
"workflow": ".github/workflows/squad-implement-worker.lock.yml",
"run_id": 70002,
"run_attempt": 2,
"event": "workflow_dispatch"
},
"pull_request": {
"repository": "octo/example",
"number": 102,
"head_ref": "squad/implement-42-replacement"
},
"goals": [
{
"repository": "octo/example",
"issue": 42,
"relationship": "closes"
},
{
"repository": "octo/example",
"issue": 43,
"relationship": "relates"
}
],
"replaces": [
{
"repository": "octo/example",
"number": 101
}
]
}
34 changes: 34 additions & 0 deletions test-fixtures/implementation-provenance/valid-v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"schema": "https://bradygaster.github.io/squad/schemas/implementation-provenance/v1",
"schema_version": "1",
"producer": "squad",
"repository": "octo/example",
"origin_issue": 42,
"implementation_session_id": "squad-implementation-session/v1/12345/67890",
"session_origin": {
"repository": "octo/example",
"workflow": ".github/workflows/squad.lock.yml",
"run_id": 67890,
"run_attempt": 1
},
"workflow_run": {
"repository": "octo/example",
"workflow": ".github/workflows/squad-implement-worker.lock.yml",
"run_id": 70001,
"run_attempt": 1,
"event": "workflow_dispatch"
},
"pull_request": {
"repository": "octo/example",
"number": 101,
"head_ref": "squad/implement-42-example"
},
"goals": [
{
"repository": "octo/example",
"issue": 42,
"relationship": "closes"
}
],
"replaces": []
}
14 changes: 7 additions & 7 deletions test/gh-aw-implement-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,7 @@ describe('gh-aw implement worker: retro-origin provenance enforcement', () => {
it('wires the guard into a pre-agent step and the safe-outputs boundary', () => {
expect(worker).toContain('shared/squad-retro-provenance.mjs');
expect(worker).toContain('--implement-inputs');
expect(worker).toContain('Checkout trusted base for the pre-agent provenance guard');
expect(worker).toContain('Checkout executing workflow commit for the pre-agent provenance guard');
expect(worker).toContain('.squad-pre-agent-trusted-base/.github/workflows/shared/squad-retro-provenance.mjs');
expect(worker).toContain('SQUAD_IMPLEMENT_PULL_BODY');
expect(worker).toContain('SQUAD_IMPLEMENT_PULL_HEAD_REF');
Expand All @@ -886,7 +886,7 @@ describe('gh-aw implement worker: retro-origin provenance enforcement', () => {

it('compiles the merge gate before the agent and before safe-output processing', () => {
const lock = compiledWorkerLock();
const preCheckout = lock.indexOf('name: Checkout trusted base for the pre-agent provenance guard');
const preCheckout = lock.indexOf('name: Checkout executing workflow commit for the pre-agent provenance guard');
const preGuard = lock.indexOf('name: Validate dispatch inputs and declared origin');
const agent = lock.indexOf('name: Execute GitHub Copilot CLI');
expect(preCheckout).toBeGreaterThan(-1);
Expand All @@ -896,7 +896,7 @@ describe('gh-aw implement worker: retro-origin provenance enforcement', () => {
const preGuardEnd = preAgentContract.indexOf('name: Download container images');
expect(preGuardEnd).toBeGreaterThan(-1);
const preGateSteps = preAgentContract.slice(0, preGuardEnd);
expect(preAgentContract).toContain('ref: refs/heads/${{ github.event.repository.default_branch }}');
expect(preAgentContract).toContain('ref: ${{ github.workflow_sha }}');
expect(preAgentContract).toContain('persist-credentials: false');
expect(preAgentContract).toContain('SQUAD_IMPLEMENT_PULL_BODY');
expect(preAgentContract).toContain('SQUAD_IMPLEMENT_PULL_HEAD_REF');
Expand All @@ -918,7 +918,7 @@ describe('gh-aw implement worker: retro-origin provenance enforcement', () => {

it('detects mutations that remove either merge-provenance boundary', () => {
const required = [
'Checkout trusted base for the pre-agent provenance guard',
'Checkout executing workflow commit for the pre-agent provenance guard',
'SQUAD_IMPLEMENT_PULL_BODY',
'SQUAD_IMPLEMENT_PULL_HEAD_REF',
'SQUAD_IMPLEMENT_PULL_HEAD_REPOSITORY',
Expand Down Expand Up @@ -954,10 +954,10 @@ describe('gh-aw implement worker: retro-origin provenance enforcement', () => {
// never processed), and the `pull_request: closed` continuation would have
// imported enforcement code from `refs/pull/N/merge`.
// -------------------------------------------------------------------------
it('compiles an UNCONDITIONAL default-branch checkout ahead of the guard, whatever the output shape', () => {
it('compiles an UNCONDITIONAL immutable workflow checkout ahead of the guard, whatever the output shape', () => {
const job = safeOutputsJob(compiledWorkerLock());

const trustedIndex = job.indexOf('name: Checkout trusted base for the provenance guard');
const trustedIndex = job.indexOf('name: Checkout executing workflow commit for the provenance guard');
const guardIndex = job.indexOf('name: Enforce implement provenance before any output');
const processIndex = job.indexOf('name: Process Safe Outputs');
expect(trustedIndex).toBeGreaterThan(-1);
Expand All @@ -969,7 +969,7 @@ describe('gh-aw implement worker: retro-origin provenance enforcement', () => {
expect(trustedStep).toContain(
'uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1',
);
expect(trustedStep).toContain('ref: refs/heads/${{ github.event.repository.default_branch }}');
expect(trustedStep).toContain('ref: ${{ github.workflow_sha }}');
expect(trustedStep).toContain('persist-credentials: false');
expect(trustedStep).toContain('path: .squad-trusted-base');
// Nothing may make the trusted checkout, or the guard it feeds, skippable:
Expand Down
Loading
Loading