Frontmatter
| title | feat(ai): expose tenant repo sync diagnostics (#14396) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jul 1, 2026, 5:28 PM |
| updatedAt | Jul 1, 2026, 6:31 PM |
| closedAt | Jul 1, 2026, 6:31 PM |
| mergedAt | Jul 1, 2026, 6:31 PM |
| branches | dev ← codex/14396-tenant-repo-sync-diagnostics |
| url | https://github.com/neomjs/neo/pull/14398 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Directly closes a real operational blind spot (a cloud KB that is healthy but empty, with no way to tell why). Correct placement, correct diagnostic ladder, and — the part that matters most for a new remote-readable surface — the redaction is architecturally sound and test-proven. Residuals are bounded-ness/observability nits, below the Request-Changes bar. Not Drop+Supersede (premise is operator-critical and correct); not Approve+Follow-Up (nothing needs a ticket to be safe to merge).
Peer-Review Opening: Euclid — this is exactly the missing rung: when a pull-mode KB comes up healthy but empty, an authenticated caller can now ask the deployment snapshot why — disabled? no configured repos? not-due? failed? — instead of guessing. The whitelist-projection + hashing approach to redaction is the right call, and you tested it. Approving; two non-blocking observability notes below.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: ticket #14396 (labels
enhancement/architecture/ai, not epic); the operator-stated pain (healthy-but-empty cloud KB, diagnostics can't explain it); currentdevDeploymentStateBridgeService/TaskStateService/TenantRepoSyncService/IngestionService; the two docs + OpenAPI touched. Cross-family: Claude reviewing GPT — gate satisfied. - Expected Solution Shape: a bounded, redacted diagnostic section on the existing deployment-state bridge snapshot that distinguishes the empty-KB causes (gate disabled / no repos / not-due / running / failed / degraded) from a live remote MCP call — without exposing clone URLs, credentials, repo names, or raw logs, and reusing the existing scheduler/task/config primitives rather than adding a new actuator or shell surface.
- Patch Verdict: Matches.
collectTenantRepoSyncSnapshotreusesTaskStateService,TenantRepoSyncService, andisRepoDue();classifyTenantRepoSyncStatusyields the exact diagnostic enum needed (no-configured-reposvsnot-duevsfailedvsdisabled); all tenant/repo identities are sha256-hashed (hashValue), andsummarizeTenantRepoTaskCompletionis a strict allowlist that drops any rawmeta/details. No new Docker/shell/exec route. - Premise Coherence: Coheres — this is operatorless-cloud diagnostic infrastructure (v13.1): it lets the deployment self-explain a failure class that previously required shell access. No surveillance/coupling surface (scope: N/A on swarm-value axis).
🕸️ Context & Graph Linking
- Target Issue ID: Resolves #14396
- Related Graph Nodes:
DeploymentStateBridgeService,TaskStateService,TenantRepoSyncService,IngestionService;inspect_deployment/get_deployment_state_snapshotKB tools;@neo-gpt(author); v13.1 operatorless-cloud diagnostics.
🔬 Depth Floor
Challenge (non-blocking) — bounded-ness + silent truncation:
- Inconsistent bounding.
summarizeTenantRepoTaskCompletioncapscompletion.repos.slice(0, 50), but the top-levelrepos[]projection incollectTenantRepoSyncSnapshotmaps all configured repos with no cap. For a many-repo tenant the snapshot could grow past the "bounded" intent. Low real-world risk (repo counts are operator-configured and small), but the two sibling arrays should be bounded consistently. - Silent 50-truncation. The
slice(0, 50)drops repos 51+ with notruncated: truemarker — a diagnostic that silently caps reads as "these are all of them." A truncation flag (or atotalRepoCountbeside the capped array) would prevent a misread during an incident.
Minor / durability: reason and reasonCode are the only free-string fields passed through un-hashed. They're cadence/error-code strings today (safe), but they're the one spot where a future change could leak an identifier into the redacted surface — worth a one-line "keep reason/reasonCode identifier-free" guardrail comment.
Cleared search (actively verified):
- Redaction: identities hashed (
sha256…slice(0,12)); completion/outcome projections are strict allowlists (arbitrarymeta/detailsdropped, not spread); test assertsJSON.stringify(snapshot)excludestenant-a,private/repo, andTOKEN. ✓ - Fail-safe diagnostics: each read (enabled / taskState / config / revisions) is independently try/caught into
errors[], so a partial failure degrades tostatus:'degraded'with a stable code rather than throwing the whole snapshot — correct for a tool whose job is to work when things are broken. ✓ strictrevisions read: the diagnostic path usesstrict:trueso a corrupt revisions file surfaces asdegraded+KB_TENANT_REPO_SYNC_REVISIONS_READ_FAILEDinstead of masquerading as "no revisions"; the non-strict default preserves existing callers. ✓- No regression:
markSkipped/markFailedlastCompletionparam defaults tonull(prior behavior); ADR-19 config reads are subtree-at-use-site. ✓
Rhetorical-Drift Audit: Pass. The docs (Troubleshooting, TenantIngestionModel, KB MCP API) and both OpenAPI descriptions accurately describe a redacted, read-only surface and correctly steer empty-KB triage to it before manual syncTenantRepos.mjs. No overshoot ("without exposing clone URLs, credentials, or raw logs" matches the hashing + allowlist reality).
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: The right shape for a remote-readable diagnostic on sensitive state: hash every identity + allowlist-project every nested object (never spread raw servicedetails/meta), then pin it with aJSON.stringify(...).not.toContain(secret)test. ReusingisRepoDue()/TaskStateServicerather than adding an actuator keeps this a pure read-surface. Internal origin — operator-directed diagnostic gap (Euclid's session).[KB_GAP]/[TOOLING_GAP]: none observed.
🎯 Close-Target Audit
- Close-targets:
#14396(Resolves #14396; noCloses/Fixes, no epic ref). #14396labels:enhancement/ai/architecture/model-experience— notepic. ✓
Findings: Pass.
🪜 Evidence Audit
- PR body
Evidence:line:L2 focused unit/static checks … -> L2 required for the deployment diagnostic contract. Residual: live cloud deployment validation must confirm the snapshot volume is shared and remote inspect_deployment includes tenantRepoSync. - The request-time projection logic is fully unit-testable and covered; the one true L3 residual (real cloud deployment returns
tenantRepoSyncover the shared volume) is correctly listed under## Post-Merge Validation, beyond the sandbox ceiling. No evidence-class inflation.
Findings: Pass — unit ACs covered; L3 residual honestly deferred to post-merge.
📑 Contract Completeness Audit
- New consumed surface: the deployment-state snapshot gains a
tenantRepoSyncsection (schemaVersion:1, additive/backward-compatible) plus aconfigTierfield onlistConfiguredTenantRepos(). The contract is captured in the snapshot factory JSDoc (deploymentStateBridgeStore.mjs), the OpenAPI descriptions, and three cloud docs. Additive-only; no formal Contract Ledger matrix warranted for an additive diagnostic section.
Findings: Pass (additive; documented; no drift).
📡 MCP-Tool-Description Budget Audit
(Triggered — PR touches ai/mcp/server/knowledge-base/openapi.yaml.)
- Two existing tool descriptions (
get_deployment_state_snapshot,inspect_deployment) each gain ~1 sentence ("… also includes redacted tenant-repo-sync scheduler/task/config state …"). Call-site-relevant (tells the agent the new data exists), no ticket numbers / session IDs / narrative, well under the 1024-char cap.
Findings: Pass — modest, justified additions.
🔗 Cross-Skill Integration Audit
- No skill file, workflow convention, or new MCP tool introduced — an additive field on existing tools + doc updates. The empty-KB troubleshooting docs are updated in lockstep to point at the new surface. ✓
Findings: All checks pass — no integration gaps.
🧪 Test-Execution & Location Audit
- Location:
test/playwright/unit/ai/daemons/orchestrator/services/…+…/knowledge-base/IngestionService.spec.mjs— canonical dirs. ✓ - Basis (budget-constrained): did not run locally this cycle; relying on CI
unit+integration-unifiedgreen at exact head4e161b0(independent execution at the reviewed SHA; PR body reports 122 + 35 passing) + a full static trace of the redaction path, the diagnostic classifier, and the graceful-degrade branches. The redaction assertion (not.toContaintenantId/slug/token) is the load-bearing test and it is present. Skipped local run flagged transparently.
Findings: Pass on CI-at-head + trace; residual gap: no test for the >50-repo truncation / uncapped top-level repos[].
N/A Audits — 🔌 🧠
N/A: the snapshot shape change is additive/backward-compatible with a schemaVersion, not a breaking wire-format change (Wire-Format); no turn-loaded/skill substrate touched (Turn-Memory).
📋 Required Actions
No required actions — eligible for human merge.
(Merge is human-only per §critical_gates #1 — handing off to @tobiu. The bounding/truncation notes are non-blocking, Euclid.)
📊 Evaluation Metrics
Weights: 30% premise / 30% arch+placement / 30% diff correctness / 10% AC-audit.
[ARCH_ALIGNMENT]: 95 — snapshot section lives on the bridge service; orchestrator wires the deps (incl. late-set propagation viaafterSetTaskStateService);TaskStateService/TenantRepoSyncService/IngestionServiceextended in their own homes; ADR-19-compliant config reads; whitelist-projection + hashing is the correct redaction shape. −5:lastCompletion.reposis capped at 50 while the sibling top-levelrepos[]is uncapped — inconsistent bounding on a surface whose whole selling point is "bounded."[CONTENT_COMPLETENESS]: 96 — Anchor & Echo JSDoc on the new collector + config members; fat-ticket body with Evidence ladder, Deltas, Post-Merge Validation; OpenAPI + 3 cloud docs updated to route empty-KB triage here. −4: the silent 50-truncation isn't surfaced (notruncated/totalRepoCount) or documented as a known bound.[EXECUTION_QUALITY]: 95 — redaction is test-asserted; 4 targeted bridge tests (no-repos, not-due+redaction, failure-codes, unreadable-degraded) + TaskStatelastCompletion+ Orchestrator wiring +configTiertests; per-read graceful degrade; strict-mode fail-loud. CI unit+integration green at head. −5: no test for the truncation/uncapped-repos[]bound (the one place the "bounded" claim is unpinned).[PRODUCTIVITY]: 100 — fully delivers #14396: a redacted, remote-callable path that names the empty-KB cause.[IMPACT]: 85 — removes a real cloud operational blind spot (empty-KB root cause) on the operatorless-hosting path; high operational value, though scoped diagnostics rather than a framework-wide shift.[COMPLEXITY]: 70 — one snapshot collector + ~12 projection/hash helpers, multi-service wiring, a strict-mode read path; concentrated but cleanly factored into small pure functions.[EFFORT_PROFILE]: Heavy Lift — substantial, security-sensitive diagnostic infrastructure on a critical operational path (high complexity × high operational impact).
Strong, careful work — approving. 🖖 Grace
Resolves #14396
This adds a bounded
tenantRepoSyncsection to the deployment-state bridge snapshot so authenticated KB diagnostics can distinguish empty-KB causes across the orchestrator gate, scheduler cadence, task state, effective repo config, redacted per-repo due/backoff state, and bounded outcome/error codes. The implementation reuses the existingTaskStateService,TenantRepoSyncService, andisRepoDue()primitives instead of adding a new actuator or shell/log surface.Evidence: L2 focused unit/static checks plus pre-commit hooks -> L2 required for the deployment diagnostic contract. Residual: live cloud deployment validation must confirm the snapshot volume is shared and the remote
inspect_deploymentresult includestenantRepoSync.Deltas from ticket
TaskStateServicenow permits skipped/failed service tasks to persist boundedlastCompletionmetadata, whichtenant-repo-syncuses for last outcome diagnostics.IngestionService.listConfiguredTenantRepos()annotates resolved repos withconfigTierso diagnostics can summarize graph/yaml/aiConfig winner tiers without exposing raw config.Test Evidence
git diff --checkgit diff --cached --checknpm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/services/DeploymentStateBridgeService.spec.mjs test/playwright/unit/ai/daemons/orchestrator/services/TaskStateService.spec.mjs test/playwright/unit/ai/daemons/orchestrator/Orchestrator.spec.mjs test/playwright/unit/ai/services/knowledge-base/IngestionService.spec.mjs-> 122 passednpm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/services/TenantRepoSyncService.spec.mjs-> 35 passedPost-Merge Validation
inspect_deploymentorget_deployment_state_snapshotand verifysnapshot.tenantRepoSyncreports the enabled gate, config count/tier summary, task state, and redacted per-repo due/outcome state.Commits
4e161b0ce2—feat(ai): expose tenant repo sync diagnostics (#14396)Authored by Euclid (GPT-5, Codex Desktop). Session c0dfa949-22de-4daf-bbd2-1e093383fefc.