Frontmatter
| title | fix(orchestrator): unblock tenant-repo CLI sync path (#12036) |
| author | neo-opus-ada |
| state | Merged |
| createdAt | May 26, 2026, 9:11 PM |
| updatedAt | May 26, 2026, 9:39 PM |
| closedAt | May 26, 2026, 9:39 PM |
| mergedAt | May 26, 2026, 9:39 PM |
| branches | dev ← fix/12036-cloud-tenant-sync-substrate |
| url | https://github.com/neomjs/neo/pull/12037 |
🚨 Agent PR Body Lint Violation
@neo-opus-ada — your PR body on PR #12037 does not match the pull-request template structure.
Required action: read .agents/skills/pull-request/SKILL.md BEFORE editing the PR body. The skill points at:
- Minimum-viable PR body structure:
.agents/skills/pull-request/references/pull-request-workflow.md §9 - Self-Identification mandate:
.agents/skills/pull-request/references/pull-request-workflow.md §5
Do NOT compose a substitute template or hallucinate section headings. The validator checks more structural anchors than this comment names. The only reliable path to passing is reading the actual workflow file and following its structure.
Diagnostic hint: at least one recognized anchor like FAIR-band: is missing.
Visible anchors missing (full list)
FAIR-band:Evidence:## Test Evidence## Post-Merge Validation
This is the CI tool-boundary lint companion to PR #11494's MCP manage_pr_review validator and PR #11502's agent-pr-review-body-lint.yml reviewer-side lint.
Resolves #11501.

PR Review Summary
Status: Approve+Follow-Up
Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve+Follow-Up
- Rationale: The PR cleanly fixes the two mechanical blockers it claims to address, keeps Bug C out of scope with an explicit follow-up path, and has green CI plus a focused local re-run. Follow-up remains appropriate for the unresolved mirror-root config shape and the missing real-resolver test lane.
Thanks for putting this together. I reviewed the exact head 3b7f1cfc764b06a82068ff244d9f2b95badcf43b, checked the source paths behind both fixes, and ran the related unit spec locally.
Context & Graph Linking
- Target Epic / Issue ID: Refs #12036
- Related Graph Nodes: #11731, #12032, #12030, tenant-repo sync, cloud deployment substrate
Depth Floor
Challenge OR documented search (per guide §7.1):
- Follow-up concern: the PR body correctly notes that the existing
TenantRepoSyncService.spec.mjsstubsknowledgeBaseIngestionServiceand therefore missed the resolver/export drift. This PR is still mergeable because it fixes the runtime export and the related spec passes, but the real-resolver smoke should become a follow-up ticket/PR so this exact class of adapter drift does not recur.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches what the diff substantiates. It says Bug A + Bug B are fixed and Bug C is deferred.
- Anchor & Echo summaries: no new method-level architecture prose was added; Dockerfile comment is mechanically accurate.
-
[RETROSPECTIVE]tag: N/A; no retrospective tag in PR body. - Linked anchors: cited source paths match the diff and live source.
Findings: Pass. The only wording I would watch is "resolver should fail loud"; the implementation returns the canonical export directly, so future export drift would still surface at call-site unless a later test/guard is added. That is already covered by the follow-up concern, not a blocker for this PR.
Graph Ingestion Notes
[KB_GAP]: Existing unit coverage stubbed the ingestion service and did not exerciseresolveIngestionService, allowing export-name drift to escape.[TOOLING_GAP]: Local targeted test passed in this checkout; no tooling blocker. CI is green. I added missing PR labels (ai,ai-generated,bug,architecture) as maintainer metadata polish.[RETROSPECTIVE]: Small deployment substrate fixes can be high leverage when they remove Day-0 operator surgery from the cloud tenant-ingestion path.
N/A Audits - Provenance
N/A across listed dimension: this PR does not introduce a major new abstraction or external-origin algorithm.
Close-Target Audit
- Close-targets identified: none. PR body uses
Refs #12036; commit subject names#12036without a magic close keyword. - Epic-label check: N/A because no close target is present.
Findings: Pass.
Contract Completeness Audit
- Originating ticket contains the relevant architecture and ACs for Bug A/B/C.
- Implemented PR diff matches the partial scope declared in the PR body: Bug A + Bug B fixed, Bug C explicitly deferred.
Findings: Pass for partial-scope PR. It intentionally does not close #12036.
Evidence Audit
- PR body contains an
Evidence:declaration line. - Achieved evidence is sufficient for the PR's Bug A/B scope. Author declares L4 cloud tenant-ingestion smoke; CI and local unit test independently support the narrower code-level changes.
- Residuals are explicitly listed: Bug C ACs remain deferred on #12036.
- Review language does not promote local unit evidence to L4; the L4 claim remains author-provided deployment evidence.
Findings: Pass.
N/A Audits - MCP Tool Description Budget
N/A across listed dimension: PR does not touch ai/mcp/server/*/openapi.yaml.
Cross-Skill Integration Audit
- No skill files or global workflow conventions changed.
- No new MCP tool or convention introduced.
- Follow-up need is implementation-test coverage, not skill/docs propagation.
Findings: All checks pass - no integration gaps.
Test-Execution & Location Audit
- Branch checked out locally via
gh pr checkout 12037. - Canonical Location: no new/moved test files in this PR.
- Related test executed locally:
npm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/services/TenantRepoSyncService.spec.mjs- 27/27 passed.
- CI current head is green:
gh pr checks 12037reports CodeQL, retired-primitives check, PR-body lint, unit, and integration-unified all passing.
Findings: Tests pass.
Required Actions
No required actions - eligible for human merge.
Evaluation Metrics
[ARCH_ALIGNMENT]: 90 - 10 points deducted only because the in-process vs MCP ingestion route remains a documented architectural question; the actual Bug A/B fixes align with the current in-processviaMcp: falsepath and ADR 0014 deployment reality.[CONTENT_COMPLETENESS]: 90 - 10 points deducted for leaving the real-resolver test as a documented follow-up rather than adding it here; PR body, evidence, residuals, and close-target syntax are otherwise strong.[EXECUTION_QUALITY]: 95 - 5 points deducted because the resolver still lacks an explicit guard/test for future export drift, but current head fixes the wrong export and both CI plus local targeted tests pass.[PRODUCTIVITY]: 95 - 5 points deducted because #12036 remains partially open for Bug C; the PR achieves its declared Bug A/B unblock scope efficiently.[IMPACT]: 70 - Medium-high impact: two tiny changes unblock the cloud tenant-repo CLI sync path, but this is not a broad new subsystem.[COMPLEXITY]: 25 - Low complexity: two files, one Docker runtime dependency and one service export lookup correction.[EFFORT_PROFILE]: Quick Win - High ROI relative to the small diff and strong deployment-path evidence.
Approved with follow-up discipline: please keep Bug C and the real-resolver smoke from slipping, but neither blocks this Bug A/B merge.
Refs #12036
Authored by claude-opus-4-7 (Claude Code). Session 89412233-5a87-4e21-bd1a-492fca958fc1.
FAIR-band: under-target [9/30] — Self-Selection Rule 1 fires (under-band → bias toward author lane).
Fixes two of the three Day-0 substrate gaps captured in #12036 — the two mechanical fixes that unblock the operator-bulk CLI path
node ai/scripts/maintenance/syncTenantRepos.mjs --repo-slug <slug>end-to-end. Bug C (NEO_TENANT_REPO_MIRROR_ROOTenv var dead config) is deferred to a follow-up PR because it needs a design decision on global-default vs per-repo-override semantics before the mechanical fix.Evidence: L4 (first cloud tenant-ingestion smoke — orchestrator-CLI sync over OAuth-gated stack ingested 5 chunks into
neo-knowledge-basechroma collection, all correctly taggedtenantId/repoSlug/parserId/rootKind) → L4 required (AC6 first-time clean docker-compose path). Residual: Bug C ACs [#12036] deferred.Deltas from ticket (if any)
NEO_TENANT_REPO_MIRROR_ROOTbecomes a global default (with per-repo override semantics) or whether the canonical compose's env-var should simply be removed as a misleading dead config. Will surface in a separate PR after design review.KB_REVISION_BOUNDARY_INVALIDfirst-sync edge case + missingbranchRefintenantRepos[]schema) will be filed as separate tickets — they're orthogonal to the CLI-unblock scope.Bug A — Runtime image missing
gitai/deploy/Dockerfileruntime stage installed onlylibstdc++.node:24-alpinedoesn't ship withgit, butGitMirror.mjsshells out togit clone/git fetch. Failure:[Backup] failed to capture gitSha: spawn git ENOENTfrom orchestrator stderr; outer wrapperKB_TENANT_REPO_SYNC_SYNC_FAILED (GitMirror clone failed). Fix addsgitto the runtime-stage apk install (~23 MiB image-size delta).Bug B —
resolveIngestionServicewrong export nameTenantRepoSyncService.resolveIngestionServicelooked upservices.KB_KnowledgeBaseIngestionService || services.KnowledgeBaseIngestionService. Neither is exported — the canonical export isKB_IngestionService(ai/services.mjs:212,:282). Resolver always returnedundefined; the downstreamingestionService.ingestSourceFiles(...)NPE'd. Fix returnsKB_IngestionServicedirectly and drops the silent-undefined||chain — resolver should fail loud, not pretend.Architectural sub-question (recorded, not resolved)
The orchestrator's
viaMcp: falsehardcoded at line 442 implies a same-process model. Under ADR 0014's cloud topology, orchestrator + kb-server are separate containers; the orchestrator'sKB_IngestionServiceinstance shares chroma + sqlite with kb-server's instance but is otherwise an independent process. Is in-process the intended path, or should the orchestrator route through kb-server's MCPingestSourceFilestool? This PR picks the export-name fix (preserves in-process); empirical R3 verification confirms it works. If consensus is MCP-call, that's a separate ticket.Why tests didn't catch Bug B
test/playwright/unit/ai/daemons/orchestrator/services/TenantRepoSyncService.spec.mjsstubsknowledgeBaseIngestionServiceas a constructor option rather than exercisingresolveIngestionService. Same pattern as friction memory anchorfeedback_stub_tests_miss_adapter_drift(#11999): stub-based tests pass while real adapter path is broken. Worth a separate ticket for a "real-resolver" smoke that catches export drift; flagged here so it doesn't get lost.Test Evidence
docker compose up -d --build orchestratorproduces image withgit --versionresolving (git version 2.52.0). Verified inside running container viadocker compose exec orchestrator git --version.docker compose exec orchestrator node ai/scripts/maintenance/syncTenantRepos.mjs --repo-slug <slug>:neo-knowledge-basecollection via chromadb client returnscount=5, all 5 chunks filterable bywhere: {tenantId: '<tenant-id>'}and carrying full metadata schema (tenantId,repoSlug=<slug>,parserId=raw-text,rootKind=external-source,ingestedAt,hash,parserVersion,schemaVersion).TenantRepoSyncService.spec.mjshit a pre-existingNeo.ai.Confignamespace collision inunitTestModecaused by operator-overlay loading. Orthogonal to this PR (operator overlay hastenantRepos[]config which trips namespace strictness); CI runs from clean checkout where the overlay doesn't load, so this should pass there.Post-Merge Validation
docker compose up -d --buildproduces orchestrator image withgitresolvable.tenantRepos[]populated in gitignoredai/mcp/server/knowledge-base/config.mjscan runsyncTenantRepos.mjs --repo-slug <slug>withoutapk add gitworkaround.mirrorRootoverride in tenantRepos[] entries once that lands.Related
file:scheme — multi-tenant credential scaling)