Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Handle absent work-queue branch in dispatcher snapshots
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
  • Loading branch information
Copilot and pelikhan authored Oct 9, 2026
commit b1c7aa7f88da9301769f8f65784a6364f87420a8
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.

19 changes: 9 additions & 10 deletions .github/workflows/eslint-factory-dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,19 @@ 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"` means the queue Policy is absent, not that an initialized queue
has no work. Report this deployment failure with `missing_data`, then stop without
dispatching or calling `noop`. Surface read/tool errors explicitly; never turn an
error into an empty-backlog report.
`"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
Comment on lines +54 to +55

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the shared work-queue guidance in cb6cdb9 to distinguish a null-snapshot_sha absent branch from an existing policyless ledger; workers still require Policy.

installed Policy to authorize submission or dispatch. 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
argument through the shell tool:

```bash
work-queue work_queue_read '{"pool":"default","limit":32}'
safeoutputs missing_data '{"data_type":"work-queue policy","reason":"Queue is uninitialized; an authenticated administrator must install Policy and producer entitlements before dispatch."}'
```

Run the second command only for an uninitialized queue. Do not call `safeoutputs`
as a structured tool with `command` and `description`; it is a CLI wrapper.
Use the advertised MCP tools directly when wrappers are not available.

For an initialized queue, submit the prepared cohort:
Expand Down Expand Up @@ -102,6 +100,7 @@ revision, or principal binding. Do not call ordinary `dispatch_workflow` or type
per-worker dispatch tools.

The authenticated operator provisions the queue Policy and grants this
dispatcher's authenticated principal producer entitlement before this workflow
runs. Do not bootstrap Policy. If producer entitlement is missing, report the
admission failure explicitly; do not call `noop` or ordinary worker dispatch.
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.
4 changes: 2 additions & 2 deletions actions/setup/js/work_queue_claim_scope.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ function readClaimScopeContext() {
if ((role === "worker") !== (value !== null)) throw scopeError("declared workers require their original assignment and nonworkers cannot acquire one");
if (role === "observer" && configuredRole !== "observer") throw scopeError("ordinary observer outputs require an explicit protected compiler role");
const runtime = require("./aw_context.cjs").resolveWorkQueueRuntime(value === null ? {} : { inputs: { work_queue_assignment: value } }, { role: configuredRole ?? snapshot.role });
if (snapshot.transactionLog === "" && !(configuredRole === "observer" && role === "observer" && snapshot.sha === null))
throw scopeError("work_queue_policy_missing: existing and participant queues require an installed Policy; only a protected observer may read a genuinely absent branch", "work_queue_policy_missing");
if (snapshot.transactionLog === "" && !(snapshot.sha === null && role !== "worker" && (role !== "observer" || configuredRole === "observer")))
throw scopeError("work_queue_policy_missing: existing queues and workers require an installed Policy", "work_queue_policy_missing");
Comment on lines +141 to +142

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb6cdb9: the safe-output append path now permits only noop for an absent dispatcher snapshot, with regressions rejecting queue-control messages, worker effects, ordinary dispatch, and existing policyless ledgers.

if (role === "observer") {
if (snapshot.transactionLog !== "") {
const queue = require("./work_queue_replay.cjs");
Expand Down
9 changes: 4 additions & 5 deletions actions/setup/js/work_queue_documentation.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,13 @@ describe("work-queue deployment documentation", () => {
expect(automation).not.toContain('pass `work_queue: {work_id: "<id>"}`');
});

it("distinguishes an uninitialized factory from an empty backlog and documents real CLI boundaries", () => {
it("treats genuine factory queue absence as empty without bypassing Policy", () => {
const dispatcher = readRepositoryFile(".github/workflows/eslint-factory-dispatcher.md");
expect(dispatcher).toContain('work-queue work_queue_read \'{"pool":"default","limit":32}\'');
expect(dispatcher).toContain("Check `queue_state`, not just `total`");
expect(dispatcher).toContain("then stop without");
expect(dispatcher).toContain("dispatching or calling `noop`");
expect(dispatcher).toContain("safeoutputs missing_data");
expect(dispatcher).toContain("not that an initialized queue");
expect(dispatcher).toContain("Treat it as an empty backlog and stop with `noop`");
expect(dispatcher).toMatch(/no\s+installed Policy to authorize submission or dispatch/);
expect(dispatcher).toContain("existing policyless ledger");
expect(dispatcher).toContain('work-queue work_queue_dispatch_next \'{"pool":"default","max_claims":3,"max_dispatches":3}\'');
expect(dispatcher).toContain('`status: "staged"`');
expect(dispatcher).toContain("an empty snapshot or a prediction of no eligible Work can be stale");
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/work_queue_mcp_server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ function loadWorkQueueSnapshot(snapshotPath = process.env.GH_AW_WORK_QUEUE_SNAPS
integer(snapshot.captured_at, 0, Number.MAX_SAFE_INTEGER, "snapshot timestamp");
const worker = snapshot.worker === null ? null : normalizeAssignment(snapshot.worker);
const role = snapshotRuntimeRole({ ...snapshot, worker });
const absent = role === "observer" && snapshot.sha === null && snapshot.transactionLog === "";
const absent = role !== "worker" && snapshot.sha === null && snapshot.transactionLog === "";
if (snapshot.transactionLog === "" && !absent) throw new Error("work_queue_policy_missing");
const projection = absent ? queue.newState() : queue.replayTransactions(queue.parseTransactionLog(snapshot.transactionLog));
if (!projection.policy && !(role === "observer" && snapshot.sha === null && snapshot.transactionLog === "")) throw new Error("work_queue_policy_missing");
if (!projection.policy && !absent) throw new Error("work_queue_policy_missing");
if (snapshot.visible_work_ids !== undefined && (!Array.isArray(snapshot.visible_work_ids) || snapshot.visible_work_ids.some(id => typeof id !== "string"))) throw new TypeError("work queue snapshot visibility is invalid");
log.debug("snapshot.load.complete", { bytes: stat.size, works: projection.works.size, worker: !!worker, absent });
return Object.freeze({ sha: snapshot.sha, worker, role, captured_at: snapshot.captured_at, origin: snapshot.origin, visible_work_ids: snapshot.visible_work_ids, projection });
Expand Down
12 changes: 12 additions & 0 deletions actions/setup/js/work_queue_mcp_server.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ describe("bounded credential-free work queue MCP", () => {
expect(() => loadWorkQueueSnapshot(test.snapshotPath)).toThrow(/policy_missing/);
});

it("reads an absent dispatcher queue as empty but rejects existing policyless and worker snapshots", () => {
const test = setup();
fs.writeFileSync(test.snapshotPath, JSON.stringify({ ...test.envelope, sha: null, transactionLog: "", worker: null, role: "dispatcher" }));
const snapshot = loadWorkQueueSnapshot(test.snapshotPath);
expect(readWorkQueueState(snapshot)).toMatchObject({ queue_state: "uninitialized", total: 0, snapshot_sha: null });
expect(snapshot.projection.policy).toBeNull();
fs.writeFileSync(test.snapshotPath, JSON.stringify({ ...test.envelope, sha: "existing", transactionLog: "", worker: null, role: "dispatcher" }));
expect(() => loadWorkQueueSnapshot(test.snapshotPath)).toThrow(/policy_missing/);
fs.writeFileSync(test.snapshotPath, JSON.stringify({ ...test.envelope, sha: null, transactionLog: "", role: "worker" }));
expect(() => loadWorkQueueSnapshot(test.snapshotPath)).toThrow(/assignment_required|policy_missing/);
});

it("reports independent Work/Claim states, pending delivery, native accounting and absent Work without payloads", () => {
const test = setup({ granted: true, bound: true });
test.fixture.append("finish", { dispatch_id: test.fixture.assignment.dispatch_id, claim_handle: "h1", outcome: "completed" }, { ...test.fixture.workerActor, dispatch_id: test.fixture.assignment.dispatch_id, claim_handle: "h1" });
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/work_queue_summary.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async function main(options = {}) {
const githubClient = options.githubClient || github;
const repositoryContext = options.context || context;
const latest = await (options.readWorkQueueLog || readWorkQueueLog)({ githubClient, owner: repositoryContext.repo.owner, repo: repositoryContext.repo.repo, core: coreApi });
const current = snapshot.role === "observer" && snapshot.sha === null && latest.sha === null && latest.transactions.length === 0 ? newState() : replayTransactions(latest.transactions);
const current = snapshot.role !== "worker" && snapshot.sha === null && latest.sha === null && latest.transactions.length === 0 ? newState() : replayTransactions(latest.transactions);
await coreApi.summary.addRaw(renderSummary(snapshot, current) + "\n" + renderWorkQueue(current)).write();
} catch (error) {
coreApi.warning("Work queue activity is unavailable; snapshot or authoritative ledger validation failed.");
Expand Down
11 changes: 11 additions & 0 deletions actions/setup/js/work_queue_summary.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ describe("fair DAG queue lifecycle summaries", () => {
).rejects.toThrow(/summarize/);
});

it("summarizes genuine dispatcher absence but rejects an existing policyless ledger", async () => {
const fixture = queueFixture({ granted: false });
const snapshotPath = snapshotFile(fixture);
fs.writeFileSync(snapshotPath, JSON.stringify({ version: 3, sha: null, worker: null, origin: fixture.dispatcher, role: "dispatcher", captured_at: fixture.at, transactionLog: "" }));
const addRaw = vi.fn(() => ({ write: async () => {} }));
const options = { snapshotPath, githubClient: fixture.githubClient, context: fixture.dispatcherContext, core: { summary: { addRaw }, warning: vi.fn() } };
await main({ ...options, readWorkQueueLog: async () => ({ sha: null, transactions: [] }) });
expect(addRaw).toHaveBeenCalledWith(expect.stringContaining("0 Work nodes; 0 new checked commits"));
await expect(main({ ...options, readWorkQueueLog: async () => ({ sha: "existing", transactions: [] }) })).rejects.toThrow(/summarize/);
});

it("distinguishes observer reports from unassigned queue-control authority", () => {
const fixture = queueFixture({ granted: false });
const summary = renderSummary({ projection: fixture.state, worker: null, role: "observer" }, fixture.state);
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/write_work_queue_snapshot.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async function main(options = {}) {
if (runtime.role === "observer" && options.initializationContext !== undefined) throw new Error("work_queue_observer_read_only");
const readConfiguration = runtime.role === "observer" ? { ...configuration, policyProposal: undefined } : configuration;
let latest = await loadQueue(readConfiguration);
if (!latest.projection.policy && !(runtime.role === "observer" && latest.sha === null && latest.transactions.length === 0)) throw new Error("work_queue_policy_missing");
if (!latest.projection.policy && !(runtime.role !== "worker" && latest.sha === null && latest.transactions.length === 0)) throw new Error("work_queue_policy_missing");
let worker = runtime.assignment ? validateStoredAssignment(latest.projection, runtime.assignment).assignment : null;
let trustedContext;
if (worker) {
Expand All @@ -42,6 +42,7 @@ async function main(options = {}) {
trustedContext = await authenticatePublisher({ ...configuration, role: runtime.role === "observer" ? "producer" : "dispatcher" });
latest = await loadQueue(readConfiguration);
}
if (!latest.projection.policy && !(runtime.role !== "worker" && latest.sha === null && latest.transactions.length === 0)) throw new Error("work_queue_policy_missing");
const snapshot = {
version: 3,
sha: latest.sha,
Expand Down
26 changes: 26 additions & 0 deletions actions/setup/js/write_work_queue_snapshot.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,38 @@ describe("authenticated immutable activation snapshots", () => {
initializeWorkQueue: initialize,
readWorkQueueLog: async () => ({ sha: null, transactions: [], state: newState() }),
});

expect(snapshot).toMatchObject({ role: "observer", sha: null, worker: null, transactionLog: "", origin: { role: "producer" } });
expect(loadWorkQueueSnapshot(options.snapshotPath).projection.policy).toBeNull();
expect(initialize).not.toHaveBeenCalled();
await expect(main({ ...options, role: "observer", readWorkQueueLog: async () => ({ sha: "existing", transactions: [], state: newState() }) })).rejects.toThrow(/policy_missing/);
});

it("captures a genuinely absent queue for a dispatcher without creating policy or granting worker authority", async () => {
const { options } = setup({ granted: false });
const readWorkQueueLog = vi.fn(async () => ({ sha: null, transactions: [], state: newState() }));
const snapshot = await main({ ...options, readWorkQueueLog });
expect(snapshot).toMatchObject({ role: "dispatcher", sha: null, worker: null, transactionLog: "" });
expect(loadWorkQueueSnapshot(options.snapshotPath).projection.policy).toBeNull();
expect(readWorkQueueLog).toHaveBeenCalledTimes(2);
const keys = ["GH_AW_WORK_QUEUE_ENABLED", "GH_AW_WORK_QUEUE_ROLE", "GH_AW_WORK_QUEUE_SNAPSHOT"];
const previous = keys.map(key => process.env[key]);
try {
process.env.GH_AW_WORK_QUEUE_ENABLED = "true";
process.env.GH_AW_WORK_QUEUE_ROLE = "dispatcher";
process.env.GH_AW_WORK_QUEUE_SNAPSHOT = options.snapshotPath;
expect(readClaimScopeContext()).toMatchObject({ assignment: null, snapshot: { sha: null } });
} finally {
keys.forEach((key, index) => {
if (previous[index] === undefined) delete process.env[key];
else process.env[key] = previous[index];
});
}
const existing = setup({ granted: false });
await expect(main({ ...existing.options, readWorkQueueLog: async () => ({ sha: "existing", transactions: [], state: newState() }) })).rejects.toThrow(/policy_missing/);
expect(fs.existsSync(existing.options.snapshotPath)).toBe(false);
});

it("cannot downgrade declared workers or supplied assignments into read-only observers", async () => {
const unassigned = setup({ granted: false });
await expect(main({ ...unassigned.options, role: "worker" })).rejects.toThrow(/assignment_required/);
Expand Down