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
80 changes: 36 additions & 44 deletions .github/aw/work-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,25 @@ description: Agent instructions for Git-backed work queue producers, dispatchers

# Work Queue

Choose the queue pattern: Git storage for fair scheduling and Claim authority, or
[WorkQueueOps](../../docs/src/content/docs/patterns/workqueue-ops.md) for
issue-backed WorkQueueOps: lists, sub-issues, Discussions or cache backlogs.

Use `tools.work-queue` for fair scheduling, immutable Work DAGs and Claim-scoped
effects. The causal `work-queue.jsonl` log is authoritative. Weights distribute
Claim opportunities, not CPU or successful completions. Protocol v3 always uses
Git; Issues/PRs are dependencies, not storage. Never set `storage`. WorkQueueOps
is a separate pattern.
Choose Git-backed `tools.work-queue` for fair scheduling, immutable Work DAGs
and Claim-scoped effects; `work-queue.jsonl` is authoritative. Protocol v3 has
no storage selector; Issues/PRs are dependencies, not storage. Weights distribute
Claim opportunities, not CPU or successful completions. For issue-backed WorkQueueOps
alternatives, see [WorkQueueOps](../../docs/src/content/docs/patterns/workqueue-ops.md).

## Select the role

- **Observer:** read/explain only; use ordinary configured report or `noop`
authorization. Do not submit, dispatch or finish Work.
- **Producer/dispatcher:** stage entitled task plans and bounded pool requests.
Queue-control authority does not permit arbitrary resource-writing outputs.
- **Worker:** require a compiler-supplied version-3 `claims` array. Never
construct/override reserved `work_queue_assignment` or caller context.
- **Observer:** read/explain only; never submit, dispatch or finish Work.
- **Producer/dispatcher:** submit entitled plans and request bounded pool work;
queue authority grants no arbitrary resource-writing outputs.
- **Worker:** require compiler-supplied version-3 `claims`; never override
reserved `work_queue_assignment` or caller context.

Trust the compiler/runtime role, never snapshot metadata or assignment alone;
don't downgrade declared workers. Activation authenticates run/workflow/revision,
and reruns cannot inherit attempt 1 authority. Report absent queues as
uninitialized; reject empty, malformed or unsupported logs.
Trust compiler/runtime role, not snapshot metadata; don't downgrade workers.
Activation authenticates run/workflow/revision; reruns cannot inherit attempt 1.
An absent branch is uninitialized/empty. Its first producer submit atomically
creates it with compiled Policy and Work; reads and dispatch alone never do.
Reject malformed or unsupported existing logs.

## Configure deployment

Expand All @@ -39,17 +35,17 @@ tools:
worker: true
```

Dispatchers need `tools.work-queue: true`, an explicit
`safe-outputs.dispatch-workflow.workflows` allowlist and a bounded `max`.
The allowlist is compiler approval, not authority: installed Policy binds each
profile's exact workflow path, immutable SHA, authenticated principal, trust
domain and effect scope. Queue dispatch ignores moving `target-ref`.
Dispatchers need `tools.work-queue: true`, a worker allowlist and bounded
`dispatch-workflow.max`. Compiler-approved Policy binds each profile's workflow,
immutable SHA, principal, trust domain and effect scope; moving `target-ref` is
ignored.

Require administrator-installed Policy and protected queue-branch writers;
frontmatter provisions neither, and administrator status grants no producer
entitlement. Never give agents or snapshot MCP queue-write credentials. Read the
[deployment guide](../../docs/src/content/docs/guides/deploy-work-queue.md) only
for installation; writer-restriction automation remains deferred.
Configure producer entitlements and branch protections before enabling
producers; rulesets must match before branch creation. First submit installs
compiled Policy; later changes are admin-only. Admin status grants no producer
entitlement. Never expose queue-write credentials to agents/snapshot MCP. See
[deployment](../../docs/src/content/docs/guides/deploy-work-queue.md);
writer-restriction automation remains deferred.

## Mirror admitted Work with Issues

Expand All @@ -66,16 +62,12 @@ Upgrade all closed-schema readers first. See the

## Plan and dispatch

- Use `work_queue_read` / `work_queue_explain` for the immutable activation
snapshot. Predictions may be stale; sorting cannot override Policy.
- Stage bounded, secret-free task/graph plans with `work_queue_submit`, within
installed producer pools, priorities and accounting keys.
- Request a pool prefix using `work_queue_dispatch_next` with `pool`,
`max_claims` and `max_dispatches`. Never select winning Work or a target, or
use legacy `dispatch_workflow` with `work_queue: {work_id: ...}`.
- Let trusted processing refresh the ledger, enforce Policy/DAG/scope and commit
a compatible fair prefix atomically. A CAS loser discards tentative choices
and charges before recomputing. Ordinary non-queue dispatch is separate.
- Read the immutable snapshot; predictions may be stale and cannot override
Policy. Submit bounded, secret-free plans within producer entitlements.
- Request a pool prefix with `work_queue_dispatch_next` (`pool`, `max_claims`,
`max_dispatches`). Never choose winners/targets or use scalar dispatch.
- Trusted processing refreshes the ledger and commits a compatible fair prefix.
CAS losers discard tentative choices/charges and recompute.

## Execute original Claims

Expand All @@ -92,11 +84,11 @@ Upgrade all closed-schema readers first. See the
- Treat finish as an intent: trusted processing publishes Completion before
scoped effects and Result only after independently verified delivery.

Use `<mcp-clis>` advertised MCP subcommands with one JSON argument (not
structured-tool `command`/`description`). Dispatcher: `uninitialized` plus
null `snapshot_sha` means absent; stop with `noop`, not submit/dispatch.
Existing policyless ledgers are deployment failures; workers require Policy.
`status: "staged"` is no grant or launch.
Use advertised `<mcp-clis>` subcommands with one JSON argument, not
structured-tool `command`/`description`. `uninitialized` plus null SHA is empty;
first `work_queue_submit` atomically installs compiled Policy and Work. Submit
before dispatch. Existing policyless ledgers are failures; workers require
Policy. `status: "staged"` is no grant or launch.

## Dependencies and recovery

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/eslint-factory-dispatcher.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions .github/workflows/eslint-factory-dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ Do not invent additional tasks or broaden resource scopes.
Read the activation snapshot first with `work_queue_read`, using
`{"pool":"default","limit":32}`. Check `queue_state`, not just `total`:
`"uninitialized"` with a null snapshot SHA means the queue branch does not
exist yet. Treat it as an empty backlog and stop with `noop`; there is no
installed Policy to authorize submission or dispatch. Surface read/tool errors
explicitly; never turn an error or an existing policyless ledger into an
exist yet. Treat it as an empty backlog; the first trusted producer submission
will atomically bootstrap the branch with its compiler-approved Policy and Work.
Do not dispatch until that submission has been admitted. Surface read/tool
errors explicitly; never turn an error or an existing policyless ledger into an
empty-backlog report.

When `<mcp-clis>` advertises the wrappers, invoke their subcommands with one JSON
Expand All @@ -66,7 +67,7 @@ work-queue work_queue_read '{"pool":"default","limit":32}'

Use the advertised MCP tools directly when wrappers are not available.

For an initialized queue, submit the prepared cohort:
Submit the prepared cohort whether the queue is initialized or absent:
Call `work_queue_submit` once with
`{"nodes": <the plan.nodes array>}` before requesting any grants. Date-keyed
graph/node identities make repeated submissions on the same UTC day idempotent,
Expand Down Expand Up @@ -99,8 +100,7 @@ worker-name allowlist; it does not replace the installed policy's profile,
revision, or principal binding. Do not call ordinary `dispatch_workflow` or typed
per-worker dispatch tools.

The authenticated operator provisions the queue Policy and grants this
dispatcher's authenticated principal producer entitlement before it can submit
or dispatch. Do not bootstrap Policy. If producer entitlement is missing from
an existing queue, report the admission failure explicitly; do not call `noop`
or ordinary worker dispatch.
Safe-output processing uses only the compiler-approved Policy proposal; never
choose or modify Policy. The proposal must grant this dispatcher's principal
producer entitlement. If bootstrap or admission fails, report the failure; do
not bypass it with `noop` or ordinary worker dispatch.
3 changes: 2 additions & 1 deletion actions/setup/js/eslint_factory_portfolio.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ describe("ESLint factory producer and dispatcher", () => {
expect(source).toContain("run.created_at");
expect(source).toContain("cat /tmp/gh-aw/agent/eslint-factory-plan.json");
expect(source.indexOf("Call `work_queue_submit` once")).toBeLessThan(source.indexOf("request the trusted scheduler"));
expect(source).toContain("Do not bootstrap Policy");
expect(source).toContain("will atomically bootstrap the branch with its compiler-approved Policy and Work.");
expect(source).toContain("Safe-output processing uses only the compiler-approved Policy proposal; never");
});

it("executes the actual preparation step with only two metadata reads and no ambient filesystem or credentials", async () => {
Expand Down
11 changes: 8 additions & 3 deletions actions/setup/js/work_queue_control_adapter.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"use strict";
const { SAFE_OUTPUT_E001 } = require("./error_codes.cjs");
const { parseStrictJSON, utf8Compare } = require("./work_queue_codec.cjs");
const { loadQueue } = require("./work_queue_binding.cjs");
const { loadQueue, policyProposalFor } = require("./work_queue_binding.cjs");
const { readStagedIntentBatch } = require("./work_queue_intents.cjs");
const { DEFAULT_INTENT_PATH } = require("./work_queue_mcp_server.cjs");
const { API_VERSION, nativeId } = require("./work_queue_native.cjs");
Expand Down Expand Up @@ -126,6 +126,11 @@ function createCompilerDependencyResolver(options) {
return Object.freeze({ scopes, getClient });
}

function dependencyStateForIntents(latest, intents, options) {
const bootstrapPolicy = !latest.projection.policy && latest.sha === null && intents.some(intent => intent.kind === "submit") ? policyProposalFor(options) : undefined;
return bootstrapPolicy ? { ...latest.projection, policy: bootstrapPolicy } : latest.projection;
}

/** @param {CompilerControlOptions} [options] */
async function main(options = {}) {
/** @type {CompilerControlOptions & {githubClient: object, context: {repo: {owner: string, repo: string}}}} */
Expand All @@ -147,7 +152,7 @@ async function main(options = {}) {
}
if (intents.some(intent => ["submit", "dispatch_next"].includes(intent.kind)) && !preview) {
const latest = await loadQueue({ ...configured, policyProposal: undefined, initializationContext: undefined });
configured.dependencyResolver = createCompilerDependencyResolver({ ...configured, state: latest.projection });
configured.dependencyResolver = createCompilerDependencyResolver({ ...configured, state: dependencyStateForIntents(latest, intents, configured) });
}
if (launching) {
const token = configured.config?.["github-token"];
Expand All @@ -164,4 +169,4 @@ async function main(options = {}) {
return require("./work_queue_dispatch.cjs").main(configured);
}

module.exports = { credentialBindings, readOnlyRepositoryClient, createCompilerDependencyResolver, main };
module.exports = { credentialBindings, readOnlyRepositoryClient, createCompilerDependencyResolver, dependencyStateForIntents, main };
12 changes: 11 additions & 1 deletion actions/setup/js/work_queue_control_adapter_checks.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { temporaryDirectory } = require("./work_queue_effect_test_helpers.cjs");
const { createCompilerDependencyResolver, credentialBindings, main } = require("./work_queue_control_adapter.cjs");
const { createCompilerDependencyResolver, credentialBindings, dependencyStateForIntents, main } = require("./work_queue_control_adapter.cjs");
const { queueFixture, WORKFLOW, DISPATCHER, REF, REPOSITORY } = require("./work_queue_lifecycle.test_helpers.cjs");
const { main: captureIntentOrigin } = require("./capture_work_queue_intent_origin.cjs");
const { canonical } = require("./work_queue_codec.cjs");
Expand Down Expand Up @@ -57,6 +57,16 @@ function registerTests({ describe, it }) {
}
});

it("uses the compiled Policy proposal only to resolve dependencies for a first submit on an absent branch", () => {
const fixture = queueFixture({ granted: false });
const latest = { sha: null, projection: { policy: null, credential_generation: "initial" } };
const state = dependencyStateForIntents(latest, [{ kind: "submit" }], { policyProposal: fixture.policy });
assert.equal(canonical(state.policy), canonical(fixture.policy));
assert.equal(latest.projection.policy, null);
assert.equal(dependencyStateForIntents(latest, [{ kind: "dispatch_next" }], { policyProposal: fixture.policy }), latest.projection);
assert.equal(dependencyStateForIntents({ ...latest, sha: "existing" }, [{ kind: "submit" }], { policyProposal: fixture.policy }), latest.projection);
});

it("rejects spoofed credential kinds, App metadata, human bot lookups, lossy IDs and unavailable identity APIs without a fallback", async () => {
let reads = 0;
const client = {
Expand Down
Loading
Loading