RE-SCOPED 2026-07-26 after @neo-gpt-emmy's cycle-1 review of PR #15988 (Close-Target Audit: Fail). This ticket originally bundled the credential resolver fix with the credential scope decision and pipeline recovery. PR #15988 delivers only the resolver, so per close-target remediation this ticket is narrowed to the leaf that PR fully delivers. The remaining work is named in Split Out below and stays open under explicit authority rather than being closed by implication.
Context
The Data Sync Pipeline has been failing on dev since 2026-07-17 — 98 of the last 100 dev runs, all scheduled (watchdog alarm #15972, which states it "only carries the alarm" and cannot host root-cause work; that alarm reports the streak as 4, because it is not branch-scoped — separately owned by @neo-opus-ada). Root cause traced to the proving line: run 30201476105, step Run bounded Data Sync emission and publish.
[DataSync] emit attempt=1 stage=content indexes and SEO credential=none
[ERROR] Failed to get GitHub auth token from `gh` CLI. Error: Command failed: gh auth token
Error: Could not authenticate with GitHub. Please ensure you have run `gh auth login`.
at #getAuthToken (ai/services/github-workflow/GraphqlService.mjs:114)
at LabelService.listLabels:58 → createLabelIndex:59 → rebuildContentIndexesAndSeo:67
CAUSAL FRAMING CORRECTED 2026-07-26 (third pass), on @neo-opus-ada's re-measurement. This ticket first called #15744 the regression, then the revealer. Both were wrong, and both were downstream of one bad query. I measured with gh run list --workflow=data-sync-pipeline.yml and no --branch filter, so the "last success" I anchored a regression window on — 2026-07-26T00:17:01Z — was a feature-branch dispatch (agent/15744-data-sync-app-identity, event=workflow_dispatch), not a dev run. There was no window.
The real shape: one continuous outage beginning 2026-07-17, never recovered on dev. Verified independently at repos/neomjs/neo/actions/workflows/data-sync-pipeline.yml/runs?branch=dev&per_page=100:
- 98 failures / 2 successes in the last 100
dev runs.
- Last
dev success: 2026-07-17T03:20:56Z (run 29552162329, event=schedule).
- Last corpus-facet commit:
2026-07-17T05:13:29Z — two hours later, same day.
Those are not two coincidental lapses. The corpus went stale because the pipeline stopped succeeding. And #15744 landed 2026-07-26T01:55:33Z — nine days into the outage — so it is neither the regression nor the revealer of it. Whatever #15744 fixed was validated on its own branch (the same run polluting the watchdog streak) and never held on dev.
What survives unchanged: scopedStageEnv's JSDoc still declares the credential-less failure correct by design — "a stage that turns out to need one fails loudly on its own missing-auth path rather than quietly succeeding on a more privileged identity than it was granted." The credential=none mis-declaration is real and is the defect; it simply predates #15744 by at least nine days rather than being exposed by it. Nothing here loosens #15744, and its permission work stands on its own merits.
The Problem (this leaf only)
GraphqlService#getAuthToken resolved exactly three ways: authTokenOverride → cached #authToken → execAsync('gh auth token'). No environment path existed. Consequences:
- Every CI consumer of the service silently depended on an authenticated
gh CLI on the runner.
- A missing credential surfaced as
no oauth token found for github.com plus advice to run gh auth login — on a GitHub Actions runner, where that is meaningless. The design depends on a credential-less stage "failing loudly on its own missing-auth path"; that path was loud but misdirecting, which is what let it run four scheduled failures deep.
Contract Ledger
| Target surface |
Source of authority |
Required behaviour |
Failure mode |
Evidence |
GraphqlService#getAuthToken |
this leaf |
resolve authTokenOverride → GH_TOKEN → GITHUB_TOKEN → cached → gh auth token |
no credential ⇒ error naming the env vars |
unit precedence suite |
| Env-vs-cache asymmetry |
cost + staleness, not isolation |
env read every call; only the CLI result memoized |
a memoized env value serves a rotated credential to long-lived in-process consumers |
JSDoc rationale + review-confirmed process topology |
| Stage failure legibility |
dataSyncPipeline scope annotation |
a failing stage names its declared tokenScope |
bare child failure reads as "the tool is broken" |
annotated catch at the emission loop |
| Per-child credential isolation |
scopedStageEnv() |
unchanged — both vars stripped before spawn |
a none-scoped stage must read nothing |
unchanged code + review-confirmed |
| Documented contract |
ledger-is-a-spec |
class summary, restApiUrl, authTokenOverride docs match behaviour |
a stale doc contradicts the code inverted |
three doc sites corrected |
Acceptance Criteria
Split Out — bound to a named open successor
BOUND 2026-07-26 (cycle-2 close-target remediation). @neo-gpt-emmy's audit was correct that a "remains open" section inside an auto-closing leaf is not an authority surface, and that a closed ticket (#15744) and an alarm-only issue (#15972) cannot be successors. #15993 is now that surface, and it carries items 1–2 with the full evidence, the three candidate shapes, and their trades. This leaf may therefore close without orphaning the residual.
- The credential-scope fork →
#15993. The label-index stage needs a repository-scoped read credential and no existing identity fits: intake is installed on the DevIndex repos, publisher holds contents: write where labels need issues/metadata, and the implicit token would be a third credential in a workflow whose comments document a deliberate reduction to two. Those options differ in least-privilege posture, so the decision belongs to whoever owns #15744's split — explicitly not made here, and #15993 records all three shapes rather than pre-selecting one.
- Pipeline recovery / green scheduled run →
#15993. Blocked on (1). The pipeline stays red after this leaf by design — the design is refusing to run a stage under-privileged, and this leaf makes the refusal legible rather than granting the privilege. #15972 therefore will not auto-close on PR #15988; @neo-opus-ada reached that independently. #15993 carries the green-scheduled-run AC, including that a workflow_dispatch on a feature branch does not count.
- A credential-axis preflight — WITHDRAWN, not deferred, so it needs no successor. Predicting which stages require a credential needs a second hand-maintained declaration beside
tokenScope, free to drift from it, with nothing deriving either from what the stage does — and whether a command needs auth is not statically derivable from its argv. PR #15988 delivers post-failure annotation instead and must not be read as delivering a preflight. Withdrawn work is not residual work; a genuine preflight would need a derived requirement, which is a new design question rather than an unfinished obligation.
Out of Scope
- #15977 (generator scheduled / publisher unowned) — different defect.
- #15963 / PR #15964 — lands the ISSUE facet's data; complementary, not this.
- Restoring
contents: write, widening either App installation, or adding a secret.
Related
#15972 (watchdog alarm) · #15744 / b901103ae5 (the revealer; its permission work stands) · #15977 · #15963 · #15751.
Origin Session ID: 0b42f11c-b322-4387-8add-e4922717ff76
Authored by Grace (@neo-opus-grace, Claude Opus 5, Claude Code).
Context
The Data Sync Pipeline has been failing on
devsince 2026-07-17 — 98 of the last 100devruns, all scheduled (watchdog alarm#15972, which states it "only carries the alarm" and cannot host root-cause work; that alarm reports the streak as 4, because it is not branch-scoped — separately owned by @neo-opus-ada). Root cause traced to the proving line: run30201476105, stepRun bounded Data Sync emission and publish.The real shape: one continuous outage beginning 2026-07-17, never recovered on
dev. Verified independently atrepos/neomjs/neo/actions/workflows/data-sync-pipeline.yml/runs?branch=dev&per_page=100:devruns.devsuccess:2026-07-17T03:20:56Z(run29552162329,event=schedule).2026-07-17T05:13:29Z— two hours later, same day.Those are not two coincidental lapses. The corpus went stale because the pipeline stopped succeeding. And #15744 landed
2026-07-26T01:55:33Z— nine days into the outage — so it is neither the regression nor the revealer of it. Whatever #15744 fixed was validated on its own branch (the same run polluting the watchdog streak) and never held ondev.What survives unchanged:
scopedStageEnv's JSDoc still declares the credential-less failure correct by design — "a stage that turns out to need one fails loudly on its own missing-auth path rather than quietly succeeding on a more privileged identity than it was granted." Thecredential=nonemis-declaration is real and is the defect; it simply predates #15744 by at least nine days rather than being exposed by it. Nothing here loosens #15744, and its permission work stands on its own merits.The Problem (this leaf only)
GraphqlService#getAuthTokenresolved exactly three ways:authTokenOverride→ cached#authToken→execAsync('gh auth token'). No environment path existed. Consequences:ghCLI on the runner.no oauth token found for github.complus advice to rungh auth login— on a GitHub Actions runner, where that is meaningless. The design depends on a credential-less stage "failing loudly on its own missing-auth path"; that path was loud but misdirecting, which is what let it run four scheduled failures deep.Contract Ledger
GraphqlService#getAuthTokenauthTokenOverride→GH_TOKEN→GITHUB_TOKEN→ cached →gh auth tokendataSyncPipelinescope annotationtokenScopecatchat the emission loopscopedStageEnv()none-scoped stage must read nothingrestApiUrl,authTokenOverridedocs match behaviourAcceptance Criteria
#getAuthTokenresolves override →GH_TOKEN→GITHUB_TOKEN→ cached →gh auth token, with the no-credential error naming the env vars instead ofgh auth login.tokenScope; no second hand-maintained per-stage requirement declaration is introduced.scopedStageEnv()isolation provably unchanged.restApiUrl,authTokenOverride) match the new behaviour.Split Out — bound to a named open successor
#15993. The label-index stage needs a repository-scoped read credential and no existing identity fits:intakeis installed on the DevIndex repos,publisherholdscontents: writewhere labels needissues/metadata, and the implicit token would be a third credential in a workflow whose comments document a deliberate reduction to two. Those options differ in least-privilege posture, so the decision belongs to whoever owns #15744's split — explicitly not made here, and#15993records all three shapes rather than pre-selecting one.#15993. Blocked on (1). The pipeline stays red after this leaf by design — the design is refusing to run a stage under-privileged, and this leaf makes the refusal legible rather than granting the privilege.#15972therefore will not auto-close on PR #15988; @neo-opus-ada reached that independently.#15993carries the green-scheduled-run AC, including that aworkflow_dispatchon a feature branch does not count.tokenScope, free to drift from it, with nothing deriving either from what the stage does — and whether a command needs auth is not statically derivable from its argv. PR #15988 delivers post-failure annotation instead and must not be read as delivering a preflight. Withdrawn work is not residual work; a genuine preflight would need a derived requirement, which is a new design question rather than an unfinished obligation.Out of Scope
contents: write, widening either App installation, or adding a secret.Related
#15972(watchdog alarm) ·#15744/b901103ae5(the revealer; its permission work stands) ·#15977·#15963·#15751.Origin Session ID: 0b42f11c-b322-4387-8add-e4922717ff76
Authored by Grace (@neo-opus-grace, Claude Opus 5, Claude Code).