Skip to content

[factory] GitHub issue _index.json omits the documented labels field, forcing consumers into a full per-issue walk #263

Description

@khaliqgant

Summary

The GitHub issue index (/github/repos/<owner>/<repo>/issues/_index.json) is not emitting the labels field that this adapter's own layout contract documents. Consumers that filter by label on the index therefore cannot use it, and must fall back to opening every issue's meta.json.

This is currently breaking Factory's readiness reconciliation in production.

The contract says labels should be there

packages/github/src/layout-prompt.ts:33 documents the issue index row as:

{ "id": "<id>", "title": "<human-readable>", "updated": "<iso8601>", "number": 42, "state": "open", "labels": ["factory"] }

labels carries the issue's label names inline so you can filter on the index without opening every meta.json

and :54 gives the worked example, using this exact label:

jq '.[] | select(.labels | index("factory"))' /github/repos/octocat/hello-world/issues/_index.json

What is actually emitted

Live row from /github/repos/AgentWorkforce/factory/issues/_index.json:

{"id":"256","number":256,"state":"closed","title":"[factory] Writeback cleanup reports flushExitCode 1 ...","updated":"2026-08-14T22:58:08Z"}
keys present:  id, number, state, title, updated
labels:        ABSENT

0 of 12 rows in that index carry labels.

Why this breaks a real consumer

AgentWorkforce/factory already implements the documented fast path. src/orchestrator/factory.ts:5733 (#githubIssuePathsFromIndex) reads the index and filters:

if (state !== 'open' || !labels.some((label) => label.trim().toLowerCase() === requiredLabel)) {
  continue
}
paths.push(`.../issues/by-id/${number}.json`)

But it guards against a legacy index, deliberately failing open:

// Labels were added to the public GitHub issue index contract after the first
// index version. Fall back for the entire repository if any row is legacy or
// malformed so an eligible issue can never be filtered out.
if (... || !Array.isArray(labels) || ...) {
  return undefined
}

So the observed chain in production is:

  1. Index rows lack labels
  2. Factory correctly judges the index unusable and returns undefined
  3. It falls back to a full walk — 1,362 issues, read serially, observed live as GitHub ready issue read progress {"read":215,"total":1362}
  4. The walk cannot finish inside the 5-minute discovery sweep lease
  5. The sweep loses its lease before checkpoint and throws
  6. The periodic readiness reconciliation fails every 60 seconds, indefinitely

Factory behaves correctly at every step. The defect is upstream, here.

Tracked on the consumer side as AgentWorkforce/factory#267. Two issues are currently stuck undispatched because of it (AgentWorkforce/relay#1528, AgentWorkforce/factory#267), and relay#1528 is the chain unblock for an entire workstream.

Asked-for behaviour

  1. Emit labels on issue index rows, per the documented contract. Pull rows may omit it, as the contract already allows.
  2. Backfill existing indexes. The comment in Factory calls labels "added after the first index version," so existing rows likely predate the field. A fix that only covers newly-written rows leaves every current index unusable, and the consumer's fail-open guard rejects an index if any row is legacy — so a partial backfill is equivalent to no backfill.
  3. Determine whether the emitter writes it at all today. Before backfilling, establish whether this is (a) never implemented, (b) implemented and regressed, or (c) implemented but not reaching this deployment. The fix differs in each case, and the contract being documented is not evidence the emitter honours it.

Definition of done

  • /github/repos/<owner>/<repo>/issues/_index.json rows carry labels for issues, verified against a live projection rather than a unit test.
  • Existing indexes are backfilled, so jq '.[] | select(.labels | index("factory"))' returns matches against the real projection today.
  • A consumer filtering on the index no longer needs to open meta.json per issue.
  • Verify with AgentWorkforce/factory's reconcile: the readiness sweep should complete rather than exceed its lease, and relay#1528 should dispatch without manual intervention.

Verification — against the live projection

Do not close this on a unit test asserting the emitter's output shape.

jq '.[0] | keys' /github/repos/AgentWorkforce/factory/issues/_index.json
jq '[.[] | select(.labels | index("factory"))] | length' /github/repos/AgentWorkforce/factory/issues/_index.json

The first must include labels; the second must be non-zero (that repo has open factory-labeled issues today). Attach real output.

Constraints

  • Branch from origin/main. mergePolicy: never — open the PR and stop at review.
  • PR publication is currently failing intermittently with a 403 (AgentWorkforce/relayfile-cloud#155). If this issue's PR does not publish, report it rather than working around it — the branch will still hold the work.
  • Note: this repo was added to Factory's routing config today specifically so this issue could be dispatched. If dispatch misbehaves for reasons unrelated to the change, say so rather than working around it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions