Context
Narrow delivery leaf under #15931 after its required consumer census separated two different path owners. #16008 / PR #16009 owns the Neural Link stdio consumer; this ticket owns the remaining TenantRepoSyncService resolver and leaves the umbrella open until both leaves and its full census are complete.
The direct env/hardcoded bridge was introduced by PR #12050 for #12036 when a full-copy, gitignored config overlay could miss a newly added leaf. The current substrate has since changed: ai/config.mjs is a delta-only subclass of ai/configBase.mjs, so an unmentioned base leaf is inherited by construction. ADR 0019 now makes env resolution the leaf's exclusive responsibility and classifies a consumer-side direct env/default chain as A1 re-derivation.
Live latest-open sweep: checked the latest 20 open issues at 2026-07-26T17:52:37Z; no equivalent ticket or in-flight A2A claim exists. Knowledge Base and local issue/discussion sweeps found the historical additive ticket #12036 / PR #12050 and the current umbrella #15931, but no narrow removal leaf.
The Problem
At current dev, TenantRepoSyncService.resolveTenantReposConfig() still owns four resolution layers:
tier1MirrorRoot test seam;
orchestratorConfig?.tenantRepoMirrorRoot;
env.NEO_TENANT_REPO_MIRROR_ROOT;
- hardcoded
'/app/.neo-ai-data'.
The last three duplicate one existing config leaf. ai/configBase.mjs already declares orchestrator.tenantRepoMirrorRoot with the /app/.neo-ai-data cloud default and the NEO_TENANT_REPO_MIRROR_ROOT env binding. The consumer can therefore disagree with the Provider and can silently keep running when the configured surface is absent. ai/scripts/lint/lint-config-template-ssot.mjs also carries an exact baseline exception for the method signature, so CI records rather than removes the drift.
Patch-blind falsifier: delete the leaf from an injected config stub while supplying env.NEO_TENANT_REPO_MIRROR_ROOT; the current method succeeds from the direct env branch. Under ADR 0019 the consumer must instead fail loud because the Provider surface did not resolve.
The original stale-overlay defense is no longer the right layer. Delta overlays inherit the base leaf; deprecated full-copy snapshots are explicitly migration debt and must not make every consumer a second config resolver.
The Architectural Reality
ai/configBase.mjs:721 is the owning surface: orchestrator.tenantRepoMirrorRoot declares the default, env binding, and planeMember: false rationale.
- ADR 0019 §3 A1 and §5 require an entrypoint-capable consumer that already imports
AiConfig to read the resolved leaf at the use site, with no direct env re-read or hidden default.
- ADR 0019 §10.5 explicitly preserves per-profile-pinned members. This leaf is cloud-profile-pinned, so the fix must not re-anchor it to
plane.dataRoot; the placement election owns any future unification.
TenantRepoSyncService remains the correct owning service because it materializes absent per-repo mirrorRoot values before GitMirror and ingestion consume them.
npm run --silent ai:structure-map -- --files --loc confirms the existing owner/sibling placement under ai/daemons/orchestrator/services/; no new module or directory is needed.
The Fix
- Simplify
resolveTenantReposConfig() to preserve the explicit tier1MirrorRoot test seam, otherwise read AiConfig.orchestrator.tenantRepoMirrorRoot directly at the use site.
- Remove the
orchestratorConfig and env resolver seams, optional chaining, direct process.env read, and hardcoded consumer default.
- Fail before config enumeration, Git access, or ingestion when the selected root is missing, empty, or whitespace-only.
- Preserve an explicit per-repo
mirrorRoot unchanged.
- Remove the exact
AI_CONFIG_PASS_THROUGH_BASELINE row that currently exempts this method.
- Replace the two stale-overlay fallback tests with Provider-SSOT and fail-loud coverage; retain the canonical no-double-segment and per-repo override witnesses.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
TenantRepoSyncService.resolveTenantReposConfig({tier1MirrorRoot, ingestionService}) |
ADR 0019 §§3, 5; #15931 |
Missing per-repo roots use the explicit test seam when supplied, otherwise the resolved AiConfig.orchestrator.tenantRepoMirrorRoot leaf |
Missing/blank selected root throws before the ingestion resolver is called; no direct env/default recovery |
Method JSDoc |
Focused positive, explicit-override, and side-effect-negative unit tests |
AiConfig.orchestrator.tenantRepoMirrorRoot |
ai/configBase.mjs |
Remains the sole owner of /app/.neo-ai-data default plus NEO_TENANT_REPO_MIRROR_ROOT env resolution |
No consumer fallback; deprecated snapshot overlays must migrate or fail loud |
Existing leaf JSDoc remains authoritative |
Exact source sweep + AiConfig template-SSOT lint |
tenantRepos[].mirrorRoot |
tenantRepoAccessContract.mjs; #12036 path contract |
Explicit per-repo value continues to win unchanged over the Tier-1 default |
Falsy/absent value materializes from the selected Tier-1 root |
Existing resolver JSDoc |
Existing override and no-double-segment regression tests |
AI_CONFIG_PASS_THROUGH_BASELINE entry for TenantRepoSyncService |
ADR 0019 enforcement stack |
Remove the exact exception once the signature no longer captures AiConfig or process.env as parameter defaults |
Reintroduction fails the lint instead of becoming recorded debt |
Baseline rationale removed with row |
npm run ai:lint-config-template-ssot |
Decision Record impact
aligned-with ADR 0019 — this ticket does not amend or supersede a Decision Record. It removes a PR-era defensive workaround that current ADR authority and inherited config substrate have rendered invalid. PR #12050 remains historical evidence for why the extra layers once existed, not authority to preserve them.
Acceptance Criteria
Out of Scope
- Changing the
/app/.neo-ai-data cloud default, env variable name, planeMember: false decision, or per-profile placement election.
- Migrating deprecated full-copy operator overlays;
migrateConfigOverlay.mjs owns that path.
- Changing
deriveTenantRepoMirrorPath() or the parent-of-tenant-repos contract.
- ConnectionService / #16008 / PR #16009, Neural Link membership #15984, or the broader #15931 census.
Avoided Traps
- Re-anchor the cloud-pinned leaf to
plane.dataRoot. Rejected: ADR 0019 §10.5 records this as a per-profile placement decision, not a generic plane-member derivation.
- Keep a smaller direct-env safety net. Rejected: two resolvers can disagree, and a deprecated overlay shape does not justify bypassing every current Provider.
- Mutate
AiConfig in tests. Rejected: ADR 0019 B4 forbids shared-singleton mutation. The existing explicit root seam supplies deterministic evidence.
- Close #15931 here. Rejected: the umbrella still owns the full derivation census, negative drain-loop finding, and sibling Neural Link receipt.
Evidence Plan
npx playwright test test/playwright/unit/ai/daemons/orchestrator/services/TenantRepoSyncService.spec.mjs \
-c test/playwright/playwright.config.unit.mjs --project=unit-brain --workers=1
npm run ai:lint-config-template-ssot
Related
Parent #15931 · sibling #16008 / PR #16009 · historical #12036 / PR #12050 · ADR 0019
Origin Session ID: 019f9e1e-2ef1-72c3-a04d-6bc67a531a8b
Retrieval Hint: query_raw_memories("a1-env-rederivation TenantRepoSyncService tenantRepoMirrorRoot PR 12050 ADR 0019"); exact source anchors ai/daemons/orchestrator/services/TenantRepoSyncService.mjs:1277-1323, ai/configBase.mjs:706-721, and ai/scripts/lint/lint-config-template-ssot.mjs AI_CONFIG_PASS_THROUGH_BASELINE.
Context
Narrow delivery leaf under #15931 after its required consumer census separated two different path owners. #16008 / PR #16009 owns the Neural Link stdio consumer; this ticket owns the remaining
TenantRepoSyncServiceresolver and leaves the umbrella open until both leaves and its full census are complete.The direct env/hardcoded bridge was introduced by PR #12050 for
#12036when a full-copy, gitignored config overlay could miss a newly added leaf. The current substrate has since changed:ai/config.mjsis a delta-only subclass ofai/configBase.mjs, so an unmentioned base leaf is inherited by construction. ADR 0019 now makes env resolution the leaf's exclusive responsibility and classifies a consumer-side direct env/default chain as A1 re-derivation.Live latest-open sweep: checked the latest 20 open issues at 2026-07-26T17:52:37Z; no equivalent ticket or in-flight A2A claim exists. Knowledge Base and local issue/discussion sweeps found the historical additive ticket
#12036/ PR #12050 and the current umbrella #15931, but no narrow removal leaf.The Problem
At current
dev,TenantRepoSyncService.resolveTenantReposConfig()still owns four resolution layers:tier1MirrorRoottest seam;orchestratorConfig?.tenantRepoMirrorRoot;env.NEO_TENANT_REPO_MIRROR_ROOT;'/app/.neo-ai-data'.The last three duplicate one existing config leaf.
ai/configBase.mjsalready declaresorchestrator.tenantRepoMirrorRootwith the/app/.neo-ai-datacloud default and theNEO_TENANT_REPO_MIRROR_ROOTenv binding. The consumer can therefore disagree with the Provider and can silently keep running when the configured surface is absent.ai/scripts/lint/lint-config-template-ssot.mjsalso carries an exact baseline exception for the method signature, so CI records rather than removes the drift.Patch-blind falsifier: delete the leaf from an injected config stub while supplying
env.NEO_TENANT_REPO_MIRROR_ROOT; the current method succeeds from the direct env branch. Under ADR 0019 the consumer must instead fail loud because the Provider surface did not resolve.The original stale-overlay defense is no longer the right layer. Delta overlays inherit the base leaf; deprecated full-copy snapshots are explicitly migration debt and must not make every consumer a second config resolver.
The Architectural Reality
ai/configBase.mjs:721is the owning surface:orchestrator.tenantRepoMirrorRootdeclares the default, env binding, andplaneMember: falserationale.AiConfigto read the resolved leaf at the use site, with no direct env re-read or hidden default.plane.dataRoot; the placement election owns any future unification.TenantRepoSyncServiceremains the correct owning service because it materializes absent per-repomirrorRootvalues before GitMirror and ingestion consume them.npm run --silent ai:structure-map -- --files --locconfirms the existing owner/sibling placement underai/daemons/orchestrator/services/; no new module or directory is needed.The Fix
resolveTenantReposConfig()to preserve the explicittier1MirrorRoottest seam, otherwise readAiConfig.orchestrator.tenantRepoMirrorRootdirectly at the use site.orchestratorConfigandenvresolver seams, optional chaining, directprocess.envread, and hardcoded consumer default.mirrorRootunchanged.AI_CONFIG_PASS_THROUGH_BASELINErow that currently exempts this method.Contract Ledger
TenantRepoSyncService.resolveTenantReposConfig({tier1MirrorRoot, ingestionService})AiConfig.orchestrator.tenantRepoMirrorRootleafAiConfig.orchestrator.tenantRepoMirrorRootai/configBase.mjs/app/.neo-ai-datadefault plusNEO_TENANT_REPO_MIRROR_ROOTenv resolutiontenantRepos[].mirrorRoottenantRepoAccessContract.mjs;#12036path contractAI_CONFIG_PASS_THROUGH_BASELINEentry forTenantRepoSyncServiceAiConfigorprocess.envas parameter defaultsnpm run ai:lint-config-template-ssotDecision Record impact
aligned-with ADR 0019 — this ticket does not amend or supersede a Decision Record. It removes a PR-era defensive workaround that current ADR authority and inherited config substrate have rendered invalid. PR #12050 remains historical evidence for why the extra layers once existed, not authority to preserve them.
Acceptance Criteria
resolveTenantReposConfig()contains no directprocess.envread, hardcoded/app/.neo-ai-datafallback,orchestratorConfigseam, or defensive optional access to the Provider subtree.mirrorRootresolves from the explicittier1MirrorRoottest seam when supplied, otherwise from the liveAiConfig.orchestrator.tenantRepoMirrorRootleaf.listConfiguredTenantRepos(), GitMirror, or ingestion side effects.tenant-repospath contract remain unchanged.TenantRepoSyncServiceconfig-parameter baseline exception is removed.TenantRepoSyncServiceunit coverage andnpm run ai:lint-config-template-ssotpass./app/.neo-ai-datadefault for this consumer path.Out of Scope
/app/.neo-ai-datacloud default, env variable name,planeMember: falsedecision, or per-profile placement election.migrateConfigOverlay.mjsowns that path.deriveTenantRepoMirrorPath()or the parent-of-tenant-reposcontract.Avoided Traps
plane.dataRoot. Rejected: ADR 0019 §10.5 records this as a per-profile placement decision, not a generic plane-member derivation.AiConfigin tests. Rejected: ADR 0019 B4 forbids shared-singleton mutation. The existing explicit root seam supplies deterministic evidence.Evidence Plan
npx playwright test test/playwright/unit/ai/daemons/orchestrator/services/TenantRepoSyncService.spec.mjs \ -c test/playwright/playwright.config.unit.mjs --project=unit-brain --workers=1 npm run ai:lint-config-template-ssotRelated
Parent #15931 · sibling #16008 / PR #16009 · historical
#12036/ PR #12050 · ADR 0019Origin Session ID: 019f9e1e-2ef1-72c3-a04d-6bc67a531a8b
Retrieval Hint:
query_raw_memories("a1-env-rederivation TenantRepoSyncService tenantRepoMirrorRoot PR 12050 ADR 0019"); exact source anchorsai/daemons/orchestrator/services/TenantRepoSyncService.mjs:1277-1323,ai/configBase.mjs:706-721, andai/scripts/lint/lint-config-template-ssot.mjsAI_CONFIG_PASS_THROUGH_BASELINE.