From 3cbf4304189a8e3d890497ee1f76d09af2d5249a Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:58:28 -0700 Subject: [PATCH 1/2] chore(config): retire the .gittensory.yml config-filename dual-read shim The config-filename grace period from the LoopOver rebrand has elapsed: drop legacy .gittensory.yml/.yaml/.json support from every reader now that the new .loopover.yml name is the only one read. - src/selfhost/private-config.ts: collapse CONFIG_BASENAMES/CONFIG_EXTENSIONS to the 3 .loopover.* entries, drop the 3 legacy .gittensory.* entries, rewrite the module-header comment. - src/signals/focus-manifest-loader.ts: drop the 4 legacy entries from MANIFEST_FILE_CANDIDATES, strip dual-brand language from the doc comments. - src/review/guardrail-config.ts and its hand-duplicated engine twin (packages/gittensory-engine/src/review/guardrail-config.ts): drop the 6 legacy globs from CONFIG_AS_CODE_GUARDRAIL_GLOBS. - packages/gittensory-engine/src/objective-anchor.ts: drop the legacy entry from the CONFIG_FILENAMES classification set. - packages/gittensory-engine/src/predicted-gate.ts: drop the legacy filename from the predictor's public-facing note text; regenerate the engine-parity golden fixtures to match. - src/queue/processors.ts: drop the legacy filename from the E2E-test-gen-not-enabled comment posted to contributors. - Renames this repo's own root config file (.gittensory.yml -> .loopover.yml, .gittensory.yml.example -> .loopover.yml.example) to dogfood the new-only behavior. - Updates every affected test: removes/collapses the dual-brand describe blocks, and fixes stub fetch-URL matchers and expected-string assertions that were still keyed on the old filename (several of these were silent regressions -- a stale ".gittensory.y" URL matcher meant a test's manifest stub was never hit, and it was instead falling through to a real bundled fallback manifest). - Updates docs: config/examples/, CONTRIBUTING.md, README.md, the self-hosting doc pages, and the contributing-to-loopover skill. Part of #4777. --- .../skills/contributing-to-loopover/SKILL.md | 6 +- .../contributing-to-loopover/reference.md | 6 +- .env.example | 6 +- .github/workflows/visual-capture-fallback.yml | 2 +- .gittensory.minimal.yml | 6 +- .gittensory.yml => .loopover.yml | 2 +- ...nsory.yml.example => .loopover.yml.example | 14 +- CONTRIBUTING.md | 5 +- README.md | 4 +- .../config-generator-yaml-preview.test.tsx | 2 - .../config-generator-yaml-preview.tsx | 6 +- .../site/app-panels/maintainer-settings.tsx | 3 +- .../src/lib/config-generator-yaml.ts | 5 +- .../src/routes/docs.github-app.tsx | 5 +- .../src/routes/docs.gittensory-commands.tsx | 7 +- .../src/routes/docs.how-reviews-work.tsx | 7 +- .../routes/docs.maintainer-self-hosting.tsx | 22 +- .../src/routes/docs.owner-checklist.tsx | 10 +- .../src/routes/docs.privacy-security.tsx | 7 +- .../docs.self-hosting-configuration.tsx | 37 ++- .../routes/docs.self-hosting-operations.tsx | 2 +- .../routes/docs.self-hosting-quickstart.tsx | 3 +- apps/gittensory-ui/src/routes/docs.tuning.tsx | 31 +-- config/examples/README.md | 53 ++-- config/examples/TEMPLATES.md | 27 +- config/examples/gittensory.full.yml | 18 +- config/examples/gittensory.minimal.yml | 10 +- config/examples/global.gittensory.yml | 4 +- config/examples/repo-override.gittensory.yml | 6 +- config/examples/shared.gittensory.yml | 2 +- docker-compose.yml | 10 +- packages/gittensory-engine/README.md | 11 +- .../gittensory-engine/src/objective-anchor.ts | 3 +- .../gittensory-engine/src/predicted-gate.ts | 4 +- .../src/review/guardrail-config.ts | 12 +- packages/gittensory-mcp/README.md | 2 +- packages/gittensory-mcp/bin/gittensory-mcp.js | 4 +- .../docs/config-precedence.md | 2 +- .../gittensory-miner/docs/miner-goal-spec.md | 8 +- .../lib/self-review-context.js | 2 +- prometheus/rules/alerts.yml | 4 +- scripts/check-docs-drift.mjs | 40 +-- scripts/check-manifest-drift.mjs | 6 +- scripts/gittensory-config-lint.ts | 8 +- scripts/selfhost-update.sh | 2 +- src/config/gittensory-repo-focus-manifest.ts | 4 +- src/github/repo-doc-pr.ts | 10 +- src/queue/processors.ts | 2 +- src/review/guardrail-config.ts | 12 +- src/selfhost/private-config.ts | 99 +++---- src/signals/focus-manifest-loader.ts | 29 +-- .../predicted-gate/clean-pass-gittensor.ts | 2 +- .../clean-pass-oss-anti-slop.ts | 2 +- .../golden/ai-review-gate-mode-inert.json | 2 +- .../golden/cla-gate-mode-inert.json | 2 +- .../golden/clean-pass-gittensor.json | 2 +- .../golden/clean-pass-oss-anti-slop.json | 2 +- .../golden/duplicate-pr-block.json | 2 +- .../golden/first-time-grace-inert.json | 2 +- .../predicted-gate/golden/guardrail-hold.json | 2 +- .../golden/manifest-blocked-path.json | 2 +- .../merge-readiness-composite-block.json | 2 +- .../golden/missing-linked-issue-block.json | 2 +- .../golden/path-gated-check-with-paths.json | 2 +- .../path-gated-check-without-paths.json | 2 +- .../golden/readiness-warning.json | 2 +- .../self-authored-linked-issue-block.json | 2 +- test/unit/backfill.test.ts | 8 +- test/unit/change-guardrail.test.ts | 6 +- test/unit/check-docs-drift-script.test.ts | 14 +- test/unit/check-manifest-drift-script.test.ts | 10 +- test/unit/config-templates.test.ts | 8 +- test/unit/docs-owner-checklist.test.ts | 2 +- .../docs-selfhost-activation-paths.test.ts | 2 +- ...ctive-anchor-config-classification.test.ts | 13 +- test/unit/focus-manifest-loader.test.ts | 87 +------ test/unit/focus-manifest.test.ts | 26 +- .../gittensory-config-lint-script.test.ts | 10 +- test/unit/gittensory-focus-manifest.test.ts | 10 +- test/unit/guardrail-config.test.ts | 6 +- test/unit/mcp-automation-state.test.ts | 4 +- test/unit/mcp-refresh-repo-docs.test.ts | 2 +- test/unit/miner-goal-spec-doc.test.ts | 4 +- test/unit/miner-self-review-context.test.ts | 8 +- test/unit/predicted-gate.test.ts | 4 +- test/unit/private-config.test.ts | 246 ++++++------------ test/unit/queue-2.test.ts | 22 +- test/unit/queue-4.test.ts | 74 +++--- test/unit/queue-5.test.ts | 48 ++-- test/unit/queue-lifecycle-guards.test.ts | 10 +- test/unit/repo-doc-pr.test.ts | 12 +- test/unit/repo-doc-refresh-runner.test.ts | 2 +- test/unit/selfhost-config-examples.test.ts | 32 +-- wrangler.jsonc | 12 +- 94 files changed, 499 insertions(+), 807 deletions(-) rename .gittensory.yml => .loopover.yml (98%) rename .gittensory.yml.example => .loopover.yml.example (99%) diff --git a/.claude/skills/contributing-to-loopover/SKILL.md b/.claude/skills/contributing-to-loopover/SKILL.md index ed40691627..af57294afa 100644 --- a/.claude/skills/contributing-to-loopover/SKILL.md +++ b/.claude/skills/contributing-to-loopover/SKILL.md @@ -121,7 +121,7 @@ minor-version deprecation cycle. with `loopover_check_before_start` + `loopover_validate_linked_issue`. - **A linked, currently-open, unassigned, eligible issue is always required before opening a PR** — there is no "small enough to skip it" exemption, no matter how self-evident the fix looks. This - holds regardless of what the committed root `.gittensory.yml`'s `linkedIssuePolicy` says: that + holds regardless of what the committed root `.loopover.yml`'s `linkedIssuePolicy` says: that file is a non-representative **example** checked into the repo, not the live enforced rule. If no suitable open issue exists, open one yourself first, then link it from the PR. - **Run the pre-start checks** via MCP: `loopover_check_before_start` (is it claimed / a duplicate @@ -146,7 +146,7 @@ minor-version deprecation cycle. - Comments: sparse but dense; explain *why*, anchor non-obvious logic to an issue number (`(#1234)`). Don't narrate the obvious. - **Config-as-code parity:** a new per-repo gate/setting field must be wired in *every* site - (DB migration + Drizzle/types + the settings resolver + OpenAPI + the `.gittensory.yml` schema) + (DB migration + Drizzle/types + the settings resolver + OpenAPI + the `.loopover.yml` schema) in the **same** PR — partial wiring fails review. (See the per-repo-setting checklist in `reference.md`.) - UI: use design tokens (`text-token-*`, `rounded-token`, `border-hairline`, …) in @@ -250,7 +250,7 @@ Run the MCP predictor with your actual PR shape: - `loopover_lint_pr_text` — your commit + PR body must read as **strong**: Conventional Commit subject, traceability (a linked, currently-open, eligible issue — no no-issue rationale accepted), and a body that says what changed, why, and how it was validated. -- `gittensory_predict_gate` — simulate the repo's public `.gittensory.yml` gate. Resolve any +- `gittensory_predict_gate` — simulate the repo's public `.loopover.yml` gate. Resolve any predicted blocker (the duplicate-PR blocker is the one that hard-fails here) before opening. Resolve **every** finding before you push. The engine MERGES only a clean + green + gate-passing PR diff --git a/.claude/skills/contributing-to-loopover/reference.md b/.claude/skills/contributing-to-loopover/reference.md index f8114ccf8e..d2e51e7b35 100644 --- a/.claude/skills/contributing-to-loopover/reference.md +++ b/.claude/skills/contributing-to-loopover/reference.md @@ -114,7 +114,7 @@ Implications for you: - Owner / automation-bot PRs are exempt from auto-close, and crucial guarded-path PRs are held — but **assume you are a contributor** and that adverse = close. -`.gittensory.yml` (the public config you can predict against) sets the gate *modes* (`linkedIssue: +`.loopover.yml` (the public config you can predict against) sets the gate *modes* (`linkedIssue: advisory`, `duplicates: block`, `readiness: advisory/60`, AI review off) and the focus manifest (`wantedPaths`: `src/ packages/ test/ migrations/ scripts/ review-enrichment/ .github/workflows/ wrangler.jsonc apps/gittensory-ui/`; `blockedPaths`: `site/ CNAME **/lovable/**`; `linkedIssuePolicy: preferred`; @@ -260,9 +260,9 @@ absence of forbidden terms. Wire it in **every** site in the **same** PR, or review fails: 1. DB migration (`migrations/NNNN_*.sql`) for the new column/table. 2. Drizzle schema + types (`src/db/schema.ts`, `src/types.ts`). -3. The settings resolver / focus-manifest loader (so `.gittensory.yml` > DB > defaults still holds). +3. The settings resolver / focus-manifest loader (so `.loopover.yml` > DB > defaults still holds). 4. OpenAPI (`npm run ui:openapi`) for any new request/response field. -5. The `.gittensory.yml` schema + docs so contributors can set it. +5. The `.loopover.yml` schema + docs so contributors can set it. 6. Tests covering the new field's resolution precedence + the gate behavior it drives. --- diff --git a/.env.example b/.env.example index 2b72ef2c7c..8d8c92b3d5 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ # code path is fully inert — the review behaves as if the feature did not exist. # # See https://gittensory.aethereal.dev/docs/tuning for the full reference -# (flags, per-repo `.gittensory.yml` settings, and secret descriptions). +# (flags, per-repo `.loopover.yml` settings, and secret descriptions). # ============================================================================= # 1. Review feature flags (GITTENSORY_REVIEW_*) @@ -163,7 +163,7 @@ GITTENSORY_REVIEW_DRAFT=false # --- Optional (capability-gated; degrade safely when absent) ----------------- # GITHUB_OAUTH_CLIENT_ID= # GitHub OAuth (dashboard sign-in, draft flow) # GITHUB_OAUTH_CLIENT_SECRET= # GitHub OAuth; also required by the draft flow -# GITHUB_PUBLIC_TOKEN= # unauthenticated public-GitHub reads (.gittensory.yml fetch) +# GITHUB_PUBLIC_TOKEN= # unauthenticated public-GitHub reads (.loopover.yml fetch) # TOKEN_ENCRYPTION_SECRET= # AES-256-GCM master secret for maintainer BYOK keys at rest # DRAFT_TOKEN_ENCRYPTION_SECRET= # AES-256-GCM secret for the contributor OAuth token (draft flow) # GITTENSORY_REVIEW_STATS_TOKEN= # bearer token guarding the stats data endpoint @@ -706,7 +706,7 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review # # shared frontier claude-code/codex chain. Unset = every advisory # # capability stays on the shared chain. Even when set, a # # capability only actually routes through it once the repo's -# # .gittensory.yml settings.advisoryAiRouting also opts it in +# # .loopover.yml settings.advisoryAiRouting also opts it in # # (global default + per-repo override) -- this binding alone # # changes nothing. # AI_ADVISORY_API_KEY= # bearer credential for AI_ADVISORY_BASE_URL, if it requires one. diff --git a/.github/workflows/visual-capture-fallback.yml b/.github/workflows/visual-capture-fallback.yml index 73e85b5f34..50f54b1b9f 100644 --- a/.github/workflows/visual-capture-fallback.yml +++ b/.github/workflows/visual-capture-fallback.yml @@ -32,7 +32,7 @@ name: LoopOver Visual Capture Fallback # preview-deploy pipeline): copy this file, unmodified, into the target repo's `.github/workflows/` at this # EXACT path and name (`visual-capture-fallback.yml` / "LoopOver Visual Capture Fallback") -- gittensory's # dispatch call and workflow_run listener both key off this fixed name. Then set `review.visual.actions_fallback: -# true` in that repo's `.gittensory.yml` (see .gittensory.yml.example) to opt in; it activates ONLY when the +# true` in that repo's `.loopover.yml` (see .loopover.yml.example) to opt in; it activates ONLY when the # existing discovery chain finds no preview at all, so a repo with its own CI-produced preview is unaffected. on: diff --git a/.gittensory.minimal.yml b/.gittensory.minimal.yml index 8781ba9d9e..a1cb9b50b3 100644 --- a/.gittensory.minimal.yml +++ b/.gittensory.minimal.yml @@ -2,10 +2,10 @@ # .gittensory.minimal.yml — smallest safe starter config for a new repo # ============================================================================ # -# Copy this file to your repo root as `.gittensory.yml` and customize from here. -# (This filename is not read directly — only `.gittensory.yml` / `.github/gittensory.yml` are.) +# Copy this file to your repo root as `.loopover.yml` and customize from here. +# (This filename is not read directly — only `.loopover.yml` / `.github/loopover.yml` are.) # Also shipped at config/examples/gittensory.minimal.yml for self-host operators. -# For every supported field, defaults, and examples see `.gittensory.yml.example` or +# For every supported field, defaults, and examples see `.loopover.yml.example` or # config/examples/gittensory.full.yml. # # Safe by default: diff --git a/.gittensory.yml b/.loopover.yml similarity index 98% rename from .gittensory.yml rename to .loopover.yml index 3e74dafafe..84821a17af 100644 --- a/.gittensory.yml +++ b/.loopover.yml @@ -29,7 +29,7 @@ testExpectations: issueDiscoveryPolicy: discouraged # Authoritative gate config, config-as-code (layered OVER dashboard repository settings: -# .gittensory.yml > DB settings > safe defaults). ONLY confirmed Gittensor contributors are ever +# .loopover.yml > DB settings > safe defaults). ONLY confirmed Gittensor contributors are ever # hard-blocked (see PR #644); these fields only choose what the gate does, not who it applies to. gate: # enabled: false # set false to disable the gate from config (turning it on is a dashboard setting) diff --git a/.gittensory.yml.example b/.loopover.yml.example similarity index 99% rename from .gittensory.yml.example rename to .loopover.yml.example index 2806a2035c..e10819c549 100644 --- a/.gittensory.yml.example +++ b/.loopover.yml.example @@ -1,5 +1,5 @@ # ============================================================================ -# .gittensory.yml — per-repo configuration for gittensory CI & gittensory review +# .loopover.yml — per-repo configuration for gittensory CI & gittensory review # ============================================================================ # # Drop this file at the root of any repo gittensory watches to tune how the @@ -10,10 +10,10 @@ # config/examples/gittensory.minimal.yml — smallest safe starter (gate off, observe-only autonomy) # config/examples/gittensory.full.yml — exhaustive reference (body synced with this file) # .gittensory.minimal.yml — same minimal starter at repo root -# .gittensory.yml.example — this file +# .loopover.yml.example — this file # # WHERE IT LIVES (first match wins): -# .gittensory.yml → .github/gittensory.yml → .gittensory.json → .github/gittensory.json +# .loopover.yml → .github/loopover.yml → .loopover.json → .github/loopover.json # # PRECEDENCE (most specific wins): # this file > per-repo dashboard/API settings > built-in safe defaults @@ -404,7 +404,7 @@ gate: # defaults. # # SELF-HOST ONLY (`review.shared_config`, #2046): when `GITTENSORY_REPO_CONFIG_DIR` is mounted, -# place a shared review base at `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.gittensory.yml` (see +# place a shared review base at `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml` (see # `config/examples/shared.gittensory.yml`). Per-repo `review:` keys overlay it field-by-field — # repo value wins when set, shared fills gaps, defaults stay byte-identical. Absent shared base is # the common case and changes nothing. A malformed shared base warns and is ignored (never blocks a @@ -635,7 +635,7 @@ settings: # rarely formally assigned to a specific contributor), the reward label needs this flag to ever reach them. # Disabled by default (no mappings) -- a self-hoster opts in per repo. If your labels carry reward or # moderation weight, configure this in PRIVATE per-repo/global config (see `config/examples/README.md`) - # rather than the public `.gittensory.yml`, so contributors cannot see the exact mapping rules. A per-repo + # rather than the public `.loopover.yml`, so contributors cannot see the exact mapping rules. A per-repo # `mappings` override in the private-config layer REPLACES the global default list wholesale -- it does # not merge with it. # linkedIssueLabelPropagation: @@ -918,7 +918,7 @@ settings: # chatQaFrontierFallback: false # Opt-in only: falls back to the frontier env.AI chain if env.AI_ADVISORY is unconfigured, instead of declining. Meaningless unless chatQa is also true. Default: false. # intentRouting: false # Closed-set intent classifier for unrecognized @gittensory mentions -> existing Q&A commands only. Ollama-ONLY, same as chatQa (never uses chatQaFrontierFallback). Co-requisite: commandRateLimitPolicy: hold. Default: false. -# Maintainer AI review tuning (`.gittensory.yml` top-level `review:` block). These knobs shape the advisory AI +# Maintainer AI review tuning (`.loopover.yml` top-level `review:` block). These knobs shape the advisory AI # review prompt and file selection only — gate/slop/secret-scan are unaffected. # review: # # Globs whose matching files are dropped from the AI review (lockfiles, generated output, etc.). @@ -1073,7 +1073,7 @@ settings: # # Config-as-code enable/disable for this repo, layered ON TOP OF (never a replacement for) the # # GITTENSORY_REVIEW_SCREENSHOTS + per-repo cutover-allowlist env-var gate above (#4083). Bool or null. # # Default: null (unset) ⇒ defers entirely to that env-var gate's own decision -- byte-identical to today. -# # Explicit `false` (set once at the global-default `.gittensory.yml`, or overridden per-repo here) forces +# # Explicit `false` (set once at the global-default `.loopover.yml`, or overridden per-repo here) forces # # capture off for this repo even when the env-var gate would otherwise allow it. Explicit `true` opts this # # repo back in at a layer where a broader default disabled it -- it does NOT bypass the env-var gate # # itself, so the env vars remain the outer infra-availability switch. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e874a551b7..8b6ad6d518 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -282,7 +282,7 @@ Public GitHub surfaces: app/infra state) resolves to a neutral, non-blocking gate. Adding a blocker must keep it confirmed-contributor-gated through `evaluateGateCheck`. -Config as code (`.loopover.yml`; the legacy `.gittensory.yml` filename is dual-read indefinitely, #4773) — every repository setting is controllable from the config file: +Config as code (`.loopover.yml`) — every repository setting is controllable from the config file: - **`settings:`** is a partial of the repository settings: any behaviour a maintainer can toggle in the dashboard can be set here as code — `commentMode`, `publicAudienceMode`, `publicSurface`, `checkRunMode`, @@ -303,8 +303,7 @@ Config as code (`.loopover.yml`; the legacy `.gittensory.yml` filename is dual-r a hand-maintained AGENTS.md/CLAUDE.md — absent it, an existing hand-written file is left alone and generation is skipped. - Precedence: `.loopover.yml` `gate:` > `.loopover.yml` `settings:` > dashboard repository settings > - safe defaults; unset fields fall back to the next layer (the legacy `.gittensory.yml` filename works - identically wherever `.loopover.yml` does, #4773). The committed root `.gittensory.yml` is the + safe defaults; unset fields fall back to the next layer. The committed root `.loopover.yml` is the worked example. Resolved once in `resolveRepositorySettings`, so the whole app honours the file. - The config chooses **what** LoopOver does (gate on/off, blockers, comments, labels, surface, panel content); it never changes **who** can be blocked — only confirmed Gittensor contributors are ever diff --git a/README.md b/README.md index 5ac9e0bf45..4102e7aa43 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ LoopOver CI and LoopOver review score, gate, and comment on pull requests. The r - **CI + full-file grounding** — grounds the AI reviewer with the PR's finished CI status and the full post-change content of the changed files, so claims are verified against reality instead of predicted. - **Codebase RAG** — retrieval-augmented context that queries the codebase vector index for related callers, modules, and conventions and appends them to the reviewer prompt (additive only; inert until an index exists). - **Submitter-reputation gating** — an internal-only spend control that downgrades new / burst / low-reputation submitters to a deterministic-only review, never surfaced on any public comment, label, or check. -- **Unified review comment** — renders the public PR feedback as one in-place comment instead of multiple panels. With `.loopover.yml`'s `review.changed_files_summary` also on (off by default; legacy `.gittensory.yml` name also still works, indefinitely), it gains a deterministic, no-AI "Changed files" collapsible: one row per file category (source/test/docs/config/generated), with file counts and +/- totals. +- **Unified review comment** — renders the public PR feedback as one in-place comment instead of multiple panels. With `.loopover.yml`'s `review.changed_files_summary` also on (off by default), it gains a deterministic, no-AI "Changed files" collapsible: one row per file category (source/test/docs/config/generated), with file counts and +/- totals. - **Per-repo activation** — capabilities roll forward (and back) one flag and one repo at a time via the `GITTENSORY_REVIEW_REPOS` allowlist. **Check-run and comment surfaces, disambiguated** (a common point of confusion — these are three independent, separately-configured things, not layers of the same feature): @@ -46,7 +46,7 @@ LoopOver CI and LoopOver review score, gate, and comment on pull requests. The r - **`LoopOver Context`** (`settings.checkRunMode` / `settings.checkRunDetailLevel`, off by default) — a separate, purely advisory Check Run. At its default `checkRunDetailLevel: minimal` it publishes no findings at all; even at `standard`/`deep` it only re-renders content already shown elsewhere. Never make this one required. - **Inline review comments** (`GITTENSORY_REVIEW_INLINE_COMMENTS` + `.loopover.yml`'s `review.inline_comments`, off by both by default) — real, reply-able line-anchored PR review comment threads (CodeRabbit-style). This is the ONLY one of the three that posts an interactive per-line thread; the two check runs above never do. With `.loopover.yml`'s `review.suggestions` also on, a precise line-anchored fix is additionally rendered as a one-click, committable GitHub suggested-change block. With `review.finding_categories` also on (off by default), each finding is additionally tagged with a category — security/correctness/performance/maintainability/tests/style — in both the inline comment label and the unified comment's "Finding categories" collapsible; a deterministic path/keyword fallback covers whatever the model omits. -See [Tuning your reviews](https://gittensory.aethereal.dev/docs/tuning) for the full flag, setting, and `.loopover.yml` reference (the legacy `.gittensory.yml` filename is dual-read indefinitely — #4773 — so an existing repo's committed file keeps working unchanged). +See [Tuning your reviews](https://gittensory.aethereal.dev/docs/tuning) for the full flag, setting, and `.loopover.yml` reference. ## Start Here diff --git a/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.test.tsx b/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.test.tsx index 43902c70ee..412360c25a 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.test.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.test.tsx @@ -14,8 +14,6 @@ describe("ConfigGeneratorYamlPreview", () => { expect(screen.getByText("Preview")).toBeTruthy(); // The filename appears both in the descriptive copy and the CodeBlock's own filename label. expect(screen.getAllByText(".loopover.yml").length).toBeGreaterThanOrEqual(2); - // The legacy filename is still mentioned as also-supported (#4773), not silently dropped. - expect(screen.getAllByText(".gittensory.yml").length).toBeGreaterThanOrEqual(1); expect(screen.getByText(/combine: consensus/)).toBeTruthy(); expect(screen.getByText(/provider: anthropic/)).toBeTruthy(); expect(screen.getByRole("button", { name: "Copy code" })).toBeTruthy(); diff --git a/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.tsx b/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.tsx index 789e1e1692..bf5ce3b73c 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/config-generator-yaml-preview.tsx @@ -5,16 +5,14 @@ import { formStateToYaml, type GeneratorFormState } from "@/lib/config-generator * Read-only `.loopover.yml` preview for the config generator (#2210, part of #1683): renders the * current GeneratorFormState as text via CodeBlock (built-in copy-to-clipboard) so the output is * explicit and reviewable before a self-hoster saves or copies it. Purely presentational — field-group - * panels own collecting the form state. The legacy `.gittensory.yml` filename works identically if a - * self-hoster already has one committed (#4773) — this preview just names the new canonical filename. + * panels own collecting the form state. */ export function ConfigGeneratorYamlPreview({ formState }: { formState: GeneratorFormState }) { return (

Preview

- The exact .loopover.yml this configuration would produce - (the legacy .gittensory.yml name also still works). + The exact .loopover.yml this configuration would produce. Nothing is saved until you copy it into your repo.

diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx index 4441db1cb1..1e3e7ee5f8 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx @@ -651,8 +651,7 @@ type FocusManifestResponse = { manifest: unknown }; /** * Edit the repo's focus manifest as JSON. The manifest is repo-public config-as-code (it mirrors - * `.loopover.yml`, or the legacy `.gittensory.yml`, #4773); this surface lets a maintainer edit the - * API-record copy without committing a file. + * `.loopover.yml`); this surface lets a maintainer edit the API-record copy without committing a file. */ function FocusManifestEditor({ base }: { base: string | null }) { const [text, setText] = useState(""); diff --git a/apps/gittensory-ui/src/lib/config-generator-yaml.ts b/apps/gittensory-ui/src/lib/config-generator-yaml.ts index 528aef22ea..239f414cad 100644 --- a/apps/gittensory-ui/src/lib/config-generator-yaml.ts +++ b/apps/gittensory-ui/src/lib/config-generator-yaml.ts @@ -3,10 +3,9 @@ // siblings) own collecting GeneratorFormState, this module only turns it into text. Key names and // nesting match the real manifest schema (packages/gittensory-engine/src/focus-manifest.ts's // gateConfigToJson `gate.aiReview` shape, the same shape documented in this repo's own root -// .gittensory.yml under the commented-out `aiReview:` example) — no parallel schema. Every field is +// .loopover.yml under the commented-out `aiReview:` example) — no parallel schema. Every field is // optional so a fresh/partial form never produces invalid output; unset keys are omitted entirely. -// The generated header names the new-brand `.loopover.yml` filename; the legacy `.gittensory.yml` -// name still works identically if that's what a self-hoster already has committed (#4773). +// The generated header names the `.loopover.yml` filename. export type AiCombineStrategy = "single" | "consensus" | "synthesis"; export type AiProvider = "anthropic" | "openai"; diff --git a/apps/gittensory-ui/src/routes/docs.github-app.tsx b/apps/gittensory-ui/src/routes/docs.github-app.tsx index b548e9f682..1ba2a03a60 100644 --- a/apps/gittensory-ui/src/routes/docs.github-app.tsx +++ b/apps/gittensory-ui/src/routes/docs.github-app.tsx @@ -227,9 +227,8 @@ GET /v1/installations/:id/repair`}

Every setting can be committed to .loopover.yml at the repo root instead of, or - layered over, the dashboard (the legacy .gittensory.yml name also still works, - indefinitely — #4773). Precedence is .loopover.yml > repository settings - > safe defaults; an unset field falls back to the next layer. It only chooses{" "} + layered over, the dashboard. Precedence is .loopover.yml > repository + settings > safe defaults; an unset field falls back to the next layer. It only chooses{" "} what LoopOver does — a configured blocker gates every author identically, regardless of config.

diff --git a/apps/gittensory-ui/src/routes/docs.gittensory-commands.tsx b/apps/gittensory-ui/src/routes/docs.gittensory-commands.tsx index 04a77c2b9e..890683f6d7 100644 --- a/apps/gittensory-ui/src/routes/docs.gittensory-commands.tsx +++ b/apps/gittensory-ui/src/routes/docs.gittensory-commands.tsx @@ -102,7 +102,7 @@ function GittensoryCommandsReference() { Commands never flip the gate to advisory and never bypass the one-shot disposition.{" "} @@ -126,9 +126,8 @@ function GittensoryCommandsReference() {

Per-command authorization overrides

Default allowed roles ship in the worker configuration. A maintainer can tighten or widen a - single verb via commandAuthorization in .loopover.yml (or legacy{" "} - .gittensory.yml, #4773; resolved in the same order as other per-repo settings: - manifest → database → defaults). + single verb via commandAuthorization in .loopover.yml (resolved in + the same order as other per-repo settings: manifest → database → defaults).

- Defaults are quiet. With no settings and no .loopover.yml (or legacy{" "} - .gittensory.yml, #4773), the gate is off, AI review is{" "} - off, and the comment is posted only to detected contributors. Every capability - is an explicit opt-in. + Defaults are quiet. With no settings and no .loopover.yml, the gate is{" "} + off, AI review is off, and the comment is posted only to detected + contributors. Every capability is an explicit opt-in.

1. The gate: advisory vs. block

diff --git a/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx b/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx index eb857668d4..5352d34ad5 100644 --- a/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx +++ b/apps/gittensory-ui/src/routes/docs.maintainer-self-hosting.tsx @@ -225,9 +225,8 @@ function MaintainerSelfHosting() {
  • Mount ./gittensory-config and copy{" "} config/examples/global.gittensory.yml →{" "} - gittensory-config/.loopover.yml (the legacy{" "} - gittensory-config/.gittensory.yml name still works too, #4773) for a - centralized private default (per-repo files deep-merge on top). + gittensory-config/.loopover.yml for a centralized private default (per-repo + files deep-merge on top).
  • Add each pilot repo to GITTENSORY_REVIEW_REPOS, watch a PR in{" "} @@ -246,12 +245,12 @@ function MaintainerSelfHosting() { { title: "Single-command repo onboarding", description: - "Today: edit .env allowlist, copy YAML templates, sign into the panel, click activate. Proposed: one CLI/API command that adds owner/repo to GITTENSORY_REVIEW_REPOS, seeds gittensory-config/owner__repo/.loopover.yml (or legacy .gittensory.yml, #4773) from global.gittensory.yml, and POSTs activation — idempotent, dry-run aware.", + "Today: edit .env allowlist, copy YAML templates, sign into the panel, click activate. Proposed: one CLI/API command that adds owner/repo to GITTENSORY_REVIEW_REPOS, seeds gittensory-config/owner__repo/.loopover.yml from global.gittensory.yml, and POSTs activation — idempotent, dry-run aware.", }, { title: "Centralized private default only", description: - "Most fleets need one gittensory-config/.loopover.yml (legacy .gittensory.yml also still supported, #4773) with optional per-repo overrides — docs now treat that as the default story instead of implying every repo needs its own file.", + "Most fleets need one gittensory-config/.loopover.yml with optional per-repo overrides — docs now treat that as the default story instead of implying every repo needs its own file.", }, { title: "Advisory-by-default on first install", @@ -338,8 +337,7 @@ function MaintainerSelfHosting() {

    - One thing genuinely does carry over: a repo's own .loopover.yml (or - legacy .gittensory.yml, #4773) + One thing genuinely does carry over: a repo's own .loopover.yml {" "} (config-as-code), because it lives in the repository's git history, not in either service's database. resolveRepositorySettings overlays it on top of @@ -369,11 +367,11 @@ function MaintainerSelfHosting() { reopens (see the callout above). Once it does, the reverse migration has the same shape and the same gap: uninstall your self-host App from the repo, install the shared hosted App (see{" "} GitHub App configuration), and re-create any DB-backed - settings on the hosted side. .loopover.yml (or legacy{" "} - .gittensory.yml, #4773) again carries over for free since it travels with the - repo; nothing else does. Your self-host instance's data volumes are untouched by this — - see Uninstalling and decommissioning if you - also intend to shut the instance down rather than keep it idle or reuse it for other repos. + settings on the hosted side. .loopover.yml again carries over for free since it + travels with the repo; nothing else does. Your self-host instance's data volumes are + untouched by this — see{" "} + Uninstalling and decommissioning if you also + intend to shut the instance down rather than keep it idle or reuse it for other repos.

    ); diff --git a/apps/gittensory-ui/src/routes/docs.owner-checklist.tsx b/apps/gittensory-ui/src/routes/docs.owner-checklist.tsx index a124065f39..73a395bdbf 100644 --- a/apps/gittensory-ui/src/routes/docs.owner-checklist.tsx +++ b/apps/gittensory-ui/src/routes/docs.owner-checklist.tsx @@ -63,11 +63,11 @@ GET /v1/repos/:owner/:repo/gittensor-config-recommendation`}

    2. Repo policy & config quality

    - Your policy lives in .loopover.yml (or legacy .gittensory.yml, - #4773; sections: settings, gate, review). The - readiness report grades it as configQuality (excellent / good / needs_attention - / fragile); a fragile config is a hard blocker. Preview exactly what a - given config would do on a real PR before you commit it: + Your policy lives in .loopover.yml (sections: settings,{" "} + gate, review). The readiness report grades it as{" "} + configQuality (excellent / good / needs_attention / fragile); a{" "} + fragile config is a hard blocker. Preview exactly what a given config would + do on a real PR before you commit it:

    diff --git a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx index ce301ebd84..8c1ba0f952 100644 --- a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx +++ b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx @@ -62,10 +62,9 @@ function PrivacySecurity() {
  • Per-repo settings — gate modes, score thresholds, and guardrails, stored in the operator's database (set through the dashboard/API) or declared as config-as-code - in a repo's .loopover.yml (or legacy .gittensory.yml, #4773). - Choosing gate.slop.minScore or setting{" "} - settings.hardGuardrailGlobs tightens the gate without telling a contributor - how to bypass it. + in a repo's .loopover.yml. Choosing gate.slop.minScore or + setting settings.hardGuardrailGlobs tightens the gate without telling a + contributor how to bypass it.
  • Operator feature flags — the GITTENSORY_REVIEW_* family of diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx index 906a7e279a..f9879d300d 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx @@ -55,7 +55,7 @@ function SelfHostingConfiguration() { { title: "Public repo config", description: - "The repo .loopover.yml (legacy .gittensory.yml still works, #4773). Useful for transparent policy, but not for thresholds or rules you need to keep private.", + "The repo .loopover.yml. Useful for transparent policy, but not for thresholds or rules you need to keep private.", }, { title: "Built-in defaults", @@ -73,8 +73,7 @@ function SelfHostingConfiguration() {
    • the repo's .loopover.yml (public repo config, or the mounted private - per-repo config file below if GITTENSORY_REPO_CONFIG_DIR is set — the legacy{" "} - .gittensory.yml name still works everywhere, indefinitely, #4773), then + per-repo config file below if GITTENSORY_REPO_CONFIG_DIR is set), then
    • the per-repo database settings (the dashboard), then
    • built-in safe defaults.
    • @@ -103,9 +102,8 @@ function SelfHostingConfiguration() {

      Start from a template instead of reverse-engineering env flags, private-config precedence, and the parser. Every template uses the same schema for a public repo-root{" "} - .loopover.yml (or the legacy .gittensory.yml, still fully - supported, #4773) or a container-private GITTENSORY_REPO_CONFIG_DIR mount — - only what you put in each file differs. + .loopover.yml or a container-private GITTENSORY_REPO_CONFIG_DIR{" "} + mount — only what you put in each file differs.

      config/examples/TEMPLATES.md documents the public-vs-private split and how to apply the templates to gittensory, awesome-claude, and{" "} metagraphed without committing private policy. Lint before deploy:{" "} - npx tsx scripts/gittensory-config-lint.ts path/to/.loopover.yml (or the legacy{" "} - .gittensory.yml, #4773). + npx tsx scripts/gittensory-config-lint.ts path/to/.loopover.yml.

      Authoritative copies in git:

        @@ -162,8 +158,8 @@ cp config/examples/global.gittensory.yml gittensory-config/.loopover.yml`} config/examples/gittensory.full.yml {" "} (same body as{" "} - - .gittensory.yml.example + + .loopover.yml.example ) @@ -410,12 +406,12 @@ GITHUB_METADATA_CACHE_TTL_SECONDS=600`} { title: "Feature allowlist (env)", description: - "GITTENSORY_REVIEW_REPOS lists which repos run the converged per-PR path (safety, unified comment, grounding, RAG, reputation, …). Empty/unset ⇒ no repo runs those features, regardless of individual GITTENSORY_REVIEW_* flags. Per-repo features: overrides in a private or public .loopover.yml (or legacy .gittensory.yml, #4773) features: block can force on/off per repo (subject to env kill-switches).", + "GITTENSORY_REVIEW_REPOS lists which repos run the converged per-PR path (safety, unified comment, grounding, RAG, reputation, …). Empty/unset ⇒ no repo runs those features, regardless of individual GITTENSORY_REVIEW_* flags. Per-repo features: overrides in a private or public .loopover.yml features: block can force on/off per repo (subject to env kill-switches).", }, { title: "Gate activation (DB or private config)", description: - "The one-click POST …/activation endpoint bundles two independent axes into one advisory-first default: the review-check publish mode (reviewCheckMode: required, checkRunMode: enabled) and the actual per-dimension gate rules (linkedIssueGateMode, duplicatePrGateMode, qualityGateMode: all advisory; AI review still off). .loopover.yml's (or legacy .gittensory.yml's, #4773) gate.checkMode / gate.enabled only ever set the first axis (the check-run publish mode) — the dimension rules themselves are configured separately via gate.linkedIssue, gate.duplicates, gate.readiness.mode, etc. (see Tuning your reviews). Gate rule evaluation itself is never gated by checkMode/enabled/checkRunMode; those only control whether/how the check-run publishes on GitHub.", + "The one-click POST …/activation endpoint bundles two independent axes into one advisory-first default: the review-check publish mode (reviewCheckMode: required, checkRunMode: enabled) and the actual per-dimension gate rules (linkedIssueGateMode, duplicatePrGateMode, qualityGateMode: all advisory; AI review still off). .loopover.yml's gate.checkMode / gate.enabled only ever set the first axis (the check-run publish mode) — the dimension rules themselves are configured separately via gate.linkedIssue, gate.duplicates, gate.readiness.mode, etc. (see Tuning your reviews). Gate rule evaluation itself is never gated by checkMode/enabled/checkRunMode; those only control whether/how the check-run publishes on GitHub.", }, { title: "Gittensor registration (is_registered)", @@ -459,15 +455,12 @@ GITTENSORY_REVIEW_REPUTATION=false`} mount root) exists, the public repo .loopover.yml is never fetched for that review. With only one of the two present, its contents are used as-is; with both present, they are deep-merged — the per-repo file overlaid onto the global default, nested mappings - merging key by key and arrays replacing wholesale. The legacy .gittensory.yml{" "} - name is accepted everywhere .loopover.yml is (#4773) — when both names exist at - the same location, the new-brand file wins outright rather than being merged with the legacy - one. + merging key by key and arrays replacing wholesale.

        Config-as-code blocks with no dashboard equivalent

        Everything above has a dashboard row it mirrors. The fields below exist{" "} - only in .loopover.yml (or the legacy{" "} - .gittensory.yml, #4773) — there is no DB column or dashboard toggle for them, - so a self-host operator who never reads the example file may not know they exist. + only in .loopover.yml — there is no DB column or dashboard + toggle for them, so a self-host operator who never reads the example file may not know they + exist.

        gate.checkMode

        diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index e7862a0b0e..4b97663d33 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -902,7 +902,7 @@ SENTRY_ORG_SLUG=

      • ./gittensory-config/ bind mount — private per-repo{" "} - .loopover.yml (or legacy .gittensory.yml, #4773) policy. + .loopover.yml policy.
      • Named data volumes — especially loopover-data (SQLite DB, Codex/Claude auth diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx index 87df53eeb0..b08574869f 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-quickstart.tsx @@ -202,7 +202,7 @@ review_context_fetch_failed # REES/RAG/grounding context failure`} { title: "Gate activation (DB or private config)", description: - "Turns on the LoopOver check-run and deterministic gate rules for a repo. One-click via the control panel or POST /v1/repos/:owner/:repo/activation; or set gate.checkMode / gate.enabled in a mounted private .loopover.yml (legacy .gittensory.yml also still works, #4773).", + "Turns on the LoopOver check-run and deterministic gate rules for a repo. One-click via the control panel or POST /v1/repos/:owner/:repo/activation; or set gate.checkMode / gate.enabled in a mounted private .loopover.yml.", }, { title: "is_registered (Gittensor registry)", @@ -229,7 +229,6 @@ SELFHOST_DEPLOYMENT_MODE=dry-run # keep shadowing until you trust output`} lang="bash" code={`mkdir -p gittensory-config cp config/examples/global.gittensory.yml gittensory-config/.loopover.yml -# legacy: gittensory-config/.gittensory.yml also still works with zero changes (#4773) # optional per-repo override: mkdir -p gittensory-config/owner__my-repo cp config/examples/global.gittensory.yml gittensory-config/owner__my-repo/.loopover.yml`} diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx index 6849c17b7b..68db11ddad 100644 --- a/apps/gittensory-ui/src/routes/docs.tuning.tsx +++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx @@ -10,13 +10,13 @@ export const Route = createFileRoute("/docs/tuning")({ { name: "description", content: - "Configure LoopOver CI and LoopOver review: gate modes, score thresholds, guardrails, and feature flags via .loopover.yml (or legacy .gittensory.yml) and repo settings.", + "Configure LoopOver CI and LoopOver review: gate modes, score thresholds, guardrails, and feature flags via .loopover.yml and repo settings.", }, { property: "og:title", content: "Tuning your reviews — LoopOver docs" }, { property: "og:description", content: - "Configure LoopOver CI and LoopOver review: gate modes, score thresholds, guardrails, and feature flags via .loopover.yml (or legacy .gittensory.yml) and repo settings.", + "Configure LoopOver CI and LoopOver review: gate modes, score thresholds, guardrails, and feature flags via .loopover.yml and repo settings.", }, { property: "og:url", content: "/docs/tuning" }, ], @@ -30,7 +30,7 @@ function Tuning() {

        How configuration fits together

        @@ -42,8 +42,7 @@ function Tuning() {

      • Per-repo settings — gate modes, score thresholds, guardrails, and which surfaces are enabled. Set them in the dashboard, or declare them as config-as-code in a{" "} - .loopover.yml file in the repo (legacy .gittensory.yml also - still works, indefinitely — #4773). + .loopover.yml file in the repo.
      • Feature flags — the GITTENSORY_REVIEW_* family of @@ -69,19 +68,18 @@ function Tuning() { Every feature flag ships OFF. A repo with no settings and no{" "} - .loopover.yml (or legacy .gittensory.yml) falls back to a quiet, - non-blocking profile: the gate is off, AI review is off, slop - scoring is off, comments go only to detected contributors, and no check-run is - published. Turning anything on is always an explicit opt-in — you roll capabilities forward, - and back, one flag and one repo at a time. + .loopover.yml falls back to a quiet, non-blocking profile: the gate is{" "} + off, AI review is off, slop scoring is off, comments + go only to detected contributors, and no check-run is published. Turning anything on is + always an explicit opt-in — you roll capabilities forward, and back, one flag and one repo + at a time.

        Precedence

        Most specific wins:

        • - .loopover.yml in the repo (or legacy .gittensory.yml, #4773), - then + .loopover.yml in the repo, then
        • per-repo database settings, then
        • built-in safe defaults.
        • @@ -97,9 +95,7 @@ function Tuning() { gate-related fields and wins over the generic settings: block for those same fields. LoopOver looks for the manifest at the first match of .loopover.yml →{" "} .github/loopover.yml.loopover.json →{" "} - .github/loopover.json → (legacy, #4773) .gittensory.yml →{" "} - .github/gittensory.yml.gittensory.json →{" "} - .github/gittensory.json. + .github/loopover.json.

          Feature flags (GITTENSORY_REVIEW_*)

          @@ -530,8 +526,7 @@ function Tuning() {

          Example .loopover.yml

          A worked manifest: focus and validation up top, a refined gate, BYOK AI review, and a few - dashboard-equivalent overrides. Same schema, same effect, if you name the file{" "} - .gittensory.yml instead (legacy name, still fully supported — #4773). + dashboard-equivalent overrides.

          Install & trust. If you're self-hosting, see Self-host configuration{" "} for the environment layer these settings sit on top of, plus the config-precedence rules and - a link to the fully-commented .gittensory.yml.example. + a link to the fully-commented .loopover.yml.example.

          ); diff --git a/config/examples/README.md b/config/examples/README.md index c688b10d0a..68780c1702 100644 --- a/config/examples/README.md +++ b/config/examples/README.md @@ -14,13 +14,7 @@ public GitHub repo** on purpose: contributors can read a public manifest, so ant maintainer/admin allowlists, autonomy dials, and model/effort settings belong here instead, where only the self-host operator can see them. -> **Filename note (#4773):** the canonical manifest filename is now **`.loopover.yml`** (LoopOver's -> new brand); the pre-rebrand name **`.gittensory.yml`** is still accepted everywhere `.loopover.yml` -> is, indefinitely and with no code changes required from an existing self-hoster. Every path below -> is shown with the new-brand name; mentally substitute `.gittensory.yml` anywhere you already have -> one on disk today — it keeps working unchanged. When BOTH names exist at the exact same location, -> the new-brand file wins outright (its content is used; the legacy file there is not merged in) — -> see `src/selfhost/private-config.ts`'s `CONFIG_BASENAMES` for the authoritative precedence. +The canonical manifest filename is **`.loopover.yml`**. ## Directory layout @@ -34,12 +28,10 @@ ${GITTENSORY_REPO_CONFIG_DIR}/.loopover.yml # 4. global default, ${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml # 5. shared base (#1959), lowest priority ``` -Every one of 1, 2, 4, and 5 above ALSO accepts the legacy `.gittensory.yml` name at that same -location (tried after `.loopover.yml`, #4773) — omitted from the listing above purely for brevity, -not because it stopped working. `.yaml` and `.json` are accepted everywhere `.yml` is, for both -brand names. Every one of these files uses the **exact same schema** as the public manifest — see -[`gittensory.full.yml`](./gittensory.full.yml) (or [`.gittensory.yml.example`](../../.gittensory.yml.example) -at the repo root) for the exhaustive, field-by-field reference. For the smallest safe starter, copy +`.yaml` and `.json` are accepted everywhere `.yml` is. Every one of these files uses the **exact +same schema** as the public manifest — see [`gittensory.full.yml`](./gittensory.full.yml) (or +[`.loopover.yml.example`](../../.loopover.yml.example) at the repo root) for the exhaustive, +field-by-field reference. For the smallest safe starter, copy [`gittensory.minimal.yml`](./gittensory.minimal.yml) (or [`.gittensory.minimal.yml`](../../.gittensory.minimal.yml)) to your repo root as `.loopover.yml` or into your private mount and customize from there. @@ -49,19 +41,17 @@ From highest to lowest priority: 1. **Private per-repo file**, deep-merged over **2** and **3** when more than one exists (see below) — or used alone when it is the only private layer present. -2. **Private global default** (`${GITTENSORY_REPO_CONFIG_DIR}/.loopover.yml`, or legacy - `.gittensory.yml`, #4773) — deep-merged under **1** when both exist; used alone when a repo has - no per-repo file of its own and no shared base is mounted. +2. **Private global default** (`${GITTENSORY_REPO_CONFIG_DIR}/.loopover.yml`) — deep-merged + under **1** when both exist; used alone when a repo has no per-repo file of its own and no + shared base is mounted. 3. **Private shared base** (`${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml`, #1959) — the lowest-priority private layer, deep-merged under both **1** and **2**. An operator running many repos writes a house review policy (e.g. a default `review.tone`, `path_filters`, or `exclude_paths`) here **once** instead of copy-pasting it into every repo's per-repo file or - the global default. `.yaml`/`.json` are accepted, same as every other candidate, and so is the - legacy `.gittensory.yml` name (#4773). Absent (the default, common case) ⇒ byte-identical - behavior to the pre-#1959 2-layer chain. + the global default. `.yaml`/`.json` are accepted, same as every other candidate. Absent (the + default, common case) ⇒ byte-identical behavior to the pre-#1959 2-layer chain. 4. When **none** of the three private layers above exists, the loader falls back to the **public - repo `.loopover.yml`** (or `.github/loopover.yml`, or the legacy `.gittensory.yml`/ - `.github/gittensory.yml`, #4773) fetched from GitHub. + repo `.loopover.yml`** (or `.github/loopover.yml`) fetched from GitHub. 5. **Dashboard/API-stored settings** for the repo. 6. **Built-in safe defaults.** @@ -106,7 +96,7 @@ below for the shared base specifically): ### Example 1 — global defaults + a per-repo override -`.loopover.yml` (global default, at the config dir root; legacy `.gittensory.yml` works the same, #4773): +`.loopover.yml` (global default, at the config dir root): ```yaml settings: @@ -163,15 +153,15 @@ even the global default. That policy lives at: ${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml ``` -(`.yaml`/`.json`, and the legacy `.gittensory.yml`/`.yaml`/`.json` names (#4773), also accepted, same -lookup order as every other candidate — see [`shared.gittensory.yml`](./shared.gittensory.yml) for a -starter). It sits at the **lowest** priority of the three private layers: a per-repo file overlays a +(`.yaml`/`.json` also accepted, same lookup order as every other candidate — see +[`shared.gittensory.yml`](./shared.gittensory.yml) for a starter). It sits at the **lowest** +priority of the three private layers: a per-repo file overlays a global default, which overlays the shared base — the shared base fills in only the fields a higher layer is silent on. This is the exact same deep-merge helper and array-replace/explicit-null-clear semantics described above, folded across one more layer; it is not a new merge algorithm. -**Absent shared base is the default, common case** — with no `_shared/.loopover.yml` (or legacy -`_shared/.gittensory.yml`) mounted, behavior is byte-identical to the pre-#1959 2-layer chain. A +**Absent shared base is the default, common case** — with no `_shared/.loopover.yml` mounted, +behavior is byte-identical to the pre-#1959 2-layer chain. A malformed or unreadable shared file fails safe exactly like a malformed per-repo or global file always has: it is dropped from the merge and the remaining, still-valid layers combine as if it were never mounted — a broken shared base never blocks a review. When a shared `review:` block @@ -291,9 +281,9 @@ array-replace overlay semantics above) — it does not merge with it. - **Private config** (this directory): anti-abuse thresholds, the contributor cap, maintainer/ admin exemption logins, autonomy dials, model/effort overrides, and anything else you don't want a contributor reading and gaming. -- **Public `.loopover.yml`** (repo root, contributor-visible; legacy `.gittensory.yml` still works, - #4773): work-area guidance (`wantedPaths`), test expectations, and review-panel presentation — - nothing here should describe your private enforcement strategy. +- **Public `.loopover.yml`** (repo root, contributor-visible): work-area guidance (`wantedPaths`), + test expectations, and review-panel presentation — nothing here should describe your private + enforcement strategy. ## Safety @@ -301,5 +291,4 @@ Never commit real policy into this directory or into these example files: no mai no repo names, no thresholds beyond illustrative placeholders, no secrets or tokens. The `.gittensory.yml`-named template files shipped alongside this README (see the catalog in [TEMPLATES.md](./TEMPLATES.md)) are deliberately generic and inert — copy one into your own mounted -`GITTENSORY_REPO_CONFIG_DIR`, name the copy `.loopover.yml` (or keep the legacy name, #4773 — both -work), and edit the copy, not this one. +`GITTENSORY_REPO_CONFIG_DIR`, name the copy `.loopover.yml`, and edit the copy, not this one. diff --git a/config/examples/TEMPLATES.md b/config/examples/TEMPLATES.md index 9566b3e696..67a4c26c18 100644 --- a/config/examples/TEMPLATES.md +++ b/config/examples/TEMPLATES.md @@ -4,14 +4,10 @@ Copy-paste templates for the per-repo review manifest. Every file in this direct **same schema** whether it lives in a public repo root or a self-host private mount (`GITTENSORY_REPO_CONFIG_DIR`). -> **Filename note (#4773):** the canonical manifest filename is now **`.loopover.yml`** (LoopOver's -> new brand). The pre-rebrand name, **`.gittensory.yml`**, is still read everywhere `.loopover.yml` -> is — indefinitely, additively, with no expiry — so an existing self-hoster's committed -> `.gittensory.yml` keeps working with zero changes required. When both names are present at the -> same location, the new-brand `.loopover.yml` wins. New setups should name their own file -> `.loopover.yml`; the *template* filenames catalogued below (e.g. `gittensory.minimal.yml`) are a -> separate, unrelated naming concern and are left as-is — see the "Quick start" section for the -> destination filename you actually create. +> **Filename note:** the canonical manifest filename is **`.loopover.yml`**. The *template* +> filenames catalogued below (e.g. `gittensory.minimal.yml`) are a separate, unrelated naming +> concern and are left as-is — see the "Quick start" section for the destination filename you +> actually create. ## Template catalog @@ -25,14 +21,11 @@ Copy-paste templates for the per-repo review manifest. Every file in this direct Canonical copies of the minimal and full templates also live at the repo root as [`.gittensory.minimal.yml`](../../.gittensory.minimal.yml) and -[`.gittensory.yml.example`](../../.gittensory.yml.example). CI keeps the `config/examples/` copies +[`.loopover.yml.example`](../../.loopover.yml.example). CI keeps the `config/examples/` copies in sync with those files. ## Public repo root vs private self-host mount -Paths below show the new-brand `.loopover.yml` name; `.gittensory.yml` (and `.github/gittensory.yml` -for the public row) still works everywhere, indefinitely (#4773) — see the filename note above. - | Layer | Path | Who can read it | Typical contents | |-------|------|-----------------|------------------| | **Public** | `.loopover.yml` or `.github/loopover.yml` in git | Contributors | `wantedPaths`, test expectations, public review presentation | @@ -55,7 +48,6 @@ contributors to read) into a public repository. Copy `global.gittensory.yml` int ```bash cp config/examples/gittensory.minimal.yml .loopover.yml # edit wantedPaths / gate when ready -# (an existing .gittensory.yml at repo root also still works -- #4773 -- no need to rename it) ``` ### Self-host private mount (operator-only policy) @@ -79,9 +71,8 @@ into public git** — use the private mount for anything marked *private* below. ### `JSONbored/gittensory` (dogfooding) -- **Public** `.gittensory.yml` in the repo (the legacy name this repo's own committed config still - uses, per #4773's dual-read — a new repo would name this `.loopover.yml` instead): work-area - guardrails, test expectations, gate dimensions contributors should understand. +- **Public** `.loopover.yml` in the repo: work-area guardrails, test expectations, gate dimensions + contributors should understand. - **Private** `gittensory-config/` (gitignored locally, operator mount in production): fleet autonomy, anti-abuse caps, maintainer exemption lists — the same split described in [`global.gittensory.yml`](./global.gittensory.yml). @@ -107,8 +98,8 @@ into public git** — use the private mount for anything marked *private* below. Every template in this directory is parsed in CI (`test/unit/config-templates.test.ts` and `test/unit/selfhost-config-examples.test.ts`). The exhaustive template body is kept identical to -`.gittensory.yml.example` from `# WHERE IT LIVES` onward. Lint a local file before deploy: +`.loopover.yml.example` from `# WHERE IT LIVES` onward. Lint a local file before deploy: ```bash -npx tsx scripts/gittensory-config-lint.ts path/to/.loopover.yml # or path/to/.gittensory.yml (legacy, #4773) +npx tsx scripts/gittensory-config-lint.ts path/to/.loopover.yml ``` diff --git a/config/examples/gittensory.full.yml b/config/examples/gittensory.full.yml index 39fe571eeb..231231f11c 100644 --- a/config/examples/gittensory.full.yml +++ b/config/examples/gittensory.full.yml @@ -3,13 +3,13 @@ # ============================================================================ # # WHERE TO COPY (pick one): -# PUBLIC REPO — repo root as `.gittensory.yml` (or `.github/gittensory.yml`). Use for work-area +# PUBLIC REPO — repo root as `.loopover.yml` (or `.github/loopover.yml`). Use for work-area # guidance (`wantedPaths`, test expectations) that contributors may read. -# PRIVATE SELF-HOST — `${GITTENSORY_REPO_CONFIG_DIR}/.gittensory.yml` or a per-repo file under +# PRIVATE SELF-HOST — `${GITTENSORY_REPO_CONFIG_DIR}/.loopover.yml` or a per-repo file under # `owner__repo/`. Use for anti-abuse thresholds, maintainer allowlists, autonomy, and anything # contributors must not see or game. See `TEMPLATES.md` for public-vs-private split. # -# Canonical copy also lives at the repo root as `.gittensory.yml.example` (kept in sync by CI). +# Canonical copy also lives at the repo root as `.loopover.yml.example` (kept in sync by CI). # # STARTER TEMPLATES (this directory): # gittensory.minimal.yml — smallest safe copy-paste starter (gate off, observe-only autonomy) @@ -19,7 +19,7 @@ # shared.gittensory.yml — private cross-repo house policy, lowest-priority layer (#1959) # # ============================================================================ -# .gittensory.yml — per-repo configuration for gittensory CI & gittensory review +# .loopover.yml — per-repo configuration for gittensory CI & gittensory review # ============================================================================ # # Drop this file at the root of any repo gittensory watches to tune how the @@ -27,7 +27,7 @@ # config-as-code, versioned alongside the project it governs. # # WHERE IT LIVES (first match wins): -# .gittensory.yml → .github/gittensory.yml → .gittensory.json → .github/gittensory.json +# .loopover.yml → .github/loopover.yml → .loopover.json → .github/loopover.json # # PRECEDENCE (most specific wins): # this file > per-repo dashboard/API settings > built-in safe defaults @@ -418,7 +418,7 @@ gate: # defaults. # # SELF-HOST ONLY (`review.shared_config`, #2046): when `GITTENSORY_REPO_CONFIG_DIR` is mounted, -# place a shared review base at `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.gittensory.yml` (see +# place a shared review base at `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml` (see # `config/examples/shared.gittensory.yml`). Per-repo `review:` keys overlay it field-by-field — # repo value wins when set, shared fills gaps, defaults stay byte-identical. Absent shared base is # the common case and changes nothing. A malformed shared base warns and is ignored (never blocks a @@ -649,7 +649,7 @@ settings: # rarely formally assigned to a specific contributor), the reward label needs this flag to ever reach them. # Disabled by default (no mappings) -- a self-hoster opts in per repo. If your labels carry reward or # moderation weight, configure this in PRIVATE per-repo/global config (see `config/examples/README.md`) - # rather than the public `.gittensory.yml`, so contributors cannot see the exact mapping rules. A per-repo + # rather than the public `.loopover.yml`, so contributors cannot see the exact mapping rules. A per-repo # `mappings` override in the private-config layer REPLACES the global default list wholesale -- it does # not merge with it. # linkedIssueLabelPropagation: @@ -932,7 +932,7 @@ settings: # chatQaFrontierFallback: false # Opt-in only: falls back to the frontier env.AI chain if env.AI_ADVISORY is unconfigured, instead of declining. Meaningless unless chatQa is also true. Default: false. # intentRouting: false # Closed-set intent classifier for unrecognized @gittensory mentions -> existing Q&A commands only. Ollama-ONLY, same as chatQa (never uses chatQaFrontierFallback). Co-requisite: commandRateLimitPolicy: hold. Default: false. -# Maintainer AI review tuning (`.gittensory.yml` top-level `review:` block). These knobs shape the advisory AI +# Maintainer AI review tuning (`.loopover.yml` top-level `review:` block). These knobs shape the advisory AI # review prompt and file selection only — gate/slop/secret-scan are unaffected. # review: # # Globs whose matching files are dropped from the AI review (lockfiles, generated output, etc.). @@ -1087,7 +1087,7 @@ settings: # # Config-as-code enable/disable for this repo, layered ON TOP OF (never a replacement for) the # # GITTENSORY_REVIEW_SCREENSHOTS + per-repo cutover-allowlist env-var gate above (#4083). Bool or null. # # Default: null (unset) ⇒ defers entirely to that env-var gate's own decision -- byte-identical to today. -# # Explicit `false` (set once at the global-default `.gittensory.yml`, or overridden per-repo here) forces +# # Explicit `false` (set once at the global-default `.loopover.yml`, or overridden per-repo here) forces # # capture off for this repo even when the env-var gate would otherwise allow it. Explicit `true` opts this # # repo back in at a layer where a broader default disabled it -- it does NOT bypass the env-var gate # # itself, so the env vars remain the outer infra-availability switch. diff --git a/config/examples/gittensory.minimal.yml b/config/examples/gittensory.minimal.yml index 4b86507835..164c212a8b 100644 --- a/config/examples/gittensory.minimal.yml +++ b/config/examples/gittensory.minimal.yml @@ -3,16 +3,16 @@ # ============================================================================ # # WHERE TO COPY (pick one): -# PUBLIC REPO — repo root as `.gittensory.yml` (or `.github/gittensory.yml`). Contributors can read it. -# PRIVATE SELF-HOST — `${GITTENSORY_REPO_CONFIG_DIR}/.gittensory.yml` (global default), -# `${GITTENSORY_REPO_CONFIG_DIR}/owner__repo/.gittensory.yml` (per-repo override), or -# `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.gittensory.yml` (lowest-priority cross-repo base, #1959). +# PUBLIC REPO — repo root as `.loopover.yml` (or `.github/loopover.yml`). Contributors can read it. +# PRIVATE SELF-HOST — `${GITTENSORY_REPO_CONFIG_DIR}/.loopover.yml` (global default), +# `${GITTENSORY_REPO_CONFIG_DIR}/owner__repo/.loopover.yml` (per-repo override), or +# `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml` (lowest-priority cross-repo base, #1959). # Never commit real policy here to a public repo — use the private mount for thresholds, # allowlists, and autonomy. # # Canonical copy also lives at the repo root as `.gittensory.minimal.yml` (kept in sync by CI). # For every supported field, defaults, and allowed values see `gittensory.full.yml` or -# `.gittensory.yml.example`. See `TEMPLATES.md` in this directory for fleet examples. +# `.loopover.yml.example`. See `TEMPLATES.md` in this directory for fleet examples. # # Safe by default: # - Gate off (enable explicitly when you are ready) diff --git a/config/examples/global.gittensory.yml b/config/examples/global.gittensory.yml index 0e686cf0e8..38f5a028f1 100644 --- a/config/examples/global.gittensory.yml +++ b/config/examples/global.gittensory.yml @@ -3,10 +3,10 @@ # ============================================================================ # # Copy this file to the ROOT of your own GITTENSORY_REPO_CONFIG_DIR mount (e.g. -# `./gittensory-config/.gittensory.yml` for the default docker-compose.yml mount) and edit your +# `./gittensory-config/.loopover.yml` for the default docker-compose.yml mount) and edit your # copy — never this one. It applies to every repo that has no per-repo file of its own, and is # deep-merged UNDER any per-repo file that does exist (see ../README.md for the precedence chain -# and merge semantics; see ../../.gittensory.yml.example at the repo root for every supported +# and merge semantics; see ../../.loopover.yml.example at the repo root for every supported # field — this file uses the exact same schema, just a private location). # # Every value below is an illustrative placeholder. Replace the login and thresholds with your diff --git a/config/examples/repo-override.gittensory.yml b/config/examples/repo-override.gittensory.yml index f264fbae87..2ba1c69403 100644 --- a/config/examples/repo-override.gittensory.yml +++ b/config/examples/repo-override.gittensory.yml @@ -2,8 +2,8 @@ # Self-host PRIVATE per-repo override — GENERIC EXAMPLE, safe to publish # ============================================================================ # -# Copy this file to `${GITTENSORY_REPO_CONFIG_DIR}/{owner}__{repo}/.gittensory.yml` (or the bare -# `{repo}/.gittensory.yml` folder, or the flat `{owner}__{repo}.yml` file — see ../README.md) and +# Copy this file to `${GITTENSORY_REPO_CONFIG_DIR}/{owner}__{repo}/.loopover.yml` (or the bare +# `{repo}/.loopover.yml` folder, or the flat `{owner}__{repo}.yml` file — see ../README.md) and # edit your copy for a SPECIFIC repo. It is deep-merged OVER global.gittensory.yml: any key it # doesn't mention is inherited from the global default unchanged. # @@ -21,7 +21,7 @@ gate: # protection required-status-checks before relying on this, or GitHub will show it pending forever. checkMode: disabled # This repo's own CI job/check names to trust as required when its branch protection is - # unreadable (a common self-host case — see ../../.gittensory.yml.example). Each repo's CI + # unreadable (a common self-host case — see ../../.loopover.yml.example). Each repo's CI # naming differs, so this is a per-repo override rather than a global default. expectedCiContexts: - build diff --git a/config/examples/shared.gittensory.yml b/config/examples/shared.gittensory.yml index 53aba5c8c9..960b7595f7 100644 --- a/config/examples/shared.gittensory.yml +++ b/config/examples/shared.gittensory.yml @@ -2,7 +2,7 @@ # Self-host PRIVATE shared base — GENERIC EXAMPLE, safe to publish (#1959) # ============================================================================ # -# Copy this file to `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.gittensory.yml` and edit your copy — +# Copy this file to `${GITTENSORY_REPO_CONFIG_DIR}/_shared/.loopover.yml` and edit your copy — # never this one. This is the LOWEST-priority layer: a house review policy an operator running # MANY repos writes ONCE here instead of copy-pasting it into every repo's private config. It is # deep-merged UNDER both the global default (global.gittensory.yml) and any per-repo override diff --git a/docker-compose.yml b/docker-compose.yml index e446b6c439..de890df9ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,9 +89,9 @@ services: environment: PORT: "8787" DATABASE_PATH: /data/loopover.sqlite - # Container-private per-repo config (#1390): point at a mounted dir holding {owner}__{repo}/.gittensory.yml - # files layered over an optional root .gittensory.yml global default, itself layered over an optional - # _shared/.gittensory.yml cross-repo base for multi-repo operators (#1959) — a higher layer overrides only the + # Container-private per-repo config (#1390): point at a mounted dir holding {owner}__{repo}/.loopover.yml + # files layered over an optional root .loopover.yml global default, itself layered over an optional + # _shared/.loopover.yml cross-repo base for multi-repo operators (#1959) — a higher layer overrides only the # keys it sets; anything it doesn't mention is inherited from the next layer down — see # config/examples/README.md). Keeps each repo's policy OUT of the public GitHub repo so contributors can't # game the rules. Empty/unmounted ⇒ repos use the public file / defaults. @@ -142,8 +142,8 @@ services: # ORB_BROKER_URL: https://api.loopover.ai # override only for a private Orb deployment volumes: - loopover-data:/data - # Container-private per-repo config dir (read-only). Create ./gittensory-config/{owner}__{repo}/.gittensory.yml - # locally, optionally alongside a ./gittensory-config/.gittensory.yml global default that every per-repo file + # Container-private per-repo config dir (read-only). Create ./gittensory-config/{owner}__{repo}/.loopover.yml + # locally, optionally alongside a ./gittensory-config/.loopover.yml global default that every per-repo file # is deep-merged over (gitignored — never commit real policy; see config/examples/ for generic templates). # Absent ⇒ Docker mounts an empty dir ⇒ defaults apply. - ./gittensory-config:/config:ro diff --git a/packages/gittensory-engine/README.md b/packages/gittensory-engine/README.md index e13ba066f4..e1163bc3f2 100644 --- a/packages/gittensory-engine/README.md +++ b/packages/gittensory-engine/README.md @@ -194,9 +194,8 @@ samples in `metrics` rather than averaging noise into the calibration signal. `resolveGateVerdictCalibrationConfig()`, `ingestGateVerdictCalibrationSignals()`, and `computeGateVerdictCompositeCalibrationScore()` provide the pure engine contract for opt-in cross-product calibration. -The hosted review stack remains responsible for loading the repo's current `.loopover.yml` (or legacy -`.gittensory.yml`, dual-read indefinitely — #4773) or private config; the engine contract is -deliberately default-off and safe to call at ingestion time. +The hosted review stack remains responsible for loading the repo's current `.loopover.yml` or +private config; the engine contract is deliberately default-off and safe to call at ingestion time. The preferred config-as-code surface is: @@ -387,8 +386,8 @@ never copied into the Markdown, and the renderer fails closed if a blocked priva `resolveFindingSeverityCalibrationConfig()`, `ingestFindingSeverityCalibrationSignals()`, and `computeFindingSeverityCompositeCalibrationScore()` provide the pure engine contract for the opt-in finding-severity calibration signal. It sits in the same family as objective-anchor and pairwise-judge: the hosted review stack decides -whether a repo is opted in from its resolved `.loopover.yml`/private config (legacy `.gittensory.yml` also still -works, #4773), and the engine contract is deliberately default-off and safe to call at ingestion time. +whether a repo is opted in from its resolved `.loopover.yml`/private config, and the engine contract is +deliberately default-off and safe to call at ingestion time. The preferred config-as-code surface is: @@ -631,7 +630,7 @@ module defines the storage contract only — it does not wire into live governor ## MinerGoalSpec -`MinerGoalSpec` is the type surface for a repo's `.gittensory-miner.yml` (miner-side analogue of `.gittensory.yml`). +`MinerGoalSpec` is the type surface for a repo's `.gittensory-miner.yml` (miner-side analogue of `.loopover.yml`). `DEFAULT_MINER_GOAL_SPEC` is the safe default a repo with no file behaves as — minable (`minerEnabled: true`, an explicit opt-out), no path/label preferences, one concurrent claim, `neutral` discovery. diff --git a/packages/gittensory-engine/src/objective-anchor.ts b/packages/gittensory-engine/src/objective-anchor.ts index 4e5a28e1cc..80e64455b7 100644 --- a/packages/gittensory-engine/src/objective-anchor.ts +++ b/packages/gittensory-engine/src/objective-anchor.ts @@ -150,8 +150,7 @@ const CONFIG_FILENAMES = new Set([ ".env", ".env.example", ".env.selfhost.example", - ".loopover.yml", // new-brand manifest name (legacy .gittensory.yml kept below, dual-read indefinitely, #4773) - ".gittensory.yml", + ".loopover.yml", "package.json", "package-lock.json", "tsconfig.json", diff --git a/packages/gittensory-engine/src/predicted-gate.ts b/packages/gittensory-engine/src/predicted-gate.ts index eadc90c2ec..ef2f45be1d 100644 --- a/packages/gittensory-engine/src/predicted-gate.ts +++ b/packages/gittensory-engine/src/predicted-gate.ts @@ -63,7 +63,7 @@ export type PredictedGateVerdict = { }; const PREDICTED_GATE_NOTE_BASE = - "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have " + + "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have " + "private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only " + "evaluated on a real PR. "; // The slop score is ALWAYS disclaimed: it needs the diff CONTENT, which the metadata-only oracle never receives. @@ -224,7 +224,7 @@ export function buildPredictedGateVerdict(args: { const advisory = buildPullRequestAdvisory(repo, syntheticPr, { otherOpenPullRequests: openSiblings, requireLinkedIssue, linkedIssueAuthorLogins }); // Deterministic pre-merge checks parity (#11/#18): the LIVE gate enforces the repo's `review.pre_merge_checks` - // (from the SAME public .gittensory.yml the predictor already reads). With the PR's changed paths supplied, + // (from the SAME public .loopover.yml the predictor already reads). With the PR's changed paths supplied, // evaluate ALL of them exactly as live (path-gated checks now have their `whenPaths` to match against); without // paths, evaluate only the PATH-INDEPENDENT checks (empty `whenPaths` — title/description/label assertions), // whose inputs are exactly the real PR's, and disclaim the path-gated ones in the note. diff --git a/packages/gittensory-engine/src/review/guardrail-config.ts b/packages/gittensory-engine/src/review/guardrail-config.ts index 023ab4ffb4..fc5c12ba26 100644 --- a/packages/gittensory-engine/src/review/guardrail-config.ts +++ b/packages/gittensory-engine/src/review/guardrail-config.ts @@ -1,9 +1,7 @@ import type { RepositorySettings } from "../types/predicted-gate-types.js"; -// New-brand (`loopover`) and legacy (`gittensory`) entries are both listed — this is a Set-membership guardrail -// list (order doesn't matter, unlike the loaders' priority-ordered candidate lists), so a contributor PR touching -// EITHER the new canonical `.loopover.*` config file or a still-supported legacy `.gittensory.*` one gets the same -// hard-guardrail protection (#4773 — dual-read, additive; the legacy globs are never removed). +// This is a Set-membership guardrail list (order doesn't matter, unlike the loaders' priority-ordered candidate +// lists): a contributor PR touching the canonical `.loopover.*` config file gets hard-guardrail protection. export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [ ".loopover.yml", ".loopover.yaml", @@ -11,12 +9,6 @@ export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [ ".github/loopover.yml", ".github/loopover.yaml", ".github/loopover.json", - ".gittensory.yml", - ".gittensory.yaml", - ".gittensory.json", - ".github/gittensory.yml", - ".github/gittensory.yaml", - ".github/gittensory.json", "**/codecov.yml", "**/codecov.yaml", "**/.codecov.yml", diff --git a/packages/gittensory-mcp/README.md b/packages/gittensory-mcp/README.md index 257822b44f..b92a71cb1b 100644 --- a/packages/gittensory-mcp/README.md +++ b/packages/gittensory-mcp/README.md @@ -61,7 +61,7 @@ gittensory-mcp analyze-branch --login jsonbored --json gittensory-mcp preflight --login jsonbored --json gittensory-mcp review-pr --login jsonbored --commit "feat(mcp): add doctor grouping" --body "Fixes #160. Validated with npm test." --linked-issue 160 --json gittensory-mcp lint-pr-text --commit "feat(mcp): add doctor grouping" --body "Fixes #160. Validated with npm test." --linked-issue 160 --json -gittensory-mcp validate-config --file ./.loopover.yml --json # or ./.gittensory.yml (legacy name, #4773) +gittensory-mcp validate-config --file ./.loopover.yml --json gittensory-mcp slop-risk --changed-file src/widget.ts:80:2 --description "Adds retry handling." --test-file test/unit/widget.test.ts --json gittensory-mcp issue-slop --title "Add retry handling" --body "Widget reconnects fail without bounded retries." --json gittensory-mcp agent plan --login jsonbored --json diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index 9871adc608..7798e21997 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -464,7 +464,7 @@ const STDIO_TOOL_DESCRIPTORS = [ }, { name: "loopover_validate_config", - description: "Parse and validate a .gittensory.yml manifest string using the same focus-manifest parser as the server. Returns normalized config fields, parse warnings, and an ok/warn/error status. Metadata-only, no GitHub writes.", + description: "Parse and validate a .loopover.yml manifest string using the same focus-manifest parser as the server. Returns normalized config fields, parse warnings, and an ok/warn/error status. Metadata-only, no GitHub writes.", }, { name: "loopover_check_slop_risk", @@ -2061,7 +2061,7 @@ function printValidateConfigHelp() { [ "Usage: gittensory-mcp validate-config --file [--source repo_file|api_record|none] [--json]", "", - "Validate a .gittensory.yml manifest before pushing.", + "Validate a .loopover.yml manifest before pushing.", "Mirrors the gittensory_validate_config MCP tool and POST /v1/validate/focus-manifest. No source upload.", "", "Pass --json for machine-readable output.", diff --git a/packages/gittensory-miner/docs/config-precedence.md b/packages/gittensory-miner/docs/config-precedence.md index 35b178376e..86e43b284e 100644 --- a/packages/gittensory-miner/docs/config-precedence.md +++ b/packages/gittensory-miner/docs/config-precedence.md @@ -93,4 +93,4 @@ If a future change adds yml or CLI for a setting documented here as env-only, up - [`miner-goal-spec.md`](miner-goal-spec.md) — goal-spec field reference - [`env-reference.md`](env-reference.md) — generated operator env list -- ORB `.loopover.yml` (or legacy `.gittensory.yml`, #4773) precedence (`yml > DB > defaults`) in the main app — analogous documentation style, different runtime +- ORB `.loopover.yml` precedence (`yml > DB > defaults`) in the main app — analogous documentation style, different runtime diff --git a/packages/gittensory-miner/docs/miner-goal-spec.md b/packages/gittensory-miner/docs/miner-goal-spec.md index 0a932adf71..f6b748b9f2 100644 --- a/packages/gittensory-miner/docs/miner-goal-spec.md +++ b/packages/gittensory-miner/docs/miner-goal-spec.md @@ -11,14 +11,14 @@ Discovery order (first match wins): Every field is optional. Unknown keys are ignored; a malformed field falls back to its documented default with a warning — a broken file never hard-fails the miner. -## Relationship to `.gittensory.yml` +## Relationship to `.loopover.yml` | File | Actor | Purpose | |------|-------|---------| -| `.loopover.yml` (or legacy `.gittensory.yml`, dual-read indefinitely — #4773) | Review stack | How a maintainer's repo **reviews** incoming PRs (focus manifest, gate, scoring knobs). | +| `.loopover.yml` | Review stack | How a maintainer's repo **reviews** incoming PRs (focus manifest, gate, scoring knobs). | | `.gittensory-miner.yml` | Miner runtime | How a miner **searches for and prioritizes** work in a target repo. Unrelated naming concern — not affected by the review-config rebrand above. | -They are read by different components and do not conflict. A miner should still treat a target repo's public `.loopover.yml`/`.gittensory.yml` `wantedPaths` / `blockedPaths` as a hard floor when both files exist. +They are read by different components and do not conflict. A miner should still treat a target repo's public `.loopover.yml` `wantedPaths` / `blockedPaths` as a hard floor when both files exist. ## Fields @@ -32,7 +32,7 @@ Work areas the maintainer wants a miner to focus on. Glob list. Empty means no p ### `blockedPaths` (string list, default: `[]`) -Paths off-limits to a miner; candidates touching one should be skipped. Glob list. Mirrors `.gittensory.yml` `blockedPaths` semantics. +Paths off-limits to a miner; candidates touching one should be skipped. Glob list. Mirrors `.loopover.yml` `blockedPaths` semantics. ### `preferredLabels` (string list, default: `[]`) diff --git a/packages/gittensory-miner/lib/self-review-context.js b/packages/gittensory-miner/lib/self-review-context.js index fa9164d684..c6c098df37 100644 --- a/packages/gittensory-miner/lib/self-review-context.js +++ b/packages/gittensory-miner/lib/self-review-context.js @@ -26,7 +26,7 @@ const DEFAULT_MAX_PAGES = 10; // Mirrors src/signals/focus-manifest-loader.ts's MANIFEST_FILE_CANDIDATES exactly -- first candidate that // resolves wins, same as the live gate's own lookup order. -const MANIFEST_FILE_CANDIDATES = [".gittensory.yml", ".github/gittensory.yml", ".gittensory.json", ".github/gittensory.json"]; +const MANIFEST_FILE_CANDIDATES = [".loopover.yml", ".github/loopover.yml", ".loopover.json", ".github/loopover.json"]; function parseRepoFullName(repoFullName) { if (typeof repoFullName !== "string") return null; diff --git a/prometheus/rules/alerts.yml b/prometheus/rules/alerts.yml index eca4873db3..2b816568a1 100644 --- a/prometheus/rules/alerts.yml +++ b/prometheus/rules/alerts.yml @@ -479,7 +479,7 @@ groups: runbook: "Check provider health and circuit-breaker state (loopover_ai_provider_failures_total / loopover_ai_provider_circuit_open_total) and verify AI_PROVIDER credentials are still valid for every configured reviewer." - alert: LoopoverAiReviewOnMergeFloorBypassAttempted - # A repo's .gittensory.yml tried to loosen the operator's onMerge/combine/reviewer-count floor and + # A repo's .loopover.yml tried to loosen the operator's onMerge/combine/reviewer-count floor and # got clamped back to the operator's own configured minimum (#3901). A governance/config signal, not # an operational outage, but same absolute-increase style as LoopoverAiProviderCircuitOpen above: # any occurrence (> 0) over 1h is worth a look, sustained 5m so a single instantaneous blip doesn't @@ -491,7 +491,7 @@ groups: annotations: summary: "a repo's review-floor clamp fired" description: "{{ $value | printf \"%.0f\" }} onMerge/combine/reviewer-count clamp event(s) over the last 1h — a repo config tried to loosen below the operator's configured floor." - runbook: "Check which repo's .gittensory.yml requested the loosened setting (loopover_ai_review_onmerge_clamped_total is labeled by mode) and confirm whether that repo's floor should be adjusted or the request declined." + runbook: "Check which repo's .loopover.yml requested the loosened setting (loopover_ai_review_onmerge_clamped_total is labeled by mode) and confirm whether that repo's floor should be adjusted or the request declined." - alert: LoopoverAiProviderCircuitOpen # A provider's circuit breaker opens after AI_PROVIDER_FAILURE_THRESHOLD consecutive failures and diff --git a/scripts/check-docs-drift.mjs b/scripts/check-docs-drift.mjs index f7b0415042..dbd658c786 100644 --- a/scripts/check-docs-drift.mjs +++ b/scripts/check-docs-drift.mjs @@ -4,7 +4,7 @@ // @gittensory commands (src/github/commands.ts's two command catalogs), gate-mode dimensions (src/types.ts's // *GateMode fields on RepositorySettings) against specific docs pages, and -- the widened part (#4617) -- the // FULL RepositorySettings field surface plus every parseable FocusManifest field (packages/gittensory-engine) -// against .gittensory.yml.example. Nothing else in CI catches a docs page/example silently falling behind when +// against .loopover.yml.example. Nothing else in CI catches a docs page/example silently falling behind when // a new flag/command/gate-mode/settings/manifest field is added to source but the place documenting that // surface is never updated -- a reviewer has to notice by eye, and often doesn't (#4617's own audit found // `agentGlobalFreezeOverride` and `review.visual.production_url` this way: both fully live in code, neither @@ -90,25 +90,25 @@ export function extractRepositorySettingsFields(typesText) { } /** RepositorySettings fields deliberately excluded from the "every field must have SOME - * `.gittensory.yml.example` mention" check below, for three distinct reasons -- flagging any as "undocumented" + * `.loopover.yml.example` mention" check below, for three distinct reasons -- flagging any as "undocumented" * would be a false drift signal, not a real gap: * - Not a maintainer-settable knob at all: `repoFullName` is the row's own identity key (set once at * creation, the opposite of something a maintainer overrides via config); `createdAt`/`updatedAt` are * DB-row bookkeeping timestamps. * - `agentGlobalFreezeOverride`: genuinely settable, but DELIBERATELY never documented in the PUBLIC - * `.gittensory.yml.example` -- it is settable only from the self-host operator's own PRIVATE config + * `.loopover.yml.example` -- it is settable only from the self-host operator's own PRIVATE config * (`source: "api_record"` in `parseSettingsOverride`, packages/gittensory-engine/src/focus-manifest.ts), * never from a repo's own committed, maintainer-owned manifest (#4391's scope-leak fix). Documenting it in * the public example would misleadingly suggest a repo maintainer can set it themselves -- see the same * exclusion, with the same rationale, in `SETTINGS_OPERATOR_ONLY_FIELDS` in - * test/unit/focus-manifest.test.ts's `.gittensory.yml.example field-exhaustiveness` suite. (An #4617 audit + * test/unit/focus-manifest.test.ts's `.loopover.yml.example field-exhaustiveness` suite. (An #4617 audit * pass first flagged this field as an undocumented gap without that context; cross-checking the existing * exhaustiveness suite before "fixing" it here caught the false positive.) * - `skipAutomationBotAuthors`: genuinely settable (global env default + per-repo `inherit`/`off`/`enabled` * override, mirroring `moderationGateMode`'s shape), but DELIBERATELY not wired into the - * FocusManifest/`.gittensory.yml` parsing path -- DB-only for now, confirmed as an intentional scope choice + * FocusManifest/`.loopover.yml` parsing path -- DB-only for now, confirmed as an intentional scope choice * for this feature rather than an oversight. It is correctly absent from `FocusManifestSettings` (so the - * separate `.gittensory.yml.example` field-exhaustiveness suite never expected a token for it either). */ + * separate `.loopover.yml.example` field-exhaustiveness suite never expected a token for it either). */ const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set([ "repoFullName", "createdAt", @@ -117,15 +117,15 @@ const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set([ "skipAutomationBotAuthors", ]); -/** RepositorySettings fields whose `.gittensory.yml.example` documentation exists under a DIFFERENT, shorter +/** RepositorySettings fields whose `.loopover.yml.example` documentation exists under a DIFFERENT, shorter * name than the field itself -- almost always because the yml groups several sibling fields under one named * block (`gate.aiReview.*`, `gate.cla.*`, `gate.slop.*`, `gate.copycat.*`, `gate.readiness.*`) and so drops the * shared prefix the flat RepositorySettings field name carries to distinguish it from its siblings (e.g. * `aiReviewCloseConfidence` is documented as just `closeConfidence`, nested under the `aiReview:` block -- - * verified against the real `.gittensory.yml.example` for every row below). A field landing here is a + * verified against the real `.loopover.yml.example` for every row below). A field landing here is a * deliberate, reviewed judgment call that it IS genuinely documented, just not findable by a literal name * match -- unlike GATE_MODE_MANIFEST (checked against specific docs ROUTE pages), `aliases` here is checked - * against the WHOLE `.gittensory.yml.example` file, matching #4617's "SOME mention" ask, so one representative + * against the WHOLE `.loopover.yml.example` file, matching #4617's "SOME mention" ask, so one representative * alias per row is enough. Any `*GateMode` field is deliberately absent from this manifest even though its own * yml key is ALSO renamed the same way -- GATE_MODE_MANIFEST above already owns that exhaustive cross-check. */ export const SETTINGS_ALIAS_MANIFEST = [ @@ -152,7 +152,7 @@ export const SETTINGS_ALIAS_MANIFEST = [ { field: "requireFreshRebaseWindowMinutes", aliases: ["requireFreshRebaseWindow"] }, ]; -/** camelCase -> snake_case, matching the casing convention `.gittensory.yml`'s `review:` block (and everything +/** camelCase -> snake_case, matching the casing convention `.loopover.yml`'s `review:` block (and everything * nested under it, e.g. `review.visual.*`) uses for its own keys -- e.g. `productionUrl` -> `production_url`. * Every OTHER FocusManifest-reachable block keeps its source field's camelCase spelling verbatim in the yml * (matching the top-level manifest fields and the `gate:`/`settings:` blocks), for which this is a harmless @@ -217,7 +217,7 @@ export function extractFocusManifestFields(focusManifestText) { } /** FocusManifest leaf fields (dotted paths, same shape `extractFocusManifestFields` returns) whose - * `.gittensory.yml.example` documentation exists under a shorter name than their own doc comment's dotted-path + * `.loopover.yml.example` documentation exists under a shorter name than their own doc comment's dotted-path * tag would suggest -- e.g. `review.footerText`'s own field carries no `` `review.footer.text` `` tag at all * (unlike most of its siblings), and is in fact documented as just `footer:` (a nested `text:` sub-key). * Mirrors SETTINGS_ALIAS_MANIFEST's reasoning exactly, one level down. */ @@ -228,7 +228,7 @@ export const FOCUS_MANIFEST_ALIAS_MANIFEST = [ ]; // The real current *GateMode fields on RepositorySettings in src/types.ts. Each row maps the field to its -// .gittensory.yml alias(es) (the field's own DB/settings name, plus any config-as-code YAML path it is also +// .loopover.yml alias(es) (the field's own DB/settings name, plus any config-as-code YAML path it is also // known by) and the docs route filenames (relative to apps/gittensory-ui/src/routes/) that must document it. // Adding a new *GateMode field to src/types.ts without adding a row here is a docs-drift failure by design // (see checkDocsDrift step 3) -- the manifest is the single place that maps "a gate dimension exists" to @@ -258,7 +258,7 @@ function defaultReadFile(root, relativePath) { /** * Cross-check feature flags, @gittensory commands, gate-mode dimensions, the full RepositorySettings surface, * and every parseable FocusManifest field between their code source of truth and wherever they're meant to be - * documented exhaustively (specific docs pages for the first three; `.gittensory.yml.example` for the last + * documented exhaustively (specific docs pages for the first three; `.loopover.yml.example` for the last * two, #4617). `readFile(root, relativePath)` is injectable so tests can simulate a broken/incomplete docs * page or source file without touching the real filesystem. Returns `{ failures, counts }` -- pure given its * inputs, no process.exit/console side effects of its own (those live in main()). @@ -318,7 +318,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { const manifestFields = new Set(GATE_MODE_MANIFEST.map((row) => row.field)); for (const field of gateModeFields) { if (!manifestFields.has(field)) { - failures.push(`src/types.ts declares ${field} but GATE_MODE_MANIFEST in scripts/check-docs-drift.mjs has no entry for it -- add a row mapping it to its .gittensory.yml alias(es) and the docs pages that must document it`); + failures.push(`src/types.ts declares ${field} but GATE_MODE_MANIFEST in scripts/check-docs-drift.mjs has no entry for it -- add a row mapping it to its .loopover.yml alias(es) and the docs pages that must document it`); } } @@ -333,7 +333,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { } } - // 4. The FULL RepositorySettings surface (#4617): every field, not just *GateMode, vs .gittensory.yml.example. + // 4. The FULL RepositorySettings surface (#4617): every field, not just *GateMode, vs .loopover.yml.example. // A field passes when its literal name appears anywhere in the example file, when it's already covered // exhaustively by GATE_MODE_MANIFEST above (checked against docs pages, not repeated here), when it's judged // not yml-configurable at all (NOT_YML_CONFIGURABLE_SETTINGS_FIELDS), or when SETTINGS_ALIAS_MANIFEST records @@ -346,7 +346,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { } else { const gateModeManifestFields = new Set(GATE_MODE_MANIFEST.map((row) => row.field)); const settingsAliases = new Map(SETTINGS_ALIAS_MANIFEST.map((row) => [row.field, row.aliases])); - const ymlExampleText = read(".gittensory.yml.example"); + const ymlExampleText = read(".loopover.yml.example"); for (const field of repositorySettingsFields) { if (NOT_YML_CONFIGURABLE_SETTINGS_FIELDS.has(field)) continue; if (gateModeManifestFields.has(field)) continue; @@ -354,13 +354,13 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { const aliases = settingsAliases.get(field); if (aliases?.some((alias) => ymlExampleText.includes(alias))) continue; failures.push( - `.gittensory.yml.example: missing any mention of RepositorySettings field "${field}" -- document it there (or the relevant reference doc), or add a SETTINGS_ALIAS_MANIFEST row in scripts/check-docs-drift.mjs if it's already documented under a different yml key name`, + `.loopover.yml.example: missing any mention of RepositorySettings field "${field}" -- document it there (or the relevant reference doc), or add a SETTINGS_ALIAS_MANIFEST row in scripts/check-docs-drift.mjs if it's already documented under a different yml key name`, ); } } // 5. Every parseable FocusManifest field (#4617), excluding gate:/settings: (already exhaustively covered by - // step 4 above through their RepositorySettings mirror), vs .gittensory.yml.example. + // step 4 above through their RepositorySettings mirror), vs .loopover.yml.example. const focusManifestText = read("packages/gittensory-engine/src/focus-manifest.ts"); const focusManifestFields = extractFocusManifestFields(focusManifestText); if (focusManifestFields.length < 15) { @@ -369,7 +369,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { ); } else { const focusManifestAliases = new Map(FOCUS_MANIFEST_ALIAS_MANIFEST.map((row) => [row.field, row.aliases])); - const ymlExampleText = read(".gittensory.yml.example"); + const ymlExampleText = read(".loopover.yml.example"); for (const path of focusManifestFields) { const segments = path.split("."); const leaf = segments[segments.length - 1]; @@ -379,7 +379,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { if (aliases?.some((alias) => ymlExampleText.includes(alias))) continue; const prettyPath = segments.map(toSnakeCase).join("."); failures.push( - `.gittensory.yml.example: missing any mention of FocusManifest field "${prettyPath}" -- document it there, or add a FOCUS_MANIFEST_ALIAS_MANIFEST row in scripts/check-docs-drift.mjs if it's already documented under a different yml key name`, + `.loopover.yml.example: missing any mention of FocusManifest field "${prettyPath}" -- document it there, or add a FOCUS_MANIFEST_ALIAS_MANIFEST row in scripts/check-docs-drift.mjs if it's already documented under a different yml key name`, ); } } diff --git a/scripts/check-manifest-drift.mjs b/scripts/check-manifest-drift.mjs index f6e2038c35..5df28b07be 100644 --- a/scripts/check-manifest-drift.mjs +++ b/scripts/check-manifest-drift.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // Cross-checks the bundled fallback YAML in src/config/gittensory-repo-focus-manifest.ts -// (GITTENSORY_REPO_FOCUS_MANIFEST_YAML) against the real root .gittensory.yml. The bundled string exists so +// (GITTENSORY_REPO_FOCUS_MANIFEST_YAML) against the real root .loopover.yml. The bundled string exists so // the focus-manifest engine still has a sane default when the live repo file is unreachable (local dev, // pre-merge branches) -- see that file's own header comment -- but nothing in CI previously caught the two // silently diverging once someone edited one and forgot the other. This script parses both with the `yaml` @@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url"; import { parse as parseYaml } from "yaml"; import { GITTENSORY_REPO_FOCUS_MANIFEST_YAML } from "../src/config/gittensory-repo-focus-manifest.ts"; -const ROOT_MANIFEST_PATH = ".gittensory.yml"; +const ROOT_MANIFEST_PATH = ".loopover.yml"; function defaultReadFile(root, relativePath) { return readFileSync(join(root, relativePath), "utf8"); @@ -33,7 +33,7 @@ function sortKeysDeep(value) { } /** - * Deep-compares the real root .gittensory.yml against the bundled fallback YAML constant. `readFile(root, + * Deep-compares the real root .loopover.yml against the bundled fallback YAML constant. `readFile(root, * relativePath)` and `bundledYaml` are both injectable so tests can simulate a diverged pair without * touching the real filesystem or the real bundled constant. Returns `{ failures, rootManifest, * bundledManifest }` -- pure given its inputs, no process.exit/console side effects of its own (those live diff --git a/scripts/gittensory-config-lint.ts b/scripts/gittensory-config-lint.ts index 3b83627cf6..5d5e9b1a9f 100644 --- a/scripts/gittensory-config-lint.ts +++ b/scripts/gittensory-config-lint.ts @@ -1,6 +1,6 @@ #!/usr/bin/env tsx // Wires up the previously-unwired src/selfhost/config-lint.ts validator (#2906): a self-hoster (or the -// maintainer, dogfooding on JSONbored/gittensory) can now actually run it against a real .gittensory.yml or +// maintainer, dogfooding on JSONbored/gittensory) can now actually run it against a real .loopover.yml or // private-config file and get actionable feedback, instead of the validator existing only in its own test suite. import { lstatSync, readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; @@ -10,12 +10,12 @@ import { MAX_FOCUS_MANIFEST_BYTES } from "../src/signals/focus-manifest"; function usage(): string { return `Usage: npm run selfhost:config-lint -- [path] -Validates a Gittensory focus manifest (.gittensory.yml, a per-repo/global self-host +Validates a Gittensory focus manifest (.loopover.yml, a per-repo/global self-host private-config file, or any equivalent YAML/JSON file with the same shape) and reports unrecognized top-level fields and parser warnings, without echoing any of the file's values. Options: - path Manifest file to lint. Defaults to ".gittensory.yml" in the current directory.`; + path Manifest file to lint. Defaults to ".loopover.yml" in the current directory.`; } export function readManifestTextForLint(path: string): string { @@ -50,7 +50,7 @@ function main(): void { console.log(usage()); return; } - const path = args[0] ?? ".gittensory.yml"; + const path = args[0] ?? ".loopover.yml"; let text; try { text = readManifestTextForLint(path); diff --git a/scripts/selfhost-update.sh b/scripts/selfhost-update.sh index f341a06e69..37ed0f81aa 100755 --- a/scripts/selfhost-update.sh +++ b/scripts/selfhost-update.sh @@ -11,7 +11,7 @@ # # What this preserves untouched (all already gitignored -- see .gitignore): # - .env and any *_FILE secret mounts -# - gittensory-config/ (private per-repo .gittensory.yml policy) +# - gittensory-config/ (private per-repo .loopover.yml policy) # - .deploy-backups/ (operator deploy-backup snapshots) # - any *.local or docker-compose.local-*.yml compose override, or alertmanager config files # - named data volumes (loopover-data, loopover-pg, qdrant-data, loopover-backups, diff --git a/src/config/gittensory-repo-focus-manifest.ts b/src/config/gittensory-repo-focus-manifest.ts index 88c1b872e2..96748263bc 100644 --- a/src/config/gittensory-repo-focus-manifest.ts +++ b/src/config/gittensory-repo-focus-manifest.ts @@ -1,6 +1,6 @@ /** * Bundled fallback for JSONbored/gittensory when the repo file is not yet reachable - * (local dev, pre-merge branches). Keep aligned with `.gittensory.yml` at repo root. + * (local dev, pre-merge branches). Keep aligned with `.loopover.yml` at repo root. */ export const GITTENSORY_REPO_FOCUS_MANIFEST_YAML = `# Gittensory repo focus manifest — machine-readable contributor policy for this project. # Private maintainerNotes stay in authenticated API surfaces only. @@ -33,7 +33,7 @@ testExpectations: issueDiscoveryPolicy: discouraged # Authoritative gate config, config-as-code (layered OVER dashboard repository settings: -# .gittensory.yml > DB settings > safe defaults). ONLY confirmed Gittensor contributors are ever +# .loopover.yml > DB settings > safe defaults). ONLY confirmed Gittensor contributors are ever # hard-blocked (see PR #644); these fields only choose what the gate does, not who it applies to. gate: # enabled: false # set false to disable the gate from config (turning it on is a dashboard setting) diff --git a/src/github/repo-doc-pr.ts b/src/github/repo-doc-pr.ts index 2edc318b19..e0b7354413 100644 --- a/src/github/repo-doc-pr.ts +++ b/src/github/repo-doc-pr.ts @@ -11,14 +11,14 @@ // gets a pull request with everything outside the markers preserved byte-for-byte, and a repo whose marker // block is missing or malformed gets neither a silent overwrite nor a guess -- just a reported reason. // -// CONFIG-AS-CODE GATE (#3002): this whole feature is opt-in per repo via `.gittensory.yml repoDocGeneration:` +// CONFIG-AS-CODE GATE (#3002): this whole feature is opt-in per repo via `.loopover.yml repoDocGeneration:` // (src/signals/focus-manifest.ts) -- a manifest-only surface with no DB-backed counterpart, since there is no // dashboard toggle for it. `enabled`/`scope` are checked BEFORE any profile extraction or GitHub call (the // common case is disabled, so this must be cheap); `allowOverwriteExisting` is checked later, once refresh // reports `manual-review-required` (the "this file looks hand-maintained" signal), and lets that specific case // proceed as a fresh wholesale generate instead of skipping. // -// SKILL FILE, ADDITIVE (#3001): when `.gittensory.yml repoDocGeneration.scope` includes `"skills"` AND the repo +// SKILL FILE, ADDITIVE (#3001): when `.loopover.yml repoDocGeneration.scope` includes `"skills"` AND the repo // profile's contribution workflow warrants one (src/review/repo-skill-render.ts's shouldGenerateRepoSkill), a // generated skill file rides along in the SAME commit/PR as AGENTS.md/CLAUDE.md -- there is no parallel // delivery path. It gets its OWN marker pair and its own refreshGeneratedDoc call (reused unchanged, per that @@ -121,7 +121,7 @@ Every fact above was read directly from this repository, not templated or guesse ## Opting out -Set \`repoDocGeneration.enabled: false\` in this repository's \`.gittensory.yml\` (or simply close this pull request) -- no further action is taken until it is re-enabled. +Set \`repoDocGeneration.enabled: false\` in this repository's \`.loopover.yml\` (or simply close this pull request) -- no further action is taken until it is re-enabled. `; } @@ -144,8 +144,8 @@ export async function openRepoDocPullRequest(env: Env, repoFullName: string, mod if (!repository?.installationId) return { opened: false, reason: "repository is not installed" }; const manifest = await loadRepoFocusManifest(env, repoFullName); - if (!manifest.repoDocGeneration.enabled) return { opened: false, reason: "repo-doc generation is not enabled for this repository (.gittensory.yml repoDocGeneration.enabled)" }; - if (!manifest.repoDocGeneration.scope.includes("agents")) return { opened: false, reason: 'repo-doc generation scope does not include "agents" for this repository (.gittensory.yml repoDocGeneration.scope)' }; + if (!manifest.repoDocGeneration.enabled) return { opened: false, reason: "repo-doc generation is not enabled for this repository (.loopover.yml repoDocGeneration.enabled)" }; + if (!manifest.repoDocGeneration.scope.includes("agents")) return { opened: false, reason: 'repo-doc generation scope does not include "agents" for this repository (.loopover.yml repoDocGeneration.scope)' }; const profile = await extractRepoProfile(env, repoFullName); if (!profile.present) return { opened: false, reason: profile.reason }; diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 9eb4f0f802..f1b5c8b0eb 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -11117,7 +11117,7 @@ async function postGenerateTestsNotEnabledComment(env: Env, installationId: numb "", "> [!NOTE]", "> **E2E test generation is not enabled for this repository**", - "> Ask a maintainer to enable `features.e2eTests` in `.gittensory.yml` (the operator's global flag must also be on).", + "> Ask a maintainer to enable `features.e2eTests` in `.loopover.yml` (the operator's global flag must also be on).", "", "---", gittensoryFooter(env), diff --git a/src/review/guardrail-config.ts b/src/review/guardrail-config.ts index 82d74f9d48..bdd48cf262 100644 --- a/src/review/guardrail-config.ts +++ b/src/review/guardrail-config.ts @@ -1,9 +1,7 @@ import type { RepositorySettings } from "../types"; -// New-brand (`loopover`) and legacy (`gittensory`) entries are both listed — this is a Set-membership guardrail -// list (order doesn't matter, unlike the loaders' priority-ordered candidate lists), so a contributor PR touching -// EITHER the new canonical `.loopover.*` config file or a still-supported legacy `.gittensory.*` one gets the same -// hard-guardrail protection (#4773 — dual-read, additive; the legacy globs are never removed). +// This is a Set-membership guardrail list (order doesn't matter, unlike the loaders' priority-ordered candidate +// lists): a contributor PR touching the canonical `.loopover.*` config file gets hard-guardrail protection. export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [ ".loopover.yml", ".loopover.yaml", @@ -11,12 +9,6 @@ export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [ ".github/loopover.yml", ".github/loopover.yaml", ".github/loopover.json", - ".gittensory.yml", - ".gittensory.yaml", - ".gittensory.json", - ".github/gittensory.yml", - ".github/gittensory.yaml", - ".github/gittensory.json", "**/codecov.yml", "**/codecov.yaml", "**/.codecov.yml", diff --git a/src/selfhost/private-config.ts b/src/selfhost/private-config.ts index 4a2b5ee9f5..702ed6a669 100644 --- a/src/selfhost/private-config.ts +++ b/src/selfhost/private-config.ts @@ -1,8 +1,8 @@ // Container-private per-repo config (self-host). A self-host operator mounts a directory at // GITTENSORY_REPO_CONFIG_DIR and configures each repo's review policy there; the focus-manifest loader reads it -// INSTEAD of fetching the public `.gittensory.yml`/`.loopover.yml`, so policy (gate, autonomy, labels, model/effort) -// is configured PRIVATELY and never exposed to contributors who could read and game the public file. Node-only — it -// is registered into the Workers-safe loader via setLocalManifestReader at boot (server.ts), so this module's fs +// INSTEAD of fetching the public `.loopover.yml`, so policy (gate, autonomy, labels, model/effort) is configured +// PRIVATELY and never exposed to contributors who could read and game the public file. Node-only — it is +// registered into the Workers-safe loader via setLocalManifestReader at boot (server.ts), so this module's fs // import never reaches the Cloudflare bundle. // // Layout (CodeRabbit-style: per-repo override, layered over a global default, layered over a cross-repo shared @@ -13,29 +13,22 @@ // 4. `.loopover.yml` — GLOBAL default at the dir root, shared by every repo. // 5. `_shared/.loopover.yml` — SHARED BASE (#1959), the lowest-priority layer: one house policy // an operator running many repos writes once instead of copy-pasting into every repo's private config. -// `.yaml` / `.json` are accepted everywhere `.yml` is, and `.gittensory.*` (the pre-rebrand name) is accepted -// everywhere `.loopover.*` is (#4773 — dual-read, additive only, indefinitely: an existing self-hoster's committed -// `.gittensory.*` file keeps working with zero changes on their part). CONFIG_BASENAMES below lists every accepted -// basename with the new-brand `.loopover.*` entries FIRST and the legacy `.gittensory.*` entries LAST; `readFirstExisting` -// (and its `WithPath` sibling) return the first candidate that exists, in list order, so within any single location -// (a given folder, or the dir root) a `.loopover.*` file present there is read and an old `.gittensory.*` file -// sitting alongside it is ignored (not merged) — see CONFIG_BASENAMES for why new-brand-first is the right -// precedence. An operator who has done nothing keeps working unchanged: only `.gittensory.*` exists anywhere, so it -// is still the first (and only) hit at every location. With only ONE of {a per-repo candidate, the global default, -// the shared base} present, its raw text is returned unchanged — byte-identical to the original #1390 behavior -// (and to the pre-#1959 2-layer behavior when no shared base is mounted, the common case). With more than one -// present, they are DEEP-MERGED in ascending priority (shared base → global default → per-repo file): nested -// mappings (`gate`, `settings`, `review`, `features`, `contentLane`, and their own nested blocks) merge key by key, -// arrays replace wholesale (never concatenated), and an explicit YAML/JSON `null` at a key always overrides a -// lower layer's value there — which clears a setting wherever the manifest parser already treats an explicit null -// as "off"/"clear" (e.g. `settings.contributorOpenPrCap`, `settings.accountAgeThresholdDays`), and is otherwise -// equivalent to omitting the key. A key that is simply absent from a higher layer leaves the lower layer's value at -// that key untouched. If a layer fails to parse as a YAML/JSON mapping (or is oversized), it is dropped from the -// fold and the remaining, still-valid layers merge as if it were never mounted — a broken layer never discards a -// still-good sibling's policy, and never blocks a review. If NONE of the present layers parse, the highest-priority -// present layer's raw text is returned (matching the original single-candidate priority), so a doubly/triply broken -// set degrades exactly like a single malformed manifest always has. The slug is lowercased (GitHub repo full-names -// are case-insensitive; #1390 already lowercased). +// `.yaml` / `.json` are accepted everywhere `.yml` is (see CONFIG_BASENAMES). `readFirstExisting` (and its +// `WithPath` sibling) return the first candidate that exists, in list order. With only ONE of {a per-repo +// candidate, the global default, the shared base} present, its raw text is returned unchanged — byte-identical to +// the original #1390 behavior (and to the pre-#1959 2-layer behavior when no shared base is mounted, the common +// case). With more than one present, they are DEEP-MERGED in ascending priority (shared base → global default → +// per-repo file): nested mappings (`gate`, `settings`, `review`, `features`, `contentLane`, and their own nested +// blocks) merge key by key, arrays replace wholesale (never concatenated), and an explicit YAML/JSON `null` at a +// key always overrides a lower layer's value there — which clears a setting wherever the manifest parser already +// treats an explicit null as "off"/"clear" (e.g. `settings.contributorOpenPrCap`, `settings.accountAgeThresholdDays`), +// and is otherwise equivalent to omitting the key. A key that is simply absent from a higher layer leaves the +// lower layer's value at that key untouched. If a layer fails to parse as a YAML/JSON mapping (or is oversized), +// it is dropped from the fold and the remaining, still-valid layers merge as if it were never mounted — a broken +// layer never discards a still-good sibling's policy, and never blocks a review. If NONE of the present layers +// parse, the highest-priority present layer's raw text is returned (matching the original single-candidate +// priority), so a doubly/triply broken set degrades exactly like a single malformed manifest always has. The slug +// is lowercased (GitHub repo full-names are case-insensitive; #1390 already lowercased). // // The reserved shared-base folder `_shared` is never treated as a bare repo-name config folder for a real GitHub // repo named `_shared`; use the owner-qualified or flat owner__repo candidates for that repository instead. @@ -53,30 +46,14 @@ import type { } from "../signals/focus-manifest-loader"; /** The bare config filenames tried inside a per-repo folder and at the dir root (global default), in priority - * order. New-brand (`.loopover.*`) candidates are listed FIRST and legacy (`.gittensory.*`) candidates LAST — this - * IS the precedence rule (#4773): every helper below (`GLOBAL_CONFIG_CANDIDATES`, `SHARED_BASE_CONFIG_CANDIDATES`, - * `localConfigCandidates`) derives its search order from this array's order, and `readFirstExisting` / - * `readFirstExistingWithPath` return the first candidate that EXISTS, so at any single location (a folder, or the - * dir root) a `.loopover.*` file wins over a `.gittensory.*` file sitting right next to it — they are never both - * read and merged as if they were two layers. Rationale: creating a `.loopover.*` file at all is clear, deliberate, - * recent migration intent, so a pre-existing `.gittensory.*` file alongside it is far more likely to be a stale - * leftover than something the operator means to keep authoritative; this mirrors the existing - * `review/AGENTS.md`-before-`review/CLAUDE.md` precedent below (new name tried first, legacy as pure fallback), - * not a new rule invented for this change. An operator with ONLY the legacy file present is completely unaffected - * — dual-read is additive, the legacy name is never removed, and there is no expiry planned. */ -const CONFIG_BASENAMES = [ - ".loopover.yml", ".loopover.yaml", ".loopover.json", - ".gittensory.yml", ".gittensory.yaml", ".gittensory.json", // legacy — dual-read indefinitely (#4773) -] as const; + * order. Every helper below (`GLOBAL_CONFIG_CANDIDATES`, `SHARED_BASE_CONFIG_CANDIDATES`, `localConfigCandidates`) + * derives its search order from this array's order, and `readFirstExisting` / `readFirstExistingWithPath` return + * the first candidate that EXISTS. */ +const CONFIG_BASENAMES = [".loopover.yml", ".loopover.yaml", ".loopover.json"] as const; -/** The extensions accepted by CONFIG_BASENAMES (derived positionally — `base.slice(base.lastIndexOf("."))` — never - * by slicing off a hardcoded brand-name length, which would silently corrupt this for any basename whose brand - * prefix isn't exactly 11 characters like `.gittensory` happens to be), deduped in first-seen order: `.yml`, - * `.yaml`, `.json`. Used only to build the flat `{owner}__{repo}.` candidate below (#1390 back-compat), which - * is brand-agnostic — it was never spelled with a brand name in it (`owner__repo.yml`, not - * `owner__repo.gittensory.yml`), so there is nothing to dual-read there beyond the plain extension. Mapping - * CONFIG_BASENAMES directly (one flat candidate per brand basename) would instead repeat the same 3 flat paths - * once per brand — redundant, not "more compatible". */ +/** The extensions accepted by CONFIG_BASENAMES, in first-seen order: `.yml`, `.yaml`, `.json`. Used only to build + * the flat `{owner}__{repo}.` candidate below (#1390 back-compat), which carries no brand name in it + * (`owner__repo.yml`, not `owner__repo.loopover.yml`). */ const CONFIG_EXTENSIONS: string[] = [...new Set(CONFIG_BASENAMES.map((base) => base.slice(base.lastIndexOf("."))))]; const GITHUB_OWNER_SEGMENT = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/; const GITHUB_REPO_SEGMENT = /^[a-z0-9._-]+$/; @@ -85,25 +62,21 @@ function isSafeRepoSegment(segment: string): boolean { return segment !== "." && segment !== ".." && GITHUB_REPO_SEGMENT.test(segment); } -/** Global-default candidates (relative to GITTENSORY_REPO_CONFIG_DIR): the dir-root `.loopover.{yml,yaml,json}` / - * `.gittensory.{yml,yaml,json}` (new-brand first, see CONFIG_BASENAMES for the precedence rule, #4773), +/** Global-default candidates (relative to GITTENSORY_REPO_CONFIG_DIR): the dir-root `.loopover.{yml,yaml,json}`, * deep-merged under any per-repo file (or applied alone, when a repo has no per-repo file of its own). */ export const GLOBAL_CONFIG_CANDIDATES: string[] = [...CONFIG_BASENAMES]; -/** Shared-base candidates (#1959, relative to GITTENSORY_REPO_CONFIG_DIR): `_shared/.loopover.{yml,yaml,json}` / - * `_shared/.gittensory.{yml,yaml,json}` (new-brand first, #4773), sibling to the per-repo folders inside the SAME - * container-private directory — no new env var. This is the lowest-priority layer: a cross-repo "house policy" an - * operator running many repos writes once, deep-merged UNDER both the global default and any per-repo file (or - * applied alone, when neither of those exists). */ +/** Shared-base candidates (#1959, relative to GITTENSORY_REPO_CONFIG_DIR): `_shared/.loopover.{yml,yaml,json}`, + * sibling to the per-repo folders inside the SAME container-private directory — no new env var. This is the + * lowest-priority layer: a cross-repo "house policy" an operator running many repos writes once, deep-merged + * UNDER both the global default and any per-repo file (or applied alone, when neither of those exists). */ export const SHARED_BASE_CONFIG_CANDIDATES: string[] = CONFIG_BASENAMES.map((base) => join("_shared", base)); const SHARED_BASE_CONFIG_CANDIDATE_SET = new Set(SHARED_BASE_CONFIG_CANDIDATES); /** Per-repo private-config candidate paths (relative to GITTENSORY_REPO_CONFIG_DIR), in priority order: * owner-qualified folder → bare repo-name folder → flat `owner__repo` file (the #1390 back-compat form). The slug * is the lowercased GitHub `owner__repo` (double underscore because `/` is not filename-safe); the bare folder is - * the lowercased repo name. An invalid repo full name (no single interior slash) yields no candidates. The two - * folder forms carry the brand name (new-brand `.loopover.*` before legacy `.gittensory.*`, per CONFIG_BASENAMES, - * #4773); the flat form doesn't (see CONFIG_EXTENSIONS) and is unaffected by the rebrand. */ + * the lowercased repo name. An invalid repo full name (no single interior slash) yields no candidates. */ export function localConfigCandidates(repoFullName: string): string[] { const slash = repoFullName.indexOf("/"); if (slash <= 0 || slash === repoFullName.length - 1 || slash !== repoFullName.lastIndexOf("/")) return []; @@ -112,9 +85,9 @@ export function localConfigCandidates(repoFullName: string): string[] { if (!GITHUB_OWNER_SEGMENT.test(owner) || !isSafeRepoSegment(repo)) return []; const slug = `${owner}__${repo}`; return [ - // 1. owner-qualified folder — `{owner}__{repo}/.loopover.{yml,yaml,json}` then `/.gittensory.{yml,yaml,json}` + // 1. owner-qualified folder — `{owner}__{repo}/.loopover.{yml,yaml,json}` ...CONFIG_BASENAMES.map((base) => join(slug, base)), - // 2. bare repo-name folder — `{repo}/.loopover.{yml,yaml,json}` then `/.gittensory.{yml,yaml,json}` + // 2. bare repo-name folder — `{repo}/.loopover.{yml,yaml,json}` ...CONFIG_BASENAMES.map((base) => join(repo, base)).filter( (candidate) => !SHARED_BASE_CONFIG_CANDIDATE_SET.has(candidate), ), @@ -196,7 +169,7 @@ function parseConfigMapping(text: string): Record | null { * non-mapping override value (including an explicit `null`) REPLACES the base value at that key wholesale — never * concatenated or blended. A key `override` never mentions leaves `base`'s value at that key completely untouched. * Exported for direct unit testing; deliberately ignorant of any manifest field name (`gate`, `settings`, - * `wantedPaths`, etc.) so it composes correctly with the whole `.gittensory.yml` schema, present and future, with + * `wantedPaths`, etc.) so it composes correctly with the whole `.loopover.yml` schema, present and future, with * zero repo- or field-specific code. */ export function mergeConfigOverlay(base: unknown, override: unknown): unknown { if (override === null) return null; @@ -258,7 +231,7 @@ function combineConfigLayersWithMeta( } /** Build the container-local manifest reader over GITTENSORY_REPO_CONFIG_DIR, or null when the dir is unset/blank - * (⇒ the loader keeps fetching the public `.gittensory.yml`). Looks up the first existing per-repo candidate, the + * (⇒ the loader keeps fetching the public `.loopover.yml`). Looks up the first existing per-repo candidate, the * global-default candidate, and the shared-base candidate (#1959) independently and folds whichever are present * in ascending priority (shared → global → per-repo) via {@link combineConfigLayers}: with only one present, its * raw text is returned unchanged; with two or more, they are deep-merged (see the module header) and returned as diff --git a/src/signals/focus-manifest-loader.ts b/src/signals/focus-manifest-loader.ts index 21a149c3b4..72fb814a9c 100644 --- a/src/signals/focus-manifest-loader.ts +++ b/src/signals/focus-manifest-loader.ts @@ -11,26 +11,16 @@ export const REPO_FOCUS_MANIFEST_MAX_AGE_MS = 6 * 60 * 60 * 1000; export const REPO_FOCUS_MANIFEST_MAX_CONCURRENT_LOADS = 4; /** - * Public-repo manifest candidate paths, tried in order via {@link fetchRepoFocusManifestFile}. New-brand - * (`loopover`) candidates are listed FIRST and legacy (`gittensory`) candidates LAST — this is the dual-read - * precedence rule (#4773): the loader returns the first candidate that responds `200 OK`, so a repo that publishes - * BOTH `.loopover.yml` and `.gittensory.yml` at its root gets the new-brand file, and a repo with only the legacy - * file (the common case today, and forever if an operator never migrates) is completely unaffected — it is still - * the first (and only) 200. Mirrors the existing `.gittensory.yml`-before-`.github/gittensory.yml`-before-`.json` - * ordering used within each brand: `.yml` anywhere beats `.json` anywhere, and root beats `.github/` within the - * same extension. Deliberately does NOT add a `.loopover.yaml` variant here — the legacy brand never had a public - * `.yaml` candidate either (only the self-host loader in `../selfhost/private-config.ts` accepts `.yaml`), so this - * stays a like-for-like widening rather than a scope increase. + * Public-repo manifest candidate paths, tried in order via {@link fetchRepoFocusManifestFile}. The loader returns + * the first candidate that responds `200 OK`: root beats `.github/` within the same extension, and `.yml` beats + * `.json`. Deliberately does NOT add a `.loopover.yaml` variant here — only the self-host loader in + * `../selfhost/private-config.ts` accepts `.yaml`. */ export const MANIFEST_FILE_CANDIDATES = [ ".loopover.yml", ".github/loopover.yml", ".loopover.json", ".github/loopover.json", - ".gittensory.yml", // legacy — dual-read indefinitely (#4773) - ".github/gittensory.yml", - ".gittensory.json", - ".github/gittensory.json", ] as const; /** @@ -42,7 +32,7 @@ export type RepoFocusManifestFetcher = (repoFullName: string) => Promise { - // Container-private per-repo config (self-host) takes priority over the public `.loopover.yml` / `.gittensory.yml`: + // Container-private per-repo config (self-host) takes priority over the public `.loopover.yml`: // read fresh from local fs each call (cheap, no network) so operator edits apply immediately. NEVER consulted on the publicOnly // (contributor-preview) path, and never persisted — so private policy can't leak into previews or the cache. if (!cachePolicy.publicOnly && localManifestReader) { @@ -180,7 +169,7 @@ async function loadRepoFocusManifestWithCachePolicy( await persistRepoFocusManifest(env, repoFullName, manifest, REPO_PUBLIC_FOCUS_MANIFEST_SIGNAL); } else { // Persist even an ABSENT manifest (negative cache): effective settings are resolved from - // `.loopover.yml` / `.gittensory.yml` on every webhook, so a repo without one must not re-fetch the raw file each time. + // `.loopover.yml` on every webhook, so a repo without one must not re-fetch the raw file each time. // The TTL still refreshes it, so a newly-added manifest is picked up on the next window. await persistRepoFocusManifest(env, repoFullName, manifest); } diff --git a/test/fixtures/engine-parity/predicted-gate/clean-pass-gittensor.ts b/test/fixtures/engine-parity/predicted-gate/clean-pass-gittensor.ts index 3c78610335..1971561f65 100644 --- a/test/fixtures/engine-parity/predicted-gate/clean-pass-gittensor.ts +++ b/test/fixtures/engine-parity/predicted-gate/clean-pass-gittensor.ts @@ -16,6 +16,6 @@ export default definePredictedGateFixture({ blockerCodes: [], warningCodes: [], funnelPresent: false, - noteIncludes: ["public .gittensory.yml", "slop score is NOT evaluated pre-submission"], + noteIncludes: ["public .loopover.yml", "slop score is NOT evaluated pre-submission"], }, }); diff --git a/test/fixtures/engine-parity/predicted-gate/clean-pass-oss-anti-slop.ts b/test/fixtures/engine-parity/predicted-gate/clean-pass-oss-anti-slop.ts index 847eb733e7..6374944fd7 100644 --- a/test/fixtures/engine-parity/predicted-gate/clean-pass-oss-anti-slop.ts +++ b/test/fixtures/engine-parity/predicted-gate/clean-pass-oss-anti-slop.ts @@ -16,6 +16,6 @@ export default definePredictedGateFixture({ blockerCodes: [], warningCodes: [], funnelPresent: true, - noteIncludes: ["public .gittensory.yml", "slop score is NOT evaluated pre-submission"], + noteIncludes: ["public .loopover.yml", "slop score is NOT evaluated pre-submission"], }, }); diff --git a/test/fixtures/engine-parity/predicted-gate/golden/ai-review-gate-mode-inert.json b/test/fixtures/engine-parity/predicted-gate/golden/ai-review-gate-mode-inert.json index cd2012d3ab..479a17cd0a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/ai-review-gate-mode-inert.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/ai-review-gate-mode-inert.json @@ -9,5 +9,5 @@ "blockers": [], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/cla-gate-mode-inert.json b/test/fixtures/engine-parity/predicted-gate/golden/cla-gate-mode-inert.json index cd2012d3ab..479a17cd0a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/cla-gate-mode-inert.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/cla-gate-mode-inert.json @@ -9,5 +9,5 @@ "blockers": [], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-gittensor.json b/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-gittensor.json index cd2012d3ab..479a17cd0a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-gittensor.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-gittensor.json @@ -9,5 +9,5 @@ "blockers": [], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-oss-anti-slop.json b/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-oss-anti-slop.json index 0320dbc466..87ad242aec 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-oss-anti-slop.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/clean-pass-oss-anti-slop.json @@ -12,5 +12,5 @@ "message": "This repo runs the Gittensor anti-slop gate. Gittensor lets GitHub contributors earn for open-source work like this — register to start earning.", "registerUrl": "https://gittensor.io" }, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/duplicate-pr-block.json b/test/fixtures/engine-parity/predicted-gate/golden/duplicate-pr-block.json index 892de7a8eb..31192aeffc 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/duplicate-pr-block.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/duplicate-pr-block.json @@ -16,5 +16,5 @@ ], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/first-time-grace-inert.json b/test/fixtures/engine-parity/predicted-gate/golden/first-time-grace-inert.json index cd2012d3ab..479a17cd0a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/first-time-grace-inert.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/first-time-grace-inert.json @@ -9,5 +9,5 @@ "blockers": [], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/guardrail-hold.json b/test/fixtures/engine-parity/predicted-gate/golden/guardrail-hold.json index 792e357b29..2f0b7b7d99 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/guardrail-hold.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/guardrail-hold.json @@ -16,5 +16,5 @@ } ], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/manifest-blocked-path.json b/test/fixtures/engine-parity/predicted-gate/golden/manifest-blocked-path.json index a942a87060..b7ee56203a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/manifest-blocked-path.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/manifest-blocked-path.json @@ -9,5 +9,5 @@ "blockers": [], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/merge-readiness-composite-block.json b/test/fixtures/engine-parity/predicted-gate/golden/merge-readiness-composite-block.json index 6408c08684..e0fee87b09 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/merge-readiness-composite-block.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/merge-readiness-composite-block.json @@ -16,5 +16,5 @@ ], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/missing-linked-issue-block.json b/test/fixtures/engine-parity/predicted-gate/golden/missing-linked-issue-block.json index 6408c08684..e0fee87b09 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/missing-linked-issue-block.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/missing-linked-issue-block.json @@ -16,5 +16,5 @@ ], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-with-paths.json b/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-with-paths.json index 08bd5fda32..dde33a95c1 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-with-paths.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-with-paths.json @@ -16,5 +16,5 @@ ], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. The size-hold prediction uses changed FILE count only, not changed LINE count (line-diff stats are not available pre-submission), so it may under-predict a hold for a PR with many changed lines across few files. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-without-paths.json b/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-without-paths.json index cd2012d3ab..479a17cd0a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-without-paths.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/path-gated-check-without-paths.json @@ -9,5 +9,5 @@ "blockers": [], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/readiness-warning.json b/test/fixtures/engine-parity/predicted-gate/golden/readiness-warning.json index a2f2617459..80ff331d1a 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/readiness-warning.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/readiness-warning.json @@ -16,5 +16,5 @@ } ], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/fixtures/engine-parity/predicted-gate/golden/self-authored-linked-issue-block.json b/test/fixtures/engine-parity/predicted-gate/golden/self-authored-linked-issue-block.json index e13c0ee9da..d812b5cfad 100644 --- a/test/fixtures/engine-parity/predicted-gate/golden/self-authored-linked-issue-block.json +++ b/test/fixtures/engine-parity/predicted-gate/golden/self-authored-linked-issue-block.json @@ -16,5 +16,5 @@ ], "warnings": [], "funnel": null, - "note": "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." + "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)." } diff --git a/test/unit/backfill.test.ts b/test/unit/backfill.test.ts index b819c52a34..cd7086a4f9 100644 --- a/test/unit/backfill.test.ts +++ b/test/unit/backfill.test.ts @@ -1193,7 +1193,7 @@ describe("GitHub backfill", () => { ); }); - it("REGRESSION (#2912): repair diagnostics honor a .gittensory.yml-only checkRunMode: enabled override (DB row left at off)", async () => { + it("REGRESSION (#2912): repair diagnostics honor a .loopover.yml-only checkRunMode: enabled override (DB row left at off)", async () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, 123); // DB row explicitly says checkRunMode: off; only the yml manifest turns it on, so this only passes if the @@ -1201,7 +1201,7 @@ describe("GitHub backfill", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", checkRunMode: "off" }); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString(); - if (url.endsWith("/.gittensory.yml")) return new Response("settings:\n checkRunMode: enabled\n", { status: 200 }); + if (url.endsWith("/.loopover.yml")) return new Response("settings:\n checkRunMode: enabled\n", { status: 200 }); return new Response("Not Found", { status: 404 }); }); @@ -1408,14 +1408,14 @@ describe("GitHub backfill", () => { expect(result.repos[0]).toMatchObject({ status: "skipped", warnings: ["Backfill is disabled for this repository."] }); }); - it("REGRESSION (#2912): honors a .gittensory.yml-only backfillEnabled: false override (DB row left at its true default)", async () => { + it("REGRESSION (#2912): honors a .loopover.yml-only backfillEnabled: false override (DB row left at its true default)", async () => { const env = createTestEnv(); await seedRegisteredRepo(env); // No upsertRepositorySettings call: the DB row stays at its default (backfillEnabled: true). Only the // yml manifest disables it, so this only passes if the resolver (not the raw DB accessor) is consulted. vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString(); - if (url.endsWith("/.gittensory.yml")) return new Response("settings:\n backfillEnabled: false\n", { status: 200 }); + if (url.endsWith("/.loopover.yml")) return new Response("settings:\n backfillEnabled: false\n", { status: 200 }); return new Response("Not Found", { status: 404 }); }); diff --git a/test/unit/change-guardrail.test.ts b/test/unit/change-guardrail.test.ts index 48158f2e76..81abbbf497 100644 --- a/test/unit/change-guardrail.test.ts +++ b/test/unit/change-guardrail.test.ts @@ -131,9 +131,9 @@ describe("change-guardrail glob matching", () => { expect(matchesAny("src/a/f.js", [atCap])).toBe(false); // wrong extension — genuinely doesn't match }); - it("a wildcard-free literal glob (e.g. an exact guarded file like '.gittensory.yml') is never treated as unsafe", () => { - expect(matchesAny(".gittensory.yml", [".gittensory.yml"])).toBe(true); - expect(matchesAny("other-file.yml", [".gittensory.yml"])).toBe(false); + it("a wildcard-free literal glob (e.g. an exact guarded file like '.loopover.yml') is never treated as unsafe", () => { + expect(matchesAny(".loopover.yml", [".loopover.yml"])).toBe(true); + expect(matchesAny("other-file.yml", [".loopover.yml"])).toBe(false); }); it("a mix of one pathological glob among otherwise-fine globs still forces a hold for ANY path (fail-safe dominates)", () => { diff --git a/test/unit/check-docs-drift-script.test.ts b/test/unit/check-docs-drift-script.test.ts index 0175458c4b..4507a5ead0 100644 --- a/test/unit/check-docs-drift-script.test.ts +++ b/test/unit/check-docs-drift-script.test.ts @@ -301,7 +301,7 @@ describe("check-docs-drift script", () => { "src/github/commands.ts": baseCommandsSource, "src/types.ts": buildRepositorySettingsSource(baseSettingsExtraFields), "packages/gittensory-engine/src/focus-manifest.ts": buildFocusManifestSource(baseFocusManifestReviewFields), - ".gittensory.yml.example": buildYmlExampleText(baseSettingsExtraFields, baseFocusManifestReviewFields), + ".loopover.yml.example": buildYmlExampleText(baseSettingsExtraFields, baseFocusManifestReviewFields), "apps/gittensory-ui/src/routes/docs.tuning.tsx": [buildFlagsPageText(baseFlagNames), buildGateModePageText()].join("\n"), "apps/gittensory-ui/src/routes/docs.privacy-security.tsx": buildFlagsPageText(baseFlagNames), "apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx": buildDocsPageText(allBaseCommandIds), @@ -457,13 +457,13 @@ describe("check-docs-drift script", () => { expect(hit).toBeDefined(); }); - it("catches a RepositorySettings field with zero .gittensory.yml.example mention and no alias/exclude entry (#4617)", () => { + it("catches a RepositorySettings field with zero .loopover.yml.example mention and no alias/exclude entry (#4617)", () => { const files = baseFixtures(); files["src/types.ts"] = files["src/types.ts"]!.replace("};", " totallyUndocumentedField: boolean;\n};"); const result = checkDocsDrift({ root: "/fake", readFile: makeReadFile(files) }); const hit = result.failures.find( - (failure) => failure.includes(".gittensory.yml.example") && failure.includes("totallyUndocumentedField"), + (failure) => failure.includes(".loopover.yml.example") && failure.includes("totallyUndocumentedField"), ); expect(hit).toBeDefined(); // This exact shape -- a plain, non-*GateMode-named field -- is invisible to the OLD narrow check: it @@ -478,7 +478,7 @@ describe("check-docs-drift script", () => { for (const row of SETTINGS_ALIAS_MANIFEST) { const files = baseFixtures(); files["src/types.ts"] = files["src/types.ts"]!.replace("};", ` ${row.field}: string;\n};`); - files[".gittensory.yml.example"] += `\n${row.aliases[0]}`; + files[".loopover.yml.example"] += `\n${row.aliases[0]}`; const result = checkDocsDrift({ root: "/fake", readFile: makeReadFile(files) }); expect(result.failures, `${row.field} should pass via alias ${row.aliases[0]}`).toEqual([]); @@ -500,7 +500,7 @@ describe("check-docs-drift script", () => { it("catches a FocusManifest field nested inside another config type with zero yml mention -- the exact review.visual.production_url shape (#4617)", () => { const files = baseFixtures(); - // A SECOND VisualConfig-shaped leaf that the synthetic .gittensory.yml.example never mentions. + // A SECOND VisualConfig-shaped leaf that the synthetic .loopover.yml.example never mentions. files["packages/gittensory-engine/src/focus-manifest.ts"] = files["packages/gittensory-engine/src/focus-manifest.ts"]!.replace( "productionUrl: string | null;", "productionUrl: string | null;\n totallyUndocumentedNestedField: string | null;", @@ -508,7 +508,7 @@ describe("check-docs-drift script", () => { const result = checkDocsDrift({ root: "/fake", readFile: makeReadFile(files) }); const hit = result.failures.find( - (failure) => failure.includes(".gittensory.yml.example") && failure.includes("review.visual.totally_undocumented_nested_field"), + (failure) => failure.includes(".loopover.yml.example") && failure.includes("review.visual.totally_undocumented_nested_field"), ); expect(hit).toBeDefined(); // A check that only enumerated FocusManifest's own TOP-LEVEL fields (never recursing into `review`, let @@ -529,7 +529,7 @@ describe("check-docs-drift script", () => { "visual: VisualConfig;", `${leafName}: string | null;\n visual: VisualConfig;`, ); - files[".gittensory.yml.example"] += `\n${row.aliases[0]}`; + files[".loopover.yml.example"] += `\n${row.aliases[0]}`; const result = checkDocsDrift({ root: "/fake", readFile: makeReadFile(files) }); expect(result.failures, `${row.field} should pass via alias ${row.aliases[0]}`).toEqual([]); diff --git a/test/unit/check-manifest-drift-script.test.ts b/test/unit/check-manifest-drift-script.test.ts index ac6e3ed5cb..5727da8d3c 100644 --- a/test/unit/check-manifest-drift-script.test.ts +++ b/test/unit/check-manifest-drift-script.test.ts @@ -12,7 +12,7 @@ const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); describe("check-manifest-drift script", () => { function makeReadFile(rootManifestYaml: string) { return (_root: string, relativePath: string): string => { - if (relativePath !== ".gittensory.yml") throw new Error(`unexpected read: ${relativePath}`); + if (relativePath !== ".loopover.yml") throw new Error(`unexpected read: ${relativePath}`); return rootManifestYaml; }; } @@ -50,7 +50,7 @@ describe("check-manifest-drift script", () => { }); expect(result.failures).toHaveLength(1); - expect(result.failures[0]).toContain(".gittensory.yml"); + expect(result.failures[0]).toContain(".loopover.yml"); expect(result.failures[0]).toContain("GITTENSORY_REPO_FOCUS_MANIFEST_YAML"); expect(result.failures[0]).toContain("linkedIssuePolicy"); }); @@ -69,11 +69,11 @@ describe("check-manifest-drift script", () => { expect(result.failures[0]).toContain("advisory"); }); - // Most important regression test in this file: proves the REAL current repo state (root .gittensory.yml + // Most important regression test in this file: proves the REAL current repo state (root .loopover.yml // vs. the real bundled GITTENSORY_REPO_FOCUS_MANIFEST_YAML constant) agrees, using the real filesystem // reader against the real repo root. If this fails, the two have genuinely drifted apart -- either way, // the check must not be weakened to make this test pass. - it("the real repo's root .gittensory.yml and the bundled fallback agree (regression guard)", () => { + it("the real repo's root .loopover.yml and the bundled fallback agree (regression guard)", () => { const result = checkManifestDrift({ root: process.cwd() }); expect(result.failures).toEqual([]); @@ -90,6 +90,6 @@ describe("check-manifest-drift script", () => { it("prints a clean summary and exits 0 for the real repo state when run as a subprocess", () => { const output = execFileSync(TSX_BIN, ["scripts/check-manifest-drift.mjs"], { encoding: "utf8" }); - expect(output).toMatch(/Manifest-drift check ok: \.gittensory\.yml and GITTENSORY_REPO_FOCUS_MANIFEST_YAML agree\./); + expect(output).toMatch(/Manifest-drift check ok: \.loopover\.yml and GITTENSORY_REPO_FOCUS_MANIFEST_YAML agree\./); }); }); diff --git a/test/unit/config-templates.test.ts b/test/unit/config-templates.test.ts index 35fc0b6304..b751a82805 100644 --- a/test/unit/config-templates.test.ts +++ b/test/unit/config-templates.test.ts @@ -32,9 +32,9 @@ function bodyFromMarker(content: string, marker: string): string { } describe("config/examples review templates (#1682)", () => { - it("gittensory.full.yml body matches .gittensory.yml.example from WHERE IT LIVES onward", () => { + it("gittensory.full.yml body matches .loopover.yml.example from WHERE IT LIVES onward", () => { const full = readConfigExample("gittensory.full.yml"); - const example = readRoot(".gittensory.yml.example"); + const example = readRoot(".loopover.yml.example"); expect(bodyFromMarker(full, CANONICAL_BODY_MARKER)).toBe(bodyFromMarker(example, CANONICAL_BODY_MARKER)); }); @@ -250,8 +250,8 @@ describe("config/examples review templates (#1682)", () => { expect(resolveAutonomy(on.settings.autonomy, "merge")).toBe("auto"); expect(resolveAutonomy(on.settings.autonomy, "close")).toBe("observe"); // unset action ⇒ default }); - it("imports an existing .gittensory.yml's field values across sections into the parsed manifest (#2212)", () => { - // The config-generator's "import existing .gittensory.yml into the form" reads an operator's file back + it("imports an existing .loopover.yml's field values across sections into the parsed manifest (#2212)", () => { + // The config-generator's "import existing .loopover.yml into the form" reads an operator's file back // through the same parser; lock in that a multi-section config's values round-trip faithfully (not just // that it parses without warnings). const yml = [ diff --git a/test/unit/docs-owner-checklist.test.ts b/test/unit/docs-owner-checklist.test.ts index 298ad35166..f7a2f4da88 100644 --- a/test/unit/docs-owner-checklist.test.ts +++ b/test/unit/docs-owner-checklist.test.ts @@ -31,7 +31,7 @@ describe("docs owner onboarding checklist page", () => { expect(source).toMatch(/\/v1\/repos\/:owner\/:repo\/gittensor-config-recommendation/); expect(source).toMatch(/\/v1\/repos\/:owner\/:repo\/settings-preview/); expect(source).toMatch(/repo-owner-intake/); - expect(source).toMatch(/\.gittensory\.yml/); + expect(source).toMatch(/\.loopover\.yml/); }); it("uses the real contribution-lane taxonomy from the engine", () => { diff --git a/test/unit/docs-selfhost-activation-paths.test.ts b/test/unit/docs-selfhost-activation-paths.test.ts index 4c1bbf22f2..ffb4f9cac0 100644 --- a/test/unit/docs-selfhost-activation-paths.test.ts +++ b/test/unit/docs-selfhost-activation-paths.test.ts @@ -21,7 +21,7 @@ describe("self-host activation + onboarding docs (#1574)", () => { it("quickstart documents allowlist, private config seed, activation POST, and Checks: write", () => { expect(quickstart).toContain("GITTENSORY_REVIEW_REPOS"); expect(quickstart).toContain("config/examples/global.gittensory.yml"); - expect(quickstart).toContain("gittensory-config/.gittensory.yml"); + expect(quickstart).toContain("gittensory-config/.loopover.yml"); expect(quickstart).toContain("/v1/repos/owner/my-repo/activation"); expect(quickstart).toContain("Checks: write"); expect(quickstart).toContain("INSTALL_AI_CLIS"); diff --git a/test/unit/engine-objective-anchor-config-classification.test.ts b/test/unit/engine-objective-anchor-config-classification.test.ts index 21d8156dcd..6b2d68c766 100644 --- a/test/unit/engine-objective-anchor-config-classification.test.ts +++ b/test/unit/engine-objective-anchor-config-classification.test.ts @@ -2,19 +2,18 @@ import { describe, expect, it } from "vitest"; import { extractObjectiveAnchorFeatures } from "../../packages/gittensory-engine/src/objective-anchor"; // packages/gittensory-engine/src/objective-anchor.ts's CONFIG_FILENAMES set is exercised almost -// exclusively by its own node:test suite (invisible to Codecov's vitest-based coverage), so the -// #4773 dual-read addition of ".loopover.yml" alongside ".gittensory.yml" needs a real vitest-side -// assertion, not just a top-level module-load hit. -describe("gittensory-engine objective-anchor config-filename classification (#4773)", () => { - it("classifies both .loopover.yml and .gittensory.yml as a 'config' change kind", () => { +// exclusively by its own node:test suite (invisible to Codecov's vitest-based coverage), so +// classifying ".loopover.yml" needs a real vitest-side assertion, not just a top-level module-load hit. +describe("gittensory-engine objective-anchor config-filename classification", () => { + it("classifies .loopover.yml as a 'config' change kind", () => { const features = extractObjectiveAnchorFeatures({ - paths: [".loopover.yml", ".gittensory.yml"], + paths: [".loopover.yml"], labels: [], titles: [], notes: [], }); expect(features.changeKinds).toContain("config"); - expect(features.paths).toEqual([".gittensory.yml", ".loopover.yml"]); + expect(features.paths).toEqual([".loopover.yml"]); }); }); diff --git a/test/unit/focus-manifest-loader.test.ts b/test/unit/focus-manifest-loader.test.ts index d7c0aeb00d..16d96eb36c 100644 --- a/test/unit/focus-manifest-loader.test.ts +++ b/test/unit/focus-manifest-loader.test.ts @@ -290,7 +290,7 @@ describe("focus-manifest loader", () => { expect(await fetchRepoFocusManifestFile("trailing/")).toBeNull(); }); - it("returns raw text from the first 200 OK candidate path (new-brand .loopover.yml, tried before legacy)", async () => { + it("returns raw text from the first 200 OK candidate path", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { const stringUrl = String(url); if (stringUrl.endsWith("/.loopover.yml")) return new Response("wantedPaths:\n - src/\n", { status: 200 }); @@ -301,18 +301,6 @@ describe("focus-manifest loader", () => { expect(fetchSpy).toHaveBeenCalledTimes(1); // first candidate in MANIFEST_FILE_CANDIDATES is a 200, no fallback needed }); - it("falls back to the legacy .gittensory.yml when none of the new-brand candidates respond (#4773 — dual-read)", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { - const stringUrl = String(url); - if (stringUrl.endsWith("/.gittensory.yml")) return new Response("wantedPaths:\n - src/\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - const text = await fetchRepoFocusManifestFile("owner/repo"); - expect(text).toBe("wantedPaths:\n - src/\n"); - // all 4 new-brand candidates 404 first, then legacy `.gittensory.yml` (5th in MANIFEST_FILE_CANDIDATES) hits. - expect(fetchSpy).toHaveBeenCalledTimes(MANIFEST_FILE_CANDIDATES.indexOf(".gittensory.yml") + 1); - }); - it("does not read public manifest responses when Content-Length is too large", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { const stringUrl = String(url); @@ -325,21 +313,11 @@ describe("focus-manifest loader", () => { headers: { "content-length": String(MAX_FOCUS_MANIFEST_BYTES + 1) }, }); } - if (stringUrl.endsWith("/.github/loopover.json")) return new Response("not found", { status: 404 }); - if (stringUrl.endsWith("/.gittensory.yml") || stringUrl.endsWith("/.github/gittensory.yml")) { - return new Response("not found", { status: 404 }); - } - if (stringUrl.endsWith("/.gittensory.json")) { - return new Response('{"wantedPaths":["too-large/"]}', { - status: 200, - headers: { "content-length": String(MAX_FOCUS_MANIFEST_BYTES + 1) }, - }); - } return new Response('{"wantedPaths":["src/"]}', { status: 200 }); }); const text = await fetchRepoFocusManifestFile("owner/repo"); expect(text).toBe('{"wantedPaths":["src/"]}'); - expect(fetchSpy).toHaveBeenCalledTimes(MANIFEST_FILE_CANDIDATES.length); // every candidate (both brands) tried; last one wins + expect(fetchSpy).toHaveBeenCalledTimes(MANIFEST_FILE_CANDIDATES.length); // every candidate tried; last one wins }); it("aborts public manifest streams that grow beyond the byte cap", async () => { @@ -380,61 +358,6 @@ describe("focus-manifest loader", () => { expect(text).toBe('{"wantedPaths":["src/"]}'); }); - describe("public manifest dual-brand filename support (#4773)", () => { - it("(a) keeps working with ONLY the legacy .gittensory.yml candidates present — zero changes required", async () => { - vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { - const stringUrl = String(url); - if (stringUrl.endsWith("/.gittensory.yml")) return new Response("wantedPaths:\n - src/\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - expect(await fetchRepoFocusManifestFile("owner/repo")).toBe("wantedPaths:\n - src/\n"); - }); - - it("(b) reads the new-brand .loopover.yml when no legacy candidate is published", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { - const stringUrl = String(url); - if (stringUrl.endsWith("/.loopover.yml")) return new Response("wantedPaths:\n - src/\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - expect(await fetchRepoFocusManifestFile("owner/repo")).toBe("wantedPaths:\n - src/\n"); - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - - it("(b) reads the new-brand .loopover.json (root) and .github/loopover.yml the same as their legacy equivalents", async () => { - vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { - const stringUrl = String(url); - if (stringUrl.endsWith("/.loopover.json")) return new Response('{"wantedPaths":["src/"]}', { status: 200 }); - return new Response("not found", { status: 404 }); - }); - expect(await fetchRepoFocusManifestFile("owner/repo")).toBe('{"wantedPaths":["src/"]}'); - }); - - it("(c) prefers the new-brand .loopover.yml over a legacy .gittensory.yml published at the SAME repo", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { - const stringUrl = String(url); - if (stringUrl.endsWith("/.loopover.yml")) return new Response("wantedPaths:\n - new-brand/\n", { status: 200 }); - if (stringUrl.endsWith("/.gittensory.yml")) return new Response("wantedPaths:\n - legacy/\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - expect(await fetchRepoFocusManifestFile("owner/repo")).toBe("wantedPaths:\n - new-brand/\n"); - expect(fetchSpy).toHaveBeenCalledTimes(1); // the new-brand root candidate is tried (and hits) first - }); - - it("(c) prefers a new-brand .github/loopover.json over a legacy root .gittensory.yml — brand is the outer sort key", async () => { - // MANIFEST_FILE_CANDIDATES orders new-brand root .yml/.github .yml/root .json/.github .json before ANY - // legacy candidate, so a legacy root .yml never even gets a chance once any new-brand candidate 200s. - vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { - const stringUrl = String(url); - if (stringUrl.endsWith("/.github/loopover.json")) { - return new Response('{"wantedPaths":["new-brand/"]}', { status: 200 }); - } - if (stringUrl.endsWith("/.gittensory.yml")) return new Response("wantedPaths:\n - legacy/\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - expect(await fetchRepoFocusManifestFile("owner/repo")).toBe('{"wantedPaths":["new-brand/"]}'); - }); - }); - it("exposes a reasonable default max-age", () => { expect(REPO_FOCUS_MANIFEST_MAX_AGE_MS).toBeGreaterThan(60 * 1000); }); @@ -547,7 +470,7 @@ describe("focus-manifest loader — container-private config (self-host)", () => }); expect(manifest.source).toBe("api_record"); expect(manifest.wantedPaths).toEqual(["private/"]); - expect(fetched).toBe(0); // the public `.gittensory.yml` was never fetched + expect(fetched).toBe(0); // the public `.loopover.yml` was never fetched }); it("falls through to the public fetcher when the local reader has no file for the repo", async () => { @@ -584,12 +507,12 @@ describe("focus-manifest loader — container-private config (self-host)", () => const env = createTestEnv(); setLocalManifestReader(async () => ({ content: JSON.stringify({ review: { profile: "assertive" } }), - sharedConfigSource: "_shared/.gittensory.yml", + sharedConfigSource: "_shared/.loopover.yml", warnings: [], })); const manifest = await loadRepoFocusManifest(env, "owner/private"); expect(manifest.review.profile).toBe("assertive"); - expect(manifest.review.sharedConfigSource).toBe("_shared/.gittensory.yml"); + expect(manifest.review.sharedConfigSource).toBe("_shared/.loopover.yml"); }); it("appends private-config warnings without sharedConfigSource (#2046)", async () => { diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 6712c580c7..0837b9cc2f 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -200,8 +200,8 @@ describe("parseFocusManifestContent", () => { expect(manifest.warnings.join(" ")).toMatch(/not valid YAML/i); }); - it("parses .gittensory.yml.example with zero warnings (#2554: doc must match parser exactly)", () => { - const content = readFileSync(".gittensory.yml.example", "utf8"); + it("parses .loopover.yml.example with zero warnings (#2554: doc must match parser exactly)", () => { + const content = readFileSync(".loopover.yml.example", "utf8"); const manifest = parseFocusManifestContent(content, "repo_file"); expect(manifest.warnings).toEqual([]); expect(manifest.present).toBe(true); @@ -233,7 +233,7 @@ describe("parseFocusManifestContent", () => { }); }); -// #1670: `.gittensory.yml.example` is meant to be THE exhaustive reference -- every field a maintainer +// #1670: `.loopover.yml.example` is meant to be THE exhaustive reference -- every field a maintainer // can configure, with a comment, default, and allowed values. The "parses with zero warnings" test above // only proves whatever IS in the file is valid; it can never catch a field that's simply missing from the // doc entirely. Each map below uses `satisfies Record` so adding a field to a config type @@ -242,8 +242,8 @@ describe("parseFocusManifestContent", () => { // raw settings-layer aliases of a `gate:` field that already documents the same knob under its friendlier // name (`resolveEffectiveSettings` maps `gate.linkedIssue` -> `settings.linkedIssueGateMode`, etc.) -- // documenting both would just be confusing about which one to actually use. -describe(".gittensory.yml.example field-exhaustiveness (#1670)", () => { - const exampleContent = readFileSync(".gittensory.yml.example", "utf8"); +describe(".loopover.yml.example field-exhaustiveness (#1670)", () => { + const exampleContent = readFileSync(".loopover.yml.example", "utf8"); const GATE_FIELD_TOKENS = { enabled: "enabled:", @@ -295,7 +295,7 @@ describe(".gittensory.yml.example field-exhaustiveness (#1670)", () => { // silently vanishing from the exhaustiveness check. const SETTINGS_GATE_ALIASED_FIELDS = ["linkedIssueGateMode", "duplicatePrGateMode", "selfAuthoredLinkedIssueGateMode", "qualityGateMode", "qualityGateMinScore", "aiReviewMode", "aiReviewByok", "aiReviewProvider", "aiReviewModel", "aiReviewAllAuthors"] as const; - // Settings fields that are DELIBERATELY absent from `.gittensory.yml.example` (unlike the gate-aliased fields + // Settings fields that are DELIBERATELY absent from `.loopover.yml.example` (unlike the gate-aliased fields // above, these are never documented anywhere in the public template): agentGlobalFreezeOverride is an // operator-only emergency lever, settable only from the operator's own private self-host config (source: // "api_record" in parseSettingsOverride, focus-manifest.ts) -- never from a repo's own committed, maintainer- @@ -2037,7 +2037,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () = // "api_record" (normalizeSource, focus-manifest.ts) -- the operator-private-config trust level -- so // agentGlobalFreezeOverride parses through and can overlay the DB value. See the dedicated // "agentGlobalFreezeOverride: operator-only" describe block below for the source-gating itself (an - // explicit source: "repo_file" manifest, mirroring a real repo-owned `.gittensory.yml`, drops it instead). + // explicit source: "repo_file" manifest, mirroring a real repo-owned `.loopover.yml`, drops it instead). expect(resolveEffectiveSettings({ agentGlobalFreezeOverride: false } as unknown as RepositorySettings, m).agentGlobalFreezeOverride).toBe(true); }); @@ -2049,7 +2049,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () = expect(resolveEffectiveSettings({ agentGlobalFreezeOverride: false } as unknown as RepositorySettings, m).agentGlobalFreezeOverride).toBe(true); }); - it("source: repo_file (a real repo-owned .gittensory.yml) — drops it with an operator-only warning; the DB value survives", () => { + it("source: repo_file (a real repo-owned .loopover.yml) — drops it with an operator-only warning; the DB value survives", () => { const m = parseFocusManifest({ source: "repo_file", settings: { agentGlobalFreezeOverride: true } }); expect(m.settings.agentGlobalFreezeOverride).toBeUndefined(); expect(m.warnings).toContain("Ignored settings.agentGlobalFreezeOverride: operator-only, not settable from a repo-owned manifest."); @@ -2694,7 +2694,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () = expect(parsedFalse.settings.typeLabelsEnabled).toBe(false); // Simulates PR #1's private-config layering: a global default of `true` (DB, standing in for the - // global .gittensory.yml layer already merged upstream) overridden by a per-repo `settings:` block. + // global .loopover.yml layer already merged upstream) overridden by a per-repo `settings:` block. const db = { typeLabelsEnabled: true } as unknown as RepositorySettings; const eff = resolveEffectiveSettings(db, parseFocusManifest({ settings: { typeLabelsEnabled: false } })); expect(eff.typeLabelsEnabled).toBe(false); // settings: override wins over the DB/global-default value @@ -2713,7 +2713,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () = const db = { typeLabels: { bug: "kind:bug", feature: "kind:feature", priority: "kind:priority" } } as unknown as RepositorySettings; const eff = resolveEffectiveSettings(db, parseFocusManifest({ settings: { typeLabels: { priority: "custom:priority" } } })); // bug/feature must come from the DB-persisted value, NOT be reset to the built-in gittensor:* defaults — - // this is the regression this test guards: a `.gittensory.yml` naming only `priority` must never silently + // this is the regression this test guards: a `.loopover.yml` naming only `priority` must never silently // discard a DB-customized bug/feature label. expect(eff.typeLabels).toEqual({ bug: "kind:bug", feature: "kind:feature", priority: "custom:priority" }); }); @@ -2849,7 +2849,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () = } as unknown as RepositorySettings; const eff = resolveEffectiveSettings(db, parseFocusManifest({ settings: { linkedIssueLabelPropagation: { enabled: true } } })); // The DB-configured mappings must survive a manifest override that only names `enabled` — this is the - // regression this test guards: a `.gittensory.yml` flipping the feature on must never silently discard a + // regression this test guards: a `.loopover.yml` flipping the feature on must never silently discard a // DB-persisted mapping list back to the built-in empty default. expect(eff.linkedIssueLabelPropagation).toEqual({ enabled: true, @@ -4060,8 +4060,8 @@ describe("overlayReviewConfig / review.shared_config (#2046)", () => { it("preserves sharedConfigSource from the override when set", () => { const base = parseReviewConfigMapping({ tone: "house" }, []); - const override = { ...parseReviewConfigMapping({ profile: "assertive" }, []), sharedConfigSource: "_shared/.gittensory.yml" }; - expect(overlayReviewConfig(base, override).sharedConfigSource).toBe("_shared/.gittensory.yml"); + const override = { ...parseReviewConfigMapping({ profile: "assertive" }, []), sharedConfigSource: "_shared/.loopover.yml" }; + expect(overlayReviewConfig(base, override).sharedConfigSource).toBe("_shared/.loopover.yml"); }); it("is byte-identical to the override when the base is empty", () => { diff --git a/test/unit/gittensory-config-lint-script.test.ts b/test/unit/gittensory-config-lint-script.test.ts index 2e58d227d4..7f295e0630 100644 --- a/test/unit/gittensory-config-lint-script.test.ts +++ b/test/unit/gittensory-config-lint-script.test.ts @@ -9,22 +9,22 @@ import { MAX_FOCUS_MANIFEST_BYTES } from "../../src/signals/focus-manifest"; describe("formatLintReport (#2906)", () => { it("reports a valid manifest's summary and recognized fields, no warnings", () => { const result = lintManifestText("wantedPaths:\n - src/\n"); - expect(formatLintReport(".gittensory.yml", result)).toBe( - ".gittensory.yml: Manifest parsed 1 recognized field.\n recognized fields: wantedPaths", + expect(formatLintReport(".loopover.yml", result)).toBe( + ".loopover.yml: Manifest parsed 1 recognized field.\n recognized fields: wantedPaths", ); }); it("reports warnings without a recognized-fields line when none are recognized", () => { const result = lintManifestText("unknownSecretKey: super-secret-value\n"); - expect(formatLintReport(".gittensory.yml", result)).toBe( + expect(formatLintReport(".loopover.yml", result)).toBe( [ - ".gittensory.yml: Manifest has 2 warnings.", + ".loopover.yml: Manifest has 2 warnings.", " - Manifest contained no recognized focus fields; falling back to deterministic signals.", " - Manifest contains unknown top-level field: unknownSecretKey.", ].join("\n"), ); // Never echoes the raw supplied value into the report (#2906 dogfoods config-lint's own secret-redaction). - expect(formatLintReport(".gittensory.yml", result)).not.toContain("super-secret-value"); + expect(formatLintReport(".loopover.yml", result)).not.toContain("super-secret-value"); }); it("reports both recognized fields and warnings together for a partially-valid manifest", () => { diff --git a/test/unit/gittensory-focus-manifest.test.ts b/test/unit/gittensory-focus-manifest.test.ts index 50c0e48ae6..8d7964bf6e 100644 --- a/test/unit/gittensory-focus-manifest.test.ts +++ b/test/unit/gittensory-focus-manifest.test.ts @@ -32,8 +32,8 @@ describe("resolveGittensorySelfRepoFullName", () => { }); describe("Gittensory repo focus manifest", () => { - it("keeps bundled YAML aligned with the committed .gittensory.yml file", () => { - const onDisk = readFileSync(resolve(process.cwd(), ".gittensory.yml"), "utf8").trim(); + it("keeps bundled YAML aligned with the committed .loopover.yml file", () => { + const onDisk = readFileSync(resolve(process.cwd(), ".loopover.yml"), "utf8").trim(); expect(GITTENSORY_REPO_FOCUS_MANIFEST_YAML.trim()).toBe(onDisk); }); @@ -128,11 +128,9 @@ describe("Gittensory repo focus manifest", () => { } }); - it("prefers YAML manifest file candidates before JSON, and new-brand candidates before legacy ones (#4773)", () => { + it("prefers YAML manifest file candidates before JSON", () => { expect(MANIFEST_FILE_CANDIDATES[0]).toBe(".loopover.yml"); expect(MANIFEST_FILE_CANDIDATES).toContain(".loopover.json"); - expect(MANIFEST_FILE_CANDIDATES).toContain(".gittensory.yml"); // legacy — dual-read indefinitely - expect(MANIFEST_FILE_CANDIDATES).toContain(".gittensory.json"); - expect(MANIFEST_FILE_CANDIDATES.indexOf(".loopover.yml")).toBeLessThan(MANIFEST_FILE_CANDIDATES.indexOf(".gittensory.yml")); + expect(MANIFEST_FILE_CANDIDATES.indexOf(".loopover.yml")).toBeLessThan(MANIFEST_FILE_CANDIDATES.indexOf(".loopover.json")); }); }); diff --git a/test/unit/guardrail-config.test.ts b/test/unit/guardrail-config.test.ts index 6d0e1aa451..bdd1f5bd2f 100644 --- a/test/unit/guardrail-config.test.ts +++ b/test/unit/guardrail-config.test.ts @@ -5,13 +5,11 @@ import { resolveHardGuardrailGlobs, } from "../../src/review/guardrail-config"; -describe("CONFIG_AS_CODE_GUARDRAIL_GLOBS (#4773 — dual-brand config filenames)", () => { - it("guards both the new-brand .loopover.* config files and the legacy .gittensory.* ones", () => { +describe("CONFIG_AS_CODE_GUARDRAIL_GLOBS", () => { + it("guards the .loopover.* config files", () => { for (const ext of ["yml", "yaml", "json"]) { expect(CONFIG_AS_CODE_GUARDRAIL_GLOBS).toContain(`.loopover.${ext}`); expect(CONFIG_AS_CODE_GUARDRAIL_GLOBS).toContain(`.github/loopover.${ext}`); - expect(CONFIG_AS_CODE_GUARDRAIL_GLOBS).toContain(`.gittensory.${ext}`); - expect(CONFIG_AS_CODE_GUARDRAIL_GLOBS).toContain(`.github/gittensory.${ext}`); } }); }); diff --git a/test/unit/mcp-automation-state.test.ts b/test/unit/mcp-automation-state.test.ts index ce5906e91a..daad10ba54 100644 --- a/test/unit/mcp-automation-state.test.ts +++ b/test/unit/mcp-automation-state.test.ts @@ -117,7 +117,7 @@ describe("MCP gittensory_get_automation_state (#784)", () => { expect(data.pendingActionCount).toBe(201); }); - it("REGRESSION (#2912): honors a .gittensory.yml-only agentPaused: true override (DB row left at its false default)", async () => { + it("REGRESSION (#2912): honors a .loopover.yml-only agentPaused: true override (DB row left at its false default)", async () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 5); await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto" } }); @@ -125,7 +125,7 @@ describe("MCP gittensory_get_automation_state (#784)", () => { // so this only passes if the resolver (not the raw DB accessor) is consulted. vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString(); - if (url.endsWith("/.gittensory.yml")) return new Response("settings:\n agentPaused: true\n", { status: 200 }); + if (url.endsWith("/.loopover.yml")) return new Response("settings:\n agentPaused: true\n", { status: 200 }); return new Response("Not Found", { status: 404 }); }); diff --git a/test/unit/mcp-refresh-repo-docs.test.ts b/test/unit/mcp-refresh-repo-docs.test.ts index 676203bef5..315753febb 100644 --- a/test/unit/mcp-refresh-repo-docs.test.ts +++ b/test/unit/mcp-refresh-repo-docs.test.ts @@ -89,7 +89,7 @@ describe("MCP gittensory_refresh_repo_docs (#3003)", () => { const client = await connect(env); const result = await client.callTool({ name: "gittensory_refresh_repo_docs", arguments: { owner: "owner", repo: "widgets" } }); expect(result.isError).toBeFalsy(); - expect(result.structuredContent).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.gittensory.yml repoDocGeneration.enabled)" }); + expect(result.structuredContent).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.loopover.yml repoDocGeneration.enabled)" }); }); it("denies a static MCP-token caller when the repo is not in MCP_ACTUATION_REPO_ALLOWLIST", async () => { diff --git a/test/unit/miner-goal-spec-doc.test.ts b/test/unit/miner-goal-spec-doc.test.ts index 4a34a9f5e1..c72f3693f8 100644 --- a/test/unit/miner-goal-spec-doc.test.ts +++ b/test/unit/miner-goal-spec-doc.test.ts @@ -24,9 +24,9 @@ const SPEC_FIELDS = [ ] as const; describe("miner goal spec docs (#2300)", () => { - it("documents every MinerGoalSpec field and the relationship to .gittensory.yml", () => { + it("documents every MinerGoalSpec field and the relationship to .loopover.yml", () => { const doc = readFileSync(docPath, "utf8"); - expect(doc).toContain("Relationship to `.gittensory.yml`"); + expect(doc).toContain("Relationship to `.loopover.yml`"); expect(doc).toContain("wantedPaths"); expect(doc).toContain("blockedPaths"); for (const field of SPEC_FIELDS) { diff --git a/test/unit/miner-self-review-context.test.ts b/test/unit/miner-self-review-context.test.ts index efbe7bc344..28a5f095e1 100644 --- a/test/unit/miner-self-review-context.test.ts +++ b/test/unit/miner-self-review-context.test.ts @@ -310,10 +310,10 @@ describe("fetchSelfReviewContext (#5145)", () => { const result = await fetchSelfReviewContext("acme/widgets", { fetchImpl: fetchImpl as never }); expect(requestedPaths).toEqual([ - "https://raw.githubusercontent.com/acme/widgets/HEAD/.gittensory.yml", - "https://raw.githubusercontent.com/acme/widgets/HEAD/.github/gittensory.yml", - "https://raw.githubusercontent.com/acme/widgets/HEAD/.gittensory.json", - "https://raw.githubusercontent.com/acme/widgets/HEAD/.github/gittensory.json", + "https://raw.githubusercontent.com/acme/widgets/HEAD/.loopover.yml", + "https://raw.githubusercontent.com/acme/widgets/HEAD/.github/loopover.yml", + "https://raw.githubusercontent.com/acme/widgets/HEAD/.loopover.json", + "https://raw.githubusercontent.com/acme/widgets/HEAD/.github/loopover.json", ]); expect(result.manifest.gate).toBeDefined(); }); diff --git a/test/unit/predicted-gate.test.ts b/test/unit/predicted-gate.test.ts index d0598213bf..9290e94ce0 100644 --- a/test/unit/predicted-gate.test.ts +++ b/test/unit/predicted-gate.test.ts @@ -56,7 +56,7 @@ describe("buildPredictedGateVerdict", () => { expect(result.basis).toBe("public_config"); expect(result.conclusion).toBe("success"); expect(result.blockers).toHaveLength(0); - expect(result.note).toContain("public .gittensory.yml"); + expect(result.note).toContain("public .loopover.yml"); }); it("threads gate.aiReview.closeConfidence into the policy without disturbing the public-config verdict (#7)", () => { @@ -249,7 +249,7 @@ describe("buildPredictedGateVerdict", () => { }); it("predicts a BLOCK for an enforced path-INDEPENDENT pre-merge check the title fails (#11/#18)", () => { - // The repo's public .gittensory.yml enforces a conventional-style title; the PR title lacks "[FEAT]". + // The repo's public .loopover.yml enforces a conventional-style title; the PR title lacks "[FEAT]". const result = verdict({ gate: {}, review: { pre_merge_checks: [{ name: "Conventional title", title_contains: "[FEAT]", enforce: true }] }, diff --git a/test/unit/private-config.test.ts b/test/unit/private-config.test.ts index 44bc9e6fe2..56aa589205 100644 --- a/test/unit/private-config.test.ts +++ b/test/unit/private-config.test.ts @@ -20,23 +20,17 @@ async function readLocalManifestLoad(reader: RepoFocusManifestFetcher, repo: str } describe("localConfigCandidates (container-private config paths)", () => { - it("builds owner-folder → repo-folder → flat candidates (lowercased), new-brand before legacy, each in .yml/.yaml/.json order (#4773)", () => { + it("builds owner-folder → repo-folder → flat candidates (lowercased), each in .yml/.yaml/.json order", () => { expect(localConfigCandidates("JSONbored/metagraphed")).toEqual([ - // 1. owner-qualified folder — new-brand (.loopover.*) before legacy (.gittensory.*) + // 1. owner-qualified folder join("jsonbored__metagraphed", ".loopover.yml"), join("jsonbored__metagraphed", ".loopover.yaml"), join("jsonbored__metagraphed", ".loopover.json"), - join("jsonbored__metagraphed", ".gittensory.yml"), - join("jsonbored__metagraphed", ".gittensory.yaml"), - join("jsonbored__metagraphed", ".gittensory.json"), - // 2. bare repo-name folder — same new-brand-before-legacy order + // 2. bare repo-name folder join("metagraphed", ".loopover.yml"), join("metagraphed", ".loopover.yaml"), join("metagraphed", ".loopover.json"), - join("metagraphed", ".gittensory.yml"), - join("metagraphed", ".gittensory.yaml"), - join("metagraphed", ".gittensory.json"), - // 3. flat owner__repo file (#1390 back-compat) — brand-agnostic, so only 3 (not 6) candidates + // 3. flat owner__repo file (#1390 back-compat) "jsonbored__metagraphed.yml", "jsonbored__metagraphed.yaml", "jsonbored__metagraphed.json", @@ -53,20 +47,14 @@ describe("localConfigCandidates (container-private config paths)", () => { expect(localConfigCandidates("bad_owner/repo")).toEqual([]); expect(localConfigCandidates("-owner/repo")).toEqual([]); }); - it("exposes the dir-root global-fallback candidates, new-brand before legacy (#4773)", () => { - expect(GLOBAL_CONFIG_CANDIDATES).toEqual([ - ".loopover.yml", ".loopover.yaml", ".loopover.json", - ".gittensory.yml", ".gittensory.yaml", ".gittensory.json", - ]); + it("exposes the dir-root global-fallback candidates", () => { + expect(GLOBAL_CONFIG_CANDIDATES).toEqual([".loopover.yml", ".loopover.yaml", ".loopover.json"]); }); - it("exposes the shared-base candidates, sibling to the per-repo folders, new-brand before legacy (#1959, #4773)", () => { + it("exposes the shared-base candidates, sibling to the per-repo folders (#1959)", () => { expect(SHARED_BASE_CONFIG_CANDIDATES).toEqual([ join("_shared", ".loopover.yml"), join("_shared", ".loopover.yaml"), join("_shared", ".loopover.json"), - join("_shared", ".gittensory.yml"), - join("_shared", ".gittensory.yaml"), - join("_shared", ".gittensory.json"), ]); }); @@ -75,9 +63,6 @@ describe("localConfigCandidates (container-private config paths)", () => { join("owner___shared", ".loopover.yml"), join("owner___shared", ".loopover.yaml"), join("owner___shared", ".loopover.json"), - join("owner___shared", ".gittensory.yml"), - join("owner___shared", ".gittensory.yaml"), - join("owner___shared", ".gittensory.json"), "owner___shared.yml", "owner___shared.yaml", "owner___shared.json", @@ -128,7 +113,7 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("reads the owner-qualified folder file first (highest-priority per-repo candidate)", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "jsonbored__metagraphed")); - writeFileSync(join(dir, "jsonbored__metagraphed", ".gittensory.yml"), "gate:\n enabled: false\n"); + writeFileSync(join(dir, "jsonbored__metagraphed", ".loopover.yml"), "gate:\n enabled: false\n"); const reader = makeLocalManifestReader(dir); expect(reader).not.toBeNull(); expect(await readLocalManifestContent(reader!,"JSONbored/metagraphed")).toBe("gate:\n enabled: false\n"); @@ -137,7 +122,7 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("falls back to the bare repo-name folder when no owner-qualified folder exists", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "metagraphed")); - writeFileSync(join(dir, "metagraphed", ".gittensory.yaml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "metagraphed", ".loopover.yaml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"JSONbored/metagraphed")).toBe("gate:\n enabled: true\n"); }); @@ -149,18 +134,18 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { expect(await readLocalManifestContent(reader!,"owner/repo")).toBe('{"gate":{"enabled":true}}'); }); - it("falls back to the dir-root global .gittensory.yml for a repo with no per-repo file", async () => { + it("falls back to the dir-root global .loopover.yml for a repo with no per-repo file", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n"); + writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: false\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/unconfigured")).toBe("gate:\n enabled: false\n"); }); it("deep-merges a per-repo file over the global default: per-repo wins on shared keys, global fills the rest", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n duplicates: block\n"); // global + writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: false\n duplicates: block\n"); // global mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); // per-repo overrides only `enabled` + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); // per-repo overrides only `enabled` const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.gate.enabled).toBe(true); // per-repo wins on the shared key @@ -169,9 +154,9 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("replaces an array wholesale instead of concatenating it with the global default's", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "wantedPaths:\n - src/**\n - test/**\n"); + writeFileSync(join(dir, ".loopover.yml"), "wantedPaths:\n - src/**\n - test/**\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "wantedPaths:\n - docs/**\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "wantedPaths:\n - docs/**\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.wantedPaths).toEqual(["docs/**"]); @@ -179,9 +164,9 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("lets an explicit per-repo null clear a global-configured contributorOpenPrCap", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "settings:\n contributorOpenPrCap: 5\n"); + writeFileSync(join(dir, ".loopover.yml"), "settings:\n contributorOpenPrCap: 5\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "settings:\n contributorOpenPrCap: null\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "settings:\n contributorOpenPrCap: null\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.settings.contributorOpenPrCap).toBeNull(); // explicit null clears the global 5, not "unset" @@ -189,9 +174,9 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("lets an explicit per-repo null clear a global-configured accountAgeThresholdDays", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "settings:\n accountAgeThresholdDays: 14\n"); + writeFileSync(join(dir, ".loopover.yml"), "settings:\n accountAgeThresholdDays: 14\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "settings:\n accountAgeThresholdDays: null\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "settings:\n accountAgeThresholdDays: null\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.settings.accountAgeThresholdDays).toBeNull(); @@ -200,36 +185,36 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("treats a config file over the manifest size cap as unmergeable and falls back to the other file alone", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); const oversized = `# ${"x".repeat(MAX_FOCUS_MANIFEST_BYTES + 10)}\ngate:\n enabled: false\n`; - writeFileSync(join(dir, ".gittensory.yml"), oversized); // global, too large to attempt a merge against + writeFileSync(join(dir, ".loopover.yml"), oversized); // global, too large to attempt a merge against mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: true\n"); // oversized global dropped; per-repo raw text unchanged }); it("falls back to the per-repo file alone when the global default fails to parse", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "{ not valid json"); // starts with `{` → JSON.parse throws + writeFileSync(join(dir, ".loopover.yml"), "{ not valid json"); // starts with `{` → JSON.parse throws mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: true\n"); }); it("falls back to the global default alone when the per-repo file parses but isn't a mapping", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n"); + writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: false\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "[1, 2, 3]"); // valid JSON, but an array, not a mapping + writeFileSync(join(dir, "repo", ".loopover.yml"), "[1, 2, 3]"); // valid JSON, but an array, not a mapping const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: false\n"); }); it("returns the per-repo raw text (today's legacy priority) when BOTH files fail to parse as mappings", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "{ also broken"); + writeFileSync(join(dir, ".loopover.yml"), "{ also broken"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "{ broken json"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "{ broken json"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("{ broken json"); // flows downstream, which warns + ignores it, same as today }); @@ -242,107 +227,24 @@ describe("makeLocalManifestReader (GITTENSORY_REPO_CONFIG_DIR)", () => { it("does NOT serve the global fallback to an invalid repo full name (no per-repo candidates)", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n"); // global present + writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: false\n"); // global present const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"no-slash")).toBeNull(); // perRepo.length === 0 early return }); it("rejects traversal repo names instead of reading outside the private config directory", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dirname(dir), ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dirname(dir), ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/..")).toBeNull(); }); }); -describe("makeLocalManifestReader — dual-brand filename support (#4773)", () => { - it("(a) keeps working with ONLY the legacy .gittensory.yml present in the per-repo folder — zero changes required", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/repo")).toBe("gate:\n enabled: true\n"); - }); - - it("(a) keeps working with ONLY the legacy .gittensory.yml present as the dir-root global default", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n"); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/unconfigured")).toBe("gate:\n enabled: false\n"); - }); - - it("(b) reads the new-brand .loopover.yml when no legacy file is present, in the per-repo folder", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/repo")).toBe("gate:\n enabled: true\n"); - }); - - it("(b) reads the new-brand .loopover.yml when no legacy file is present, as the dir-root global default", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: false\n"); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/unconfigured")).toBe("gate:\n enabled: false\n"); - }); - - it("(b) reads new-brand .loopover.yaml / .loopover.json too, same as the legacy extensions", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - mkdirSync(join(dir, "yaml-repo")); - writeFileSync(join(dir, "yaml-repo", ".loopover.yaml"), "gate:\n enabled: true\n"); - mkdirSync(join(dir, "json-repo")); - writeFileSync(join(dir, "json-repo", ".loopover.json"), '{"gate":{"enabled":false}}'); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/yaml-repo")).toBe("gate:\n enabled: true\n"); - expect(await readLocalManifestContent(reader!, "owner/json-repo")).toBe('{"gate":{"enabled":false}}'); - }); - - it("(c) prefers new-brand .loopover.yml over a legacy .gittensory.yml sitting in the SAME per-repo folder — not merged", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: false\n duplicates: block\n"); - const reader = makeLocalManifestReader(dir); - const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!, "owner/repo")); - expect(manifest.gate.enabled).toBe(true); // new-brand file wins outright - expect(manifest.gate.duplicates).toBeNull(); // legacy file's content is NOT merged in, only the winner is read - }); - - it("(c) prefers new-brand .loopover.yml over a legacy .gittensory.yml at the SAME dir-root global default", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: true\n"); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n"); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/unconfigured")).toBe("gate:\n enabled: true\n"); - }); - - it("(c) prefers new-brand .loopover.json over a legacy .gittensory.yml, even though .yml normally beats .json within a brand", async () => { - // Brand is the OUTERMOST sort key: every .loopover.* extension is tried before any .gittensory.* extension. - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".loopover.json"), '{"gate":{"enabled":true}}'); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: false\n"); - const reader = makeLocalManifestReader(dir); - expect(await readLocalManifestContent(reader!, "owner/repo")).toBe('{"gate":{"enabled":true}}'); - }); - - it("(c) a per-repo new-brand file still wins over a legacy global default (per-repo priority is unaffected by brand)", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\n duplicates: block\n"); // legacy global - mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); // new-brand per-repo - const reader = makeLocalManifestReader(dir); - const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!, "owner/repo")); - expect(manifest.gate.enabled).toBe(true); // per-repo (new-brand) still outranks the global layer - expect(manifest.gate.duplicates).toBe("block"); // deep-merge across LAYERS still applies once each layer's winner is picked - }); -}); - describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("falls back to the shared base alone when neither a per-repo file nor a global default exists", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "gate:\n enabled: false\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "gate:\n enabled: false\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: false\n"); // byte-identical raw text, no merge attempted }); @@ -350,7 +252,7 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("byte-identical to pre-#1959 behavior when no shared base file is mounted at all (repo-only)", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: true\n"); // no _shared/ present → same as the existing repo-only test }); @@ -358,9 +260,9 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("deep-merges a per-repo file over a shared base with no global default present: per-repo wins, shared fills the rest", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "gate:\n enabled: false\n duplicates: block\n"); // shared house policy + writeFileSync(join(dir, "_shared", ".loopover.yml"), "gate:\n enabled: false\n duplicates: block\n"); // shared house policy mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); // repo overrides only `enabled` + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); // repo overrides only `enabled` const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.gate.enabled).toBe(true); // per-repo wins on the shared key @@ -370,10 +272,10 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("folds all three layers: per-repo wins over global, which wins over the shared base, per-field", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "gate:\n enabled: false\n duplicates: block\n linkedIssue: advisory\n"); // shared house policy - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n duplicates: off\n"); // global overrides duplicates only + writeFileSync(join(dir, "_shared", ".loopover.yml"), "gate:\n enabled: false\n duplicates: block\n linkedIssue: advisory\n"); // shared house policy + writeFileSync(join(dir, ".loopover.yml"), "gate:\n duplicates: off\n"); // global overrides duplicates only mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); // per-repo overrides enabled only + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); // per-repo overrides enabled only const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.gate.enabled).toBe(true); // from per-repo (highest priority) @@ -384,10 +286,10 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("replaces an array wholesale across the 3-layer fold instead of concatenating", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "wantedPaths:\n - shared/**\n"); - writeFileSync(join(dir, ".gittensory.yml"), "wantedPaths:\n - src/**\n - test/**\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "wantedPaths:\n - shared/**\n"); + writeFileSync(join(dir, ".loopover.yml"), "wantedPaths:\n - src/**\n - test/**\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "wantedPaths:\n - docs/**\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "wantedPaths:\n - docs/**\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.wantedPaths).toEqual(["docs/**"]); // per-repo array wins wholesale, shared/global arrays discarded @@ -396,10 +298,10 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("lets an explicit per-repo null clear a shared-base-configured value even when global doesn't mention the key", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "settings:\n contributorOpenPrCap: 5\n"); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: true\n"); // global present, but silent on contributorOpenPrCap + writeFileSync(join(dir, "_shared", ".loopover.yml"), "settings:\n contributorOpenPrCap: 5\n"); + writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: true\n"); // global present, but silent on contributorOpenPrCap mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "settings:\n contributorOpenPrCap: null\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "settings:\n contributorOpenPrCap: null\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.settings.contributorOpenPrCap).toBeNull(); // explicit null clears the shared 5, not "unset" @@ -408,10 +310,10 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("fails safe to repo+global (as if unmounted) when the shared base is malformed, and never blocks a review", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "{ not valid json"); // starts with `{` → JSON.parse throws - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n duplicates: block\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "{ not valid json"); // starts with `{` → JSON.parse throws + writeFileSync(join(dir, ".loopover.yml"), "gate:\n duplicates: block\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/repo")); expect(manifest.gate.enabled).toBe(true); // still merged from the two still-valid layers @@ -422,9 +324,9 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); const oversized = `# ${"x".repeat(MAX_FOCUS_MANIFEST_BYTES + 10)}\ngate:\n enabled: false\n`; - writeFileSync(join(dir, "_shared", ".gittensory.yml"), oversized); // too large to attempt a merge against + writeFileSync(join(dir, "_shared", ".loopover.yml"), oversized); // too large to attempt a merge against mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: true\n"); // oversized shared base dropped; per-repo raw text unchanged }); @@ -432,19 +334,19 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("falls back to the highest-priority present layer's raw text when ALL THREE fail to parse as mappings", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "{ shared also broken"); - writeFileSync(join(dir, ".gittensory.yml"), "{ global also broken"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "{ shared also broken"); + writeFileSync(join(dir, ".loopover.yml"), "{ global also broken"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "{ broken json"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "{ broken json"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("{ broken json"); // per-repo (highest priority) raw text, same downstream "malformed" handling }); - it("tries _shared/.gittensory.yml before .yaml before .json", async () => { + it("tries _shared/.loopover.yml before .yaml before .json", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yaml"), "gate:\n enabled: true\n"); - writeFileSync(join(dir, "_shared", ".gittensory.json"), '{"gate":{"enabled":false}}'); + writeFileSync(join(dir, "_shared", ".loopover.yaml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "_shared", ".loopover.json"), '{"gate":{"enabled":false}}'); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"owner/repo")).toBe("gate:\n enabled: true\n"); // .yaml found before .json is tried }); @@ -452,8 +354,8 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("keeps global precedence over the shared base for a repo named _shared", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "gate:\n enabled: true\nwantedPaths:\n - '**/*'\n"); - writeFileSync(join(dir, ".gittensory.yml"), "gate:\n enabled: false\nwantedPaths:\n - src/safe/**\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "gate:\n enabled: true\nwantedPaths:\n - '**/*'\n"); + writeFileSync(join(dir, ".loopover.yml"), "gate:\n enabled: false\nwantedPaths:\n - src/safe/**\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/_shared")); expect(manifest.gate.enabled).toBe(false); // global still overlays the reserved shared-base folder @@ -463,9 +365,9 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("still supports owner-qualified config for a repo named _shared", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "gate:\n enabled: false\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "gate:\n enabled: false\n"); mkdirSync(join(dir, "owner___shared")); - writeFileSync(join(dir, "owner___shared", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "owner___shared", ".loopover.yml"), "gate:\n enabled: true\n"); const reader = makeLocalManifestReader(dir); const manifest = parseFocusManifestContent(await readLocalManifestContent(reader!,"owner/_shared")); expect(manifest.gate.enabled).toBe(true); // explicit owner-qualified per-repo file remains highest priority @@ -474,7 +376,7 @@ describe("makeLocalManifestReader — shared base layer (#1959)", () => { it("does NOT serve the shared base to an invalid repo full name (no per-repo candidates)", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "gate:\n enabled: false\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "gate:\n enabled: false\n"); const reader = makeLocalManifestReader(dir); expect(await readLocalManifestContent(reader!,"no-slash")).toBeNull(); // perRepo.length === 0 early return, before the shared base is even read }); @@ -484,11 +386,11 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("records sharedConfigSource when the shared base contributes a review block", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n tone: shared-tone\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "review:\n tone: shared-tone\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "review:\n profile: assertive\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "review:\n profile: assertive\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); - expect(loaded?.sharedConfigSource).toBe(join("_shared", ".gittensory.yml")); + expect(loaded?.sharedConfigSource).toBe(join("_shared", ".loopover.yml")); const manifest = parseFocusManifestContent(loaded!.content!); expect(manifest.review.tone).toBe("shared-tone"); expect(manifest.review.profile).toBe("assertive"); @@ -497,7 +399,7 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("is byte-identical when the shared base is absent", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "review:\n tone: repo-only\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "review:\n tone: repo-only\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); expect(loaded?.sharedConfigSource).toBeNull(); expect(loaded?.warnings).toEqual([]); @@ -507,9 +409,9 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("warns and ignores a malformed shared base while still serving higher layers", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "{ broken shared"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "{ broken shared"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "review:\n tone: repo-tone\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "review:\n tone: repo-tone\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); expect(loaded?.sharedConfigSource).toBeNull(); expect(loaded?.warnings.some((w) => w.includes("review.shared_config"))).toBe(true); @@ -519,9 +421,9 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("fills review fields from the shared base when the per-repo file is silent on them", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n tone: house-tone\n security_focus: true\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "review:\n tone: house-tone\n security_focus: true\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "gate:\n enabled: true\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "gate:\n enabled: true\n"); const manifest = parseFocusManifestContent((await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"))!.content!); expect(manifest.review.tone).toBe("house-tone"); expect(manifest.review.securityFocus).toBe(true); @@ -530,9 +432,9 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("lets a higher-priority review null clear an inherited shared review block", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n footer:\n text: shared footer\n tone: house-tone\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "review:\n footer:\n text: shared footer\n tone: house-tone\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "review: null\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "review: null\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); const manifest = parseFocusManifestContent(loaded!.content!); expect(manifest.review.present).toBe(false); @@ -543,9 +445,9 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("lets a higher-priority non-mapping review value replace an inherited shared review block", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n footer:\n text: shared footer\n tone: house-tone\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "review:\n footer:\n text: shared footer\n tone: house-tone\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "review: false\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "review: false\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); const manifest = parseFocusManifestContent(loaded!.content!); expect(manifest.review.present).toBe(false); @@ -557,18 +459,18 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () it("sets sharedConfigSource when only the shared base is present for a repo", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n tone: only-shared\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "review:\n tone: only-shared\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); - expect(loaded?.sharedConfigSource).toBe(join("_shared", ".gittensory.yml")); + expect(loaded?.sharedConfigSource).toBe(join("_shared", ".loopover.yml")); expect(parseFocusManifestContent(loaded!.content!).review.tone).toBe("only-shared"); }); it("ignores a non-mapping shared review block without blocking higher layers", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review: [1, 2, 3]\n"); + writeFileSync(join(dir, "_shared", ".loopover.yml"), "review: [1, 2, 3]\n"); mkdirSync(join(dir, "repo")); - writeFileSync(join(dir, "repo", ".gittensory.yml"), "review:\n tone: repo-tone\n"); + writeFileSync(join(dir, "repo", ".loopover.yml"), "review:\n tone: repo-tone\n"); const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); expect(parseFocusManifestContent(loaded!.content!).review.tone).toBe("repo-tone"); }); diff --git a/test/unit/queue-2.test.ts b/test/unit/queue-2.test.ts index 3d2e73c8c4..ac15eaf96d 100644 --- a/test/unit/queue-2.test.ts +++ b/test/unit/queue-2.test.ts @@ -687,7 +687,7 @@ describe("queue processors", () => { AI_PUBLIC_COMMENTS_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "100000", // Both gates on: the global capability switch, and — unlike grounding/enrichment/RAG/reputation, which are - // env-only — the per-repo `.gittensory.yml` opt-in mocked below, so `dynamicReviewFeatures.cultureProfile` + // env-only — the per-repo `.loopover.yml` opt-in mocked below, so `dynamicReviewFeatures.cultureProfile` // (src/queue/processors.ts) actually evaluates its `&&` right-hand side true, not just short-circuits. GITTENSORY_REVIEW_CULTURE_PROFILE: "true", }); @@ -723,7 +723,7 @@ describe("queue processors", () => { if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); // The repo's own review.culture_profile opt-in. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n culture_profile: true\n"); } return Response.json({}); @@ -761,7 +761,7 @@ describe("queue processors", () => { AI_PUBLIC_COMMENTS_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "100000", // Both gates on: the global capability switch, and (like culture-profile above, unlike - // grounding/enrichment/RAG/reputation which are env-only) the per-repo `.gittensory.yml` opt-in mocked + // grounding/enrichment/RAG/reputation which are env-only) the per-repo `.loopover.yml` opt-in mocked // below, so `dynamicReviewFeatures.impactMap` (src/queue/processors.ts) actually evaluates // shouldComputeImpactMap's `&&` right-hand side true, not just short-circuits. GITTENSORY_REVIEW_IMPACT_MAP: "true", @@ -798,12 +798,12 @@ describe("queue processors", () => { if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); // The repo's own review.impact_map opt-in. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n impact_map: true\n"); } - // Real GitHub raw-content 404s for every other manifest candidate (incl. the new-brand `.loopover.*` - // candidates tried first, #4773) -- without this, Response.json({}) below would 200 the first candidate - // tried and mask the review.impact_map config crafted above. + // Real GitHub raw-content 404s for every other manifest candidate -- without this, + // Response.json({}) below would 200 the first candidate tried and mask the + // review.impact_map config crafted above. if (url.startsWith("https://raw.githubusercontent.com/")) return new Response("not found", { status: 404 }); return Response.json({}); }); @@ -2824,7 +2824,7 @@ describe("queue processors", () => { if (url.includes("/commits/a7/check-suites")) return Response.json({ check_suites: [] }); if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.includes(".gittensory.yml")) return new Response("Not Found", { status: 404 }); + if (url.includes(".loopover.yml")) return new Response("Not Found", { status: 404 }); if (url.endsWith("/check-runs") && init?.method === "POST") return Response.json({ id: 1 }); if (url === "https://api.gittensor.io/miners") return Response.json([]); if (url.endsWith("/graphql")) return Response.json({ data: {} }); @@ -3381,7 +3381,7 @@ describe("queue processors", () => { return new Response("not found", { status: 404 }); }); - // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code). + // .loopover.yml authoritatively sets the linked-issue blocker to "block" (config-as-code). await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); await processJob(env, { type: "github-webhook", @@ -3435,7 +3435,7 @@ describe("queue processors", () => { linkedIssueGateMode: "block", requireLinkedIssue: true, }); - // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code) — mirrors the + // .loopover.yml authoritatively sets the linked-issue blocker to "block" (config-as-code) — mirrors the // existing "publishes an opt-in gate..." test above, which needs the same manifest override for the raw // DB setting to take effect as a live hard block. await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); @@ -4782,7 +4782,7 @@ describe("queue processors", () => { agentDryRun: true, // dry-run → the actions are recorded but make no GitHub mutation }); await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code, as in the gate tests above). + // .loopover.yml authoritatively sets the linked-issue blocker to "block" (config-as-code, as in the gate tests above). await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); diff --git a/test/unit/queue-4.test.ts b/test/unit/queue-4.test.ts index 75dda8baf7..40fa7cec92 100644 --- a/test/unit/queue-4.test.ts +++ b/test/unit/queue-4.test.ts @@ -1510,7 +1510,7 @@ describe("queue processors", () => { expect(patchBodies[0]?.status).toBe("completed"); }); - it("disables the gate from .gittensory.yml (gate.enabled: false) even when repo settings enable it", async () => { + it("disables the gate from .loopover.yml (gate.enabled: false) even when repo settings enable it", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await persistRegistrySnapshot( env, @@ -1552,7 +1552,7 @@ describe("queue processors", () => { }, }); - // gate.enabled: false in .gittensory.yml disables the gate entirely — no Gate check is posted. + // gate.enabled: false in .loopover.yml disables the gate entirely — no Gate check is posted. expect(calls.gateChecks).toBe(0); }); @@ -2840,7 +2840,7 @@ describe("queue processors", () => { // #review-audit (#4220): the comment reads the LIVE `dirty` merge-state (not the stale stored one), so it must // NOT headline "safe to merge" while the disposition would auto-close the base-conflicting PR. expect(postedBody).not.toMatch(/safe to merge/i); - // #1955: no `.gittensory.yml` was fetched here (the raw-content URL isn't stubbed, so it 404s and the + // #1955: no `.loopover.yml` was fetched here (the raw-content URL isn't stubbed, so it 404s and the // manifest resolves to null) — review.effort_score is absent/default OFF, so the effort chip must NOT render. expect(postedBody).not.toMatch(/review effort:/); } finally { @@ -3468,7 +3468,7 @@ describe("queue processors", () => { // #4083: review.visual.enabled: false (config-as-code, VPS-only in practice) overrides the coarser // GITTENSORY_REVIEW_SCREENSHOTS + GITTENSORY_REVIEW_REPOS env-var gate above — same fixture as the sibling // test above (same webhook, same visual-file touch, same env flag ON), the ONLY difference being the - // .gittensory.yml content, so this isolates the new enabled:false branch in processors.ts. + // .loopover.yml content, so this isolates the new enabled:false branch in processors.ts. it("skips the capture pipeline entirely when review.visual.enabled is false, even though the env-var gate allows it (#4083)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), @@ -3509,7 +3509,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n visual:\n enabled: false\n"); } if (url === "https://api.gittensor.io/miners") { @@ -3610,11 +3610,11 @@ describe("queue processors", () => { } }); - // #1957: with the unified comment on AND `.gittensory.yml` opting into `review.changed_files_summary`, the + // #1957: with the unified comment on AND `.loopover.yml` opting into `review.changed_files_summary`, the // rendered comment gains the deterministic "Changed files" collapsible built from the SAME PR-files fetch the // unified branch already does for the readiness chip — no separate call, no AI. Mirrors the base unified-comment // test above but adds the manifest opt-in and asserts the new section's presence + content. - it("renders the Changed files summary when review.changed_files_summary is on in .gittensory.yml", async () => { + it("renders the Changed files summary when review.changed_files_summary is on in .loopover.yml", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); await persistRegistrySnapshot( env, @@ -3698,8 +3698,8 @@ describe("queue processors", () => { if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - // .gittensory.yml opts into the deterministic changed-files summary — no AI involved. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + // .loopover.yml opts into the deterministic changed-files summary — no AI involved. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n changed_files_summary: true\n"); } if (url.includes("/access_tokens")) { @@ -3782,11 +3782,11 @@ describe("queue processors", () => { } }); - // #1955: with the unified comment on AND `.gittensory.yml` opting into `review.effort_score`, the rendered + // #1955: with the unified comment on AND `.loopover.yml` opting into `review.effort_score`, the rendered // comment gains the deterministic, no-AI "review effort: N/5 (~M min)" chip — computed by estimateReviewEffort // from the SAME PR-files fetch the unified branch already does (no separate call). Mirrors the // changed_files_summary test above but asserts the effort chip's presence + exact value instead. - it("renders the review effort chip when review.effort_score is on in .gittensory.yml", async () => { + it("renders the review effort chip when review.effort_score is on in .loopover.yml", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); await persistRegistrySnapshot( env, @@ -3870,8 +3870,8 @@ describe("queue processors", () => { if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - // .gittensory.yml opts into the deterministic effort score — no AI involved. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + // .loopover.yml opts into the deterministic effort score — no AI involved. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n effort_score: true\n"); } if (url.includes("/access_tokens")) { @@ -3962,12 +3962,12 @@ describe("queue processors", () => { } }); - // #2051/#4147: with the unified comment on AND `.gittensory.yml` opting into `review.auto_merge_summary`, + // #2051/#4147: with the unified comment on AND `.loopover.yml` opting into `review.auto_merge_summary`, // the rendered comment gains the deterministic, no-AI "Auto-merge readiness" collapsible — computed from the // SAME live CI state, gate conclusion, mergeable_state, and linked-issue facts this pass already resolves // for the readiness chip and gate verdict, no extra fetch. Mirrors the effort_score test above but asserts // the auto-merge-readiness table's presence + condition marks instead. - it("renders the Auto-merge readiness collapsible when review.auto_merge_summary is on in .gittensory.yml", async () => { + it("renders the Auto-merge readiness collapsible when review.auto_merge_summary is on in .loopover.yml", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); await persistRegistrySnapshot( env, @@ -4051,8 +4051,8 @@ describe("queue processors", () => { if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - // .gittensory.yml opts into the deterministic auto-merge summary — no AI involved. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + // .loopover.yml opts into the deterministic auto-merge summary — no AI involved. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n auto_merge_summary: true\n"); } if (url.includes("/access_tokens")) { @@ -4135,13 +4135,13 @@ describe("queue processors", () => { } }); - // #2044: `.gittensory.yml` `review.tone` is folded into the AI reviewer's system prompt by + // #2044: `.loopover.yml` `review.tone` is folded into the AI reviewer's system prompt by // composeManifestReviewInstructions (src/signals/focus-manifest.ts), consumed by // src/queue/processors.ts's aiReviewCacheReadDecideAndRun. That composition is unit-tested in isolation // (focus-manifest.test.ts), but nothing previously drove the full webhook -> processJob -> runGittensoryAiReview // pipeline to confirm the resolved tone text actually reaches env.AI.run's system message. Mirrors the // changed_files_summary/effort_score tests above but captures the AI system prompt instead of the posted body. - it("threads review.tone from .gittensory.yml into the AI reviewer's system prompt (#2044)", async () => { + it("threads review.tone from .loopover.yml into the AI reviewer's system prompt (#2044)", async () => { let capturedSystem = ""; const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), @@ -4187,11 +4187,10 @@ describe("queue processors", () => { if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); // The repo's own review.tone opt-in (#2044) -- a maintainer voice brief, distinct from review.instructions. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n tone: Keep findings terse and skip pleasantries\n"); } - // Real GitHub raw-content 404s for every other manifest candidate (incl. the new-brand `.loopover.*` - // candidates tried first, #4773) -- without this, Response.json({}) below would 200 the first candidate + // Real GitHub raw-content 404s for every other manifest candidate -- without this, Response.json({}) below would 200 the first candidate // tried and mask the review.tone config crafted above. if (url.startsWith("https://raw.githubusercontent.com/")) return new Response("not found", { status: 404 }); return Response.json({}); @@ -4279,11 +4278,10 @@ describe("queue processors", () => { if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); // The repo's own review.exclude_paths opt-in -- a NON-EMPTY glob (#review-exclude-paths), unlike every // existing fingerprint-only assertion of this field elsewhere in this file. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response('review:\n exclude_paths:\n - "**/*.generated.ts"\n'); } - // Real GitHub raw-content 404s for every other manifest candidate (incl. the new-brand `.loopover.*` - // candidates tried first, #4773) -- without this, the generic Response.json({}) catch-all below would + // Real GitHub raw-content 404s for every other manifest candidate -- without this, the generic Response.json({}) catch-all below would // otherwise 200 the FIRST candidate tried and mask the exclude_paths config crafted above. if (url.startsWith("https://raw.githubusercontent.com/")) return new Response("not found", { status: 404 }); return Response.json({}); @@ -4309,10 +4307,10 @@ describe("queue processors", () => { expect(capturedUser).not.toContain("generatedMarker"); }); - // #2049: with the unified comment on AND `.gittensory.yml` setting `review.max_findings`, the processor wires + // #2049: with the unified comment on AND `.loopover.yml` setting `review.max_findings`, the processor wires // manifest caps into `buildUnifiedCommentBody` and the renderer truncates blocker/nit lists with a "+N more" // footer. Mirrors the effort_score test above but asserts display-only truncation instead. - it("truncates unified-comment blockers when review.max_findings is set in .gittensory.yml (#2049)", async () => { + it("truncates unified-comment blockers when review.max_findings is set in .loopover.yml (#2049)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); await persistRegistrySnapshot( env, @@ -4397,7 +4395,7 @@ describe("queue processors", () => { if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n max_findings:\n blockers: 0\n"); } if (url.includes("/access_tokens")) { @@ -4475,7 +4473,7 @@ describe("queue processors", () => { // `readiness_score_below_threshold` ADVISORY (never a blocker — readiness stays advisory-only, see // rules.test.ts) warning finding on every pass, giving a stable target to record a suppression signal against // and verify it is (or is not) suppressed from the rendered unified comment. The manifest is seeded DIRECTLY - // via upsertRepoFocusManifest (bypassing the 6h .gittensory.yml fetch cache) so each test's `.gittensory.yml` + // via upsertRepoFocusManifest (bypassing the 6h .loopover.yml fetch cache) so each test's `.loopover.yml` // fetch response is never actually needed on the hot path — it only serves as an inert 404 fallback. async function runReadinessWarningPass(env: Env, opts: { deliveryId: string; headSha: string; reviewMemoryManifest: boolean }) { await persistRegistrySnapshot( @@ -4561,7 +4559,7 @@ describe("queue processors", () => { if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("not found", { status: 404 }); } if (url.includes("/access_tokens")) { @@ -4630,7 +4628,7 @@ describe("queue processors", () => { return postedBody; } - it("FLAG-OFF (default): review.memory in .gittensory.yml alone never suppresses the readiness warning (operator kill-switch required)", async () => { + it("FLAG-OFF (default): review.memory in .loopover.yml alone never suppresses the readiness warning (operator kill-switch required)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); // review.memory: true in the manifest, but NO GITTENSORY_REVIEW_MEMORY env flag on this env -- byte-identical. const postedBody = await runReadinessWarningPass(env, { @@ -4784,7 +4782,7 @@ describe("queue processors", () => { } }); - // #1958: with inline comments AND finding categories both on in .gittensory.yml (finding_categories rides on + // #1958: with inline comments AND finding categories both on in .loopover.yml (finding_categories rides on // inline_comments, exactly like suggestions did for #1956), the model is asked to self-categorize each // inlineFindings item, and BOTH surfaces render it — the posted inline review comment label AND the unified // comment's new "Finding categories" collapsible. @@ -4829,12 +4827,11 @@ describe("queue processors", () => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // .gittensory.yml opts into inline comments AND finding categories together. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + // .loopover.yml opts into inline comments AND finding categories together. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { return new Response("review:\n inline_comments: true\n finding_categories: true\n"); } - // Real GitHub raw-content 404s for every other manifest candidate (incl. the new-brand `.loopover.*` - // candidates tried first, #4773) -- without this, Response.json({}) below would 200 the first candidate + // Real GitHub raw-content 404s for every other manifest candidate -- without this, Response.json({}) below would 200 the first candidate // tried and mask the inline_comments/finding_categories config crafted above. if (url.startsWith("https://raw.githubusercontent.com/")) return new Response("not found", { status: 404 }); if (url.includes("/pulls/8/files")) @@ -4958,12 +4955,11 @@ describe("queue processors", () => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") { // NOTE: the manifest key is camelCase `fixHandoff` (unlike snake-case `finding_categories`) — see focus-manifest parse. return new Response("review:\n inline_comments: true\n fixHandoff: true\n"); } - // Real GitHub raw-content 404s for every other manifest candidate (incl. the new-brand `.loopover.*` - // candidates tried first, #4773) -- without this, Response.json({}) below would 200 the first candidate + // Real GitHub raw-content 404s for every other manifest candidate -- without this, Response.json({}) below would 200 the first candidate // tried and mask the inline_comments/fixHandoff config crafted above. if (url.startsWith("https://raw.githubusercontent.com/")) return new Response("not found", { status: 404 }); if (url.includes("/pulls/9/files")) diff --git a/test/unit/queue-5.test.ts b/test/unit/queue-5.test.ts index 9161f78c6b..7a4e91b8de 100644 --- a/test/unit/queue-5.test.ts +++ b/test/unit/queue-5.test.ts @@ -1274,11 +1274,11 @@ describe("queue processors", () => { await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 307, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; // advisoryAiRouting is config-as-code only (never DB-writable via upsertRepositorySettings) — enable - // chatQa the real way, through the repo's published `.gittensory.yml` raw-fetch, same as production. + // chatQa the real way, through the repo's published `.loopover.yml` raw-fetch, same as production. vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1311,7 +1311,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n commandRateLimitPolicy: hold\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1348,7 +1348,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { // chatQa enabled, but commandRateLimitPolicy is NOT set -- pr_author must not be granted. return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n", { status: 200 }); } @@ -1384,7 +1384,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n commandRateLimitPolicy: hold\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1420,7 +1420,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n commandRateLimitPolicy: hold\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1456,7 +1456,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1511,7 +1511,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1561,7 +1561,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n chatQa: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1609,11 +1609,11 @@ describe("queue processors", () => { await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 309, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; // advisoryAiRouting is config-as-code only -- enable intentRouting the real way, through the repo's - // published `.gittensory.yml` raw-fetch, same as the chat full-dispatch test above. + // published `.loopover.yml` raw-fetch, same as the chat full-dispatch test above. vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1644,7 +1644,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1693,7 +1693,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1727,7 +1727,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1758,7 +1758,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1793,7 +1793,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1821,7 +1821,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1850,7 +1850,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -1882,7 +1882,7 @@ describe("queue processors", () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url.includes("raw.githubusercontent.com") && url.includes(".gittensory.yml")) { + if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); @@ -2383,7 +2383,7 @@ describe("queue processors", () => { it("notifies issue-watchers when a new grabbable maintainer-created issue opens (#699 path B)", async () => { const enqueued: Array<{ type: string; events?: Array<{ eventType: string; recipientLogin: string; pullNumber: number }> }> = []; const env = createTestEnv({ JOBS: { async send(message: { type: string }) { enqueued.push(message); } } as unknown as Queue }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .loopover.yml → empty manifest const watcherLogins = Array.from({ length: 205 }, (_, index) => `watcher-${String(index + 1).padStart(3, "0")}`); for (const login of watcherLogins) { await upsertIssueWatchSubscription(env, { login, repoFullName: "JSONbored/gittensory" }); @@ -2420,7 +2420,7 @@ describe("queue processors", () => { const enqueueNotifyEvaluateJobs = async (loginOrder: string[]): Promise }>> => { const enqueued: Array<{ type: string; events?: Array<{ dedupKey: string }> }> = []; const env = createTestEnv({ JOBS: { async send(message: { type: string }) { enqueued.push(message); } } as unknown as Queue }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .loopover.yml → empty manifest // listIssueWatchersForRepo has no ORDER BY -- insertion order IS read-back order, so inserting in a // different order here genuinely reproduces two logically-identical detection passes disagreeing on // notificationEvents' arrival order, exactly the redelivery scenario the review is concerned about. @@ -2458,7 +2458,7 @@ describe("queue processors", () => { it("appends issue-side slop findings to the issue advisory only when slop is opted in (#533)", async () => { const env = createTestEnv(); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .loopover.yml → empty manifest await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); await upsertRepositoryFromGitHub(env, { name: "other", full_name: "JSONbored/other", private: false, owner: { login: "JSONbored" } }, 123); await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", slopGateMode: "advisory" }); @@ -5254,7 +5254,7 @@ describe("queue processors", () => { }); // Authorization used to be hardcoded to maintainer-only here, ignoring whatever a repo's own - // .gittensory.yml commandAuthorization configured -- a self-hoster who wants their contributors to be + // .loopover.yml commandAuthorization configured -- a self-hoster who wants their contributors to be // able to trigger test generation had no way to widen it. It now respects settings.commandAuthorization, // the exact same resolved (and safely clamped) policy the text-command version already uses. it("dispatches generation for a COLLABORATOR (not just a maintainer) once the repo widens commandAuthorization for generate-tests", async () => { @@ -6638,7 +6638,7 @@ describe("queue processors", () => { it("never fetches a linked issue and keeps normal behavior when propagation is left at its default (disabled) (#priority-linked-issue-gate)", async () => { // Deliberately NOT "JSONbored/gittensory" (unlike its two sibling tests above): this repo's own - // `.gittensory.yml` now enables propagation for itself (#priority-linked-issue-gate-ownership + // `.loopover.yml` now enables propagation for itself (#priority-linked-issue-gate-ownership // dogfooding), and `resolveRepositorySettings` falls back to the bundled // `GITTENSORY_REPO_FOCUS_MANIFEST_YAML` copy of it whenever a live manifest fetch is unavailable // (`isGittensorySelfRepo`, `src/signals/focus-manifest-loader.ts`) -- exactly the case in this test's diff --git a/test/unit/queue-lifecycle-guards.test.ts b/test/unit/queue-lifecycle-guards.test.ts index 5695aeb605..77ce04a50b 100644 --- a/test/unit/queue-lifecycle-guards.test.ts +++ b/test/unit/queue-lifecycle-guards.test.ts @@ -1816,11 +1816,11 @@ describe("review-evasion protection (#review-evasion-protection)", () => { if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); if (url.includes("/pulls/42/files")) return Response.json([]); - // A .gittensory.yml content fetch (raw.githubusercontent.com) must resolve to SOMETHING with no opinion + // A .loopover.yml content fetch (raw.githubusercontent.com) must resolve to SOMETHING with no opinion // on reviewEvasionProtection -- otherwise a miss here falls through to the bundled JSONbored/gittensory // fallback manifest (gittensory-repo-focus-manifest.ts), whose OWN checked-in reviewEvasionProtection: // close would silently outrank every test below's DB-level override (yml > DB precedence, #config-as-code). - if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); + if (url.includes("raw.githubusercontent.com") && url.includes("loopover.y")) return new Response("source: repo_file\n", { status: 200 }); return new Response("not found", { status: 404 }); }); } @@ -3095,7 +3095,7 @@ describe("review-evasion protection (#review-evasion-protection)", () => { if (url.includes("/access_tokens")) return Response.json({ token: "t" }); if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); - if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); + if (url.includes("raw.githubusercontent.com") && url.includes("loopover.y")) return new Response("source: repo_file\n", { status: 200 }); return new Response("not found", { status: 404 }); }); const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); @@ -3147,7 +3147,7 @@ describe("review-evasion protection (#review-evasion-protection)", () => { if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); if (url.includes("/pulls/42/files")) return Response.json([]); - if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); + if (url.includes("raw.githubusercontent.com") && url.includes("loopover.y")) return new Response("source: repo_file\n", { status: 200 }); return new Response("not found", { status: 404 }); }); const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); @@ -4250,7 +4250,7 @@ describe("auto-action convergence: end-to-end plan+execute for the general heuri const url = input.toString(); const method = init?.method ?? "GET"; if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") return new Response("settings:\n hardGuardrailGlobs:\n - .github/workflows/**\n"); + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.loopover.yml") return new Response("settings:\n hardGuardrailGlobs:\n - .github/workflows/**\n"); if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); if (url.includes("/pulls/61/files")) return Response.json([{ filename: ".github/workflows/ci.yml", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+ x: 1" }]); diff --git a/test/unit/repo-doc-pr.test.ts b/test/unit/repo-doc-pr.test.ts index 582260e07b..5e4b906a54 100644 --- a/test/unit/repo-doc-pr.test.ts +++ b/test/unit/repo-doc-pr.test.ts @@ -39,7 +39,7 @@ async function seedInstalledRepo(env: ReturnType, options: await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: REPO, private: false, owner: { login: "owner" }, ...(options.defaultBranch !== undefined ? { default_branch: options.defaultBranch } : {}) }, 555); } -// #3002: repo-doc generation is opt-in per repo via .gittensory.yml `repoDocGeneration:` -- defaults to fully +// #3002: repo-doc generation is opt-in per repo via .loopover.yml `repoDocGeneration:` -- defaults to fully // disabled, so every test exercising behavior PAST that gate needs it explicitly enabled. Defaults `enabled` to // true here (the common case for these tests) while letting callers override scope/allowOverwriteExisting. async function seedRepoDocGenerationConfig(env: ReturnType, repoFullName: string, overrides: { enabled?: boolean; scope?: string[]; allowOverwriteExisting?: boolean } = {}): Promise { @@ -81,7 +81,7 @@ describe("openRepoDocPullRequest (#3000)", () => { expect(result).toEqual({ opened: false, reason: "repository is not installed" }); }); - it("#3002: declines by default when repo-doc generation has no .gittensory.yml config at all", async () => { + it("#3002: declines by default when repo-doc generation has no .loopover.yml config at all", async () => { const env = envWithKey(); await seedInstalledRepo(env, { defaultBranch: "main" }); await seedProfileData(env); @@ -91,16 +91,16 @@ describe("openRepoDocPullRequest (#3000)", () => { return new Response("unexpected", { status: 500 }); }); const result = await openRepoDocPullRequest(env, REPO, "live"); - expect(result).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.gittensory.yml repoDocGeneration.enabled)" }); + expect(result).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.loopover.yml repoDocGeneration.enabled)" }); }); - it("#3002: declines when explicitly disabled via .gittensory.yml", async () => { + it("#3002: declines when explicitly disabled via .loopover.yml", async () => { const env = envWithKey(); await seedInstalledRepo(env, { defaultBranch: "main" }); await seedProfileData(env); await seedRepoDocGenerationConfig(env, REPO, { enabled: false }); const result = await openRepoDocPullRequest(env, REPO, "live"); - expect(result).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.gittensory.yml repoDocGeneration.enabled)" }); + expect(result).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.loopover.yml repoDocGeneration.enabled)" }); }); it("#3002: declines when enabled but scope excludes \"agents\"", async () => { @@ -109,7 +109,7 @@ describe("openRepoDocPullRequest (#3000)", () => { await seedProfileData(env); await seedRepoDocGenerationConfig(env, REPO, { scope: ["skills"] }); const result = await openRepoDocPullRequest(env, REPO, "live"); - expect(result).toEqual({ opened: false, reason: 'repo-doc generation scope does not include "agents" for this repository (.gittensory.yml repoDocGeneration.scope)' }); + expect(result).toEqual({ opened: false, reason: 'repo-doc generation scope does not include "agents" for this repository (.loopover.yml repoDocGeneration.scope)' }); }); it("declines with the profile's own reason when the repo has no RAG index yet", async () => { diff --git a/test/unit/repo-doc-refresh-runner.test.ts b/test/unit/repo-doc-refresh-runner.test.ts index b550d21efc..98ea598626 100644 --- a/test/unit/repo-doc-refresh-runner.test.ts +++ b/test/unit/repo-doc-refresh-runner.test.ts @@ -101,7 +101,7 @@ describe("performRepoDocRefresh (#3003)", () => { await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: REPO, private: false, owner: { login: "owner" } }, 555); // repoDocGeneration is NOT enabled for this repo. const result = await performRepoDocRefresh(env, REPO); - expect(result).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.gittensory.yml repoDocGeneration.enabled)" }); + expect(result).toEqual({ opened: false, reason: "repo-doc generation is not enabled for this repository (.loopover.yml repoDocGeneration.enabled)" }); expect(await getLastRepoDocRefreshAttemptedAt(env, REPO)).not.toBeNull(); }); diff --git a/test/unit/selfhost-config-examples.test.ts b/test/unit/selfhost-config-examples.test.ts index 06bc01fda9..786c33284d 100644 --- a/test/unit/selfhost-config-examples.test.ts +++ b/test/unit/selfhost-config-examples.test.ts @@ -60,9 +60,9 @@ describe("config/examples/repo-override.gittensory.yml", () => { describe("the two examples together demonstrate the documented overlay behavior", () => { it("merges exactly as config/examples/README.md describes", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-example-config-")); - writeFileSync(join(dir, ".gittensory.yml"), readExample("global.gittensory.yml")); + writeFileSync(join(dir, ".loopover.yml"), readExample("global.gittensory.yml")); mkdirSync(join(dir, "owner__repo")); - writeFileSync(join(dir, "owner__repo", ".gittensory.yml"), readExample("repo-override.gittensory.yml")); + writeFileSync(join(dir, "owner__repo", ".loopover.yml"), readExample("repo-override.gittensory.yml")); const reader = makeLocalManifestReader(dir)!; const result = await reader("owner/repo"); const content = typeof result === "string" ? result : result!.content!; @@ -82,10 +82,10 @@ describe("all three examples together demonstrate the documented shared-base ove it("merges shared → global → per-repo exactly as config/examples/README.md describes", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-example-config-")); mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".gittensory.yml"), readExample("shared.gittensory.yml")); - writeFileSync(join(dir, ".gittensory.yml"), readExample("global.gittensory.yml")); + writeFileSync(join(dir, "_shared", ".loopover.yml"), readExample("shared.gittensory.yml")); + writeFileSync(join(dir, ".loopover.yml"), readExample("global.gittensory.yml")); mkdirSync(join(dir, "owner__repo")); - writeFileSync(join(dir, "owner__repo", ".gittensory.yml"), readExample("repo-override.gittensory.yml")); + writeFileSync(join(dir, "owner__repo", ".loopover.yml"), readExample("repo-override.gittensory.yml")); const reader = makeLocalManifestReader(dir)!; const result = await reader("owner/repo"); const content = typeof result === "string" ? result : result!.content!; @@ -98,25 +98,3 @@ describe("all three examples together demonstrate the documented shared-base ove expect(manifest.settings.contributorOpenPrCap).toBeNull(); // repo-override's explicit null still clears global's 3 }); }); - -describe("the same shipped examples parse and merge identically under the new-brand .loopover.yml filename (#4773)", () => { - it("merges shared → global → per-repo the same way when every layer is saved as .loopover.yml instead of .gittensory.yml", async () => { - const dir = mkdtempSync(join(tmpdir(), "gt-example-config-")); - mkdirSync(join(dir, "_shared")); - writeFileSync(join(dir, "_shared", ".loopover.yml"), readExample("shared.gittensory.yml")); - writeFileSync(join(dir, ".loopover.yml"), readExample("global.gittensory.yml")); - mkdirSync(join(dir, "owner__repo")); - writeFileSync(join(dir, "owner__repo", ".loopover.yml"), readExample("repo-override.gittensory.yml")); - const reader = makeLocalManifestReader(dir)!; - const result = await reader("owner/repo"); - const content = typeof result === "string" ? result : result!.content!; - const manifest = parseFocusManifestContent(content); - - // Identical assertions to the .gittensory.yml-named version above — only the on-disk filename changed. - expect(manifest.review.tone).toBe("friendly-terse"); - expect(manifest.gate.duplicates).toBe("block"); - expect(manifest.gate.enabled).toBe(true); - expect(manifest.wantedPaths).toEqual(["src/**"]); - expect(manifest.settings.contributorOpenPrCap).toBeNull(); - }); -}); diff --git a/wrangler.jsonc b/wrangler.jsonc index f1ea24a845..57e02398f3 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -59,7 +59,7 @@ "GITTENSORY_REVIEW_UNIFIED_COMMENT": "false", // Inline comments (#inline-comments): leave quiet, non-blocking inline comments on changed lines, on top of // the decision summary. Requires the repo in GITTENSORY_REVIEW_REPOS AND review.inline_comments in its - // .gittensory.yml. Default OFF — flag-OFF the model is never asked for inline findings (byte-identical). + // .loopover.yml. Default OFF — flag-OFF the model is never asked for inline findings (byte-identical). "GITTENSORY_REVIEW_INLINE_COMMENTS": "false", // Convergence (safety): run the ported safety scan in the review path — defang untrusted PR // title/body/diff before the AI reviewer sees it, and surface a secret-leak blocker from the diff. @@ -83,7 +83,7 @@ // never active for any repo regardless of a per-repo features.improvementSignal override. "GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL": "false", // #one-shot-review-cadence: fleet-wide default AI review re-trigger cadence, used only when a repo's - // .gittensory.yml review.auto_review.cadence is unset (a per-repo value always wins). Default OFF (false) + // .loopover.yml review.auto_review.cadence is unset (a per-repo value always wins). Default OFF (false) // = "one_shot": AI-generated content (main review, slop, linked-issue satisfaction) freezes after its // first pass; only an explicit maintainer retrigger spends a fresh call. Truthy = "continuous": every // push/CI-completion/sweep re-runs AI content generation, for operators who prefer that. The deterministic @@ -124,20 +124,20 @@ // Deterministic impact map (#2184, part of #1971): the operator-level kill-switch for computing (from the // RAG index + changed symbols) which other repo files plausibly need re-checking, then rendering that as a // compact section in the unified review comment and/or feeding it into AI-review grounding as additive - // reference context. ANDed with the per-repo `.gittensory.yml review.impact_map` opt-in — neither alone is + // reference context. ANDed with the per-repo `.loopover.yml review.impact_map` opt-in — neither alone is // sufficient. Default OFF — flag-OFF performs no symbol extraction, no RAG query, and adds no prompt/comment // section, byte-identical to today. "GITTENSORY_REVIEW_IMPACT_MAP": "false", // Repo quality-culture profile (#2995): at review time, append an ADDITIVE "REPO QUALITY-CULTURE PROFILE" // reference block — typical merged-PR size + common accepted labels, derived deterministically from this // repo's OWN recent_merged_pull_requests history. Reference-only grounding, exactly like RAG; never a - // gate/scoring input. Also requires the per-repo `.gittensory.yml` review.culture_profile: true opt-in. + // gate/scoring input. Also requires the per-repo `.loopover.yml` review.culture_profile: true opt-in. // Default OFF — flag-OFF performs no extra D1 read and keeps the reviewer prompt byte-identical. "GITTENSORY_REVIEW_CULTURE_PROFILE": "false", // Review memory (#2179, part of #1964): the operator-level kill-switch for repeat-false-positive // suppression — before an advisory (non-blocking) AI finding is surfaced, it is matched against this // repo's stored review_suppression signals (a maintainer's own past false-positive dismissals) and - // demoted/dropped on a match. ANDed with the per-repo `.gittensory.yml review.memory` opt-in — neither + // demoted/dropped on a match. ANDed with the per-repo `.loopover.yml review.memory` opt-in — neither // alone is sufficient. ADVISORY-ONLY: never applied to gate blockers, so it can never change the // merge/close disposition. Default OFF — flag-OFF performs no suppression-store read and no matching, // byte-identical to today. @@ -157,7 +157,7 @@ "GITTENSORY_REVIEW_SELFTUNE": "false", // Experimental `gittensor` plugin (the `experimental:` manifest block, first key): the operator-level // kill-switch for gittensory's original subnet mining-registry/scoring integration, now opt-in rather than - // a core dependency. ANDed with the per-repo `.gittensory.yml experimental.gittensor` opt-in — neither + // a core dependency. ANDed with the per-repo `.loopover.yml experimental.gittensor` opt-in — neither // alone is sufficient, and unlike `features:` there is no GITTENSORY_REVIEW_REPOS allowlist fallback. // Default OFF — flag-OFF (or every repo unset), refresh-registry is never enqueued (see src/index.ts), // registry/sync.ts persists nothing for this instance, and a self-host box makes zero outbound contact From ee81b1a523d0b2395646239d0e2c9b7b46294ccc Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:32:18 -0700 Subject: [PATCH 2/2] ci: retrigger Workers Builds (Cloudflare build-concurrency contention from #5648 merge)