Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/loopover-mcp/bin/loopover-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const CLI_COMMAND_SPEC = {
profile: ["list", "create", "switch", "remove"],
cache: ["status", "clear", "list"],
agent: ["plan", "status", "explain", "packet"],
maintain: ["status", "queue", "propose", "approve", "reject", "pause", "resume", "set-level", "precision", "outcome-calibration", "onboarding-pack", "audit-feed", "automation-state", "refresh-docs", "generate-issue-drafts", "plan-issues"],
maintain: ["status", "queue", "propose", "approve", "reject", "pause", "resume", "set-level", "precision", "selftune-audit", "outcome-calibration", "onboarding-pack", "audit-feed", "automation-state", "refresh-docs", "generate-issue-drafts", "plan-issues"],
};
const COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell"];
const AGENT_PROFILE_IDS = ["miner-planner", "miner-auto-dev", "maintainer-triage", "repo-owner-intake"];
Expand Down Expand Up @@ -1031,6 +1031,13 @@ const gatePrecisionShape = {
windowDays: z.number().int().positive().optional(),
};

// #7798: owner/repo plus the optional row cap the audit route's ?limit query accepts.
const selftuneOverrideAuditShape = {
owner: z.string().min(1),
repo: z.string().min(1),
limit: z.number().int().positive().optional(),
};

// #7764: mirrors the remote loopover_plan_repo_issues tool's input (src/mcp/server.ts's planRepoIssuesShape),
// minus the create-only `milestone` which this proxy (and the `maintain plan-issues` CLI) does not expose --
// forwarded to POST /v1/repos/:owner/:repo/issue-plan-drafts/generate. `goal` is the required maintainer
Expand Down Expand Up @@ -1538,6 +1545,12 @@ const STDIO_TOOL_DESCRIPTORS = [
category: "maintainer",
description: "Return per-gate-type false-positive precision for a repo's recorded gate blocks — blocked / blocked-then-merged counts and false-positive rates with low-sample guards. Optionally bounded by windowDays. Maintainer-authenticated; measurement only.",
},
{
name: "loopover_get_selftune_override_audit",
category: "maintainer",
description:
"Return the self-tune override audit trail for a repo — why the self-tune loop promoted, shadowed, or cleared a live gate override, newest first. Optionally capped by limit. Maintainer-authenticated; read-only measurement.",
},
{
name: "loopover_get_automation_state",
category: "agent",
Expand Down Expand Up @@ -3124,6 +3137,23 @@ registerStdioTool(
},
);

// #7798: read-only mirror of GET {repoBase}/selftune/overrides/audit — the same trail the
// `maintain selftune-audit` CLI verb prints. The API enforces maintainer authorization.
registerStdioTool(
"loopover_get_selftune_override_audit",
{
description: stdioToolDescription("loopover_get_selftune_override_audit"),
inputSchema: selftuneOverrideAuditShape,
},
async ({ owner, repo, limit }: any) => {
// The schema already rejects a non-positive limit, so an omitted limit is the only way to the server's
// default cap -- matching the route's own behaviour when ?limit is absent.
const query = limit ? `?limit=${encodeURIComponent(limit)}` : "";
const payload = await apiGet(`${toolRepoBase(owner, repo)}/selftune/overrides/audit${query}`);
return toolResult(`Self-tune override audit for ${owner}/${repo}.`, payload);
},
);

// #7752: read-side counterpart to the pause/resume/set-level write tools above. Proxies the same
// GET {repoBase}/automation-state the `maintain automation-state` CLI already calls — no duplicated HTTP path.
// Summary is intentionally branch-free (no ?? / ?. / ternaries) so codecov/patch stays at 100%; the full
Expand Down Expand Up @@ -3773,6 +3803,7 @@ function printMaintainHelp() {
` actions: ${MAINTAIN_ACTION_CLASSES.join(", ")}`,
` levels: ${MAINTAIN_AUTONOMY_LEVELS.join(", ")}`,
" precision [--window-days N] Show gate false-positive telemetry (blocked-then-merged per gate type).",
" selftune-audit [--limit N] Show the self-tune override audit trail (why an override promoted/cleared).",
" outcome-calibration Show slop-band merge rates and recommendation-outcome calibration.",
" [--window-days N] Bound the recommendation window (default: full history).",
" onboarding-pack [--refresh] Preview the repo's contributor onboarding pack.",
Expand Down Expand Up @@ -3913,6 +3944,22 @@ export async function maintainCli(args: any) {
emit(payload, lines.join("\n"));
return;
}
if (subcommand === "selftune-audit") {
// #7798 self-tune override audit: read-only mirror of GET {repoBase}/selftune/overrides/audit (the same
// trail the remote loopover_get_selftune_override_audit tool returns). The API enforces maintainer
// authorization; the CLI never decides locally. Optional --limit caps the rows the same way the route's
// ?limit query does (a non-positive value falls through to the server default).
const limit = Number(options.limit);
const query = limit > 0 ? `?limit=${encodeURIComponent(limit)}` : "";
const payload = await apiGet(`${repoBase}/selftune/overrides/audit${query}`);
const audit = payload.audit ?? [];
const lines = [
`Self-tune override audit for ${repoFullName}: ${audit.length} event(s).`,
...audit.map((event: any) => `- ${event.createdAt} ${event.eventType}${event.detail ? ` ${event.detail}` : ""}`),
];
emit(payload, lines.join("\n"));
return;
}
if (subcommand === "outcome-calibration") {
// #6735 outcome calibration: read-only measurement of whether higher-slop bands merge less often and how
// agent recommendations panned out. Same --window-days handling the sibling precision command uses (a
Expand Down Expand Up @@ -4049,7 +4096,7 @@ export async function maintainCli(args: any) {
return;
}
throw new Error(
`Unknown maintain subcommand: ${subcommand}. Use status | queue | propose <action-class> <pull-number> | approve <id> | reject <id> | pause | resume | set-level <action> <level> | precision | outcome-calibration | onboarding-pack | audit-feed | automation-state | refresh-docs | generate-issue-drafts | plan-issues.`,
`Unknown maintain subcommand: ${subcommand}. Use status | queue | propose <action-class> <pull-number> | approve <id> | reject <id> | pause | resume | set-level <action> <level> | precision | selftune-audit | outcome-calibration | onboarding-pack | audit-feed | automation-state | refresh-docs | generate-issue-drafts | plan-issues.`,
);
}

Expand Down Expand Up @@ -5265,7 +5312,7 @@ function printHelp() {
loopover-mcp doctor [--profile name] [--cwd path] [--exit-code] [--json]
loopover-mcp cache status|list|clear [--json]
loopover-mcp init-client --print codex|claude|cursor|mcp|vscode [--agent-profile miner-planner|maintainer-triage|repo-owner-intake] [--json]
loopover-mcp maintain status|queue|approve|reject|pause|resume|set-level|precision|outcome-calibration|onboarding-pack|audit-feed|automation-state|refresh-docs|generate-issue-drafts --repo owner/repo [--json] (see \`loopover-mcp maintain --help\`)
loopover-mcp maintain status|queue|approve|reject|pause|resume|set-level|precision|selftune-audit|outcome-calibration|onboarding-pack|audit-feed|automation-state|refresh-docs|generate-issue-drafts --repo owner/repo [--json] (see \`loopover-mcp maintain --help\`)
loopover-mcp decision-pack --login <github-login> [--json]
loopover-mcp repo-decision --login <github-login> --repo owner/repo [--json]
loopover-mcp contributor-profile [--login <github-login>] [--json]
Expand Down
41 changes: 41 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ import { SCENARIO_MAX_BRANCH_REF_CHARS, SCENARIO_MAX_LINKED_ISSUE_NUMBERS, SCENA
import { loadUpstreamStatus } from "../upstream/ruleset";
import {
authoritativeGateOverride,
listOverrideAudit,
loadOverride,
loadShadowOverride,
toLiveGateThresholdFields,
Expand Down Expand Up @@ -230,6 +231,13 @@ const ownerRepoWindowShape = {
windowDays: z.number().int().positive().optional(),
};

// (#7798) owner/repo plus the optional row cap the audit route's ?limit query accepts.
const selftuneOverrideAuditShape = {
owner: z.string().min(1),
repo: z.string().min(1),
limit: z.number().int().positive().optional(),
};

const windowOnlyShape = {
windowDays: z.number().int().positive().optional(),
};
Expand Down Expand Up @@ -1052,6 +1060,13 @@ const gatePrecisionOutputSchema = {
signals: z.array(z.string()).optional(),
};

// (#7798) self-tune override audit surfaced over MCP. Rows stay z.unknown(): listOverrideAudit is the
// single source of truth for the event fields, matching gatePrecisionOutputSchema's sub-report pattern.
const selftuneOverrideAuditOutputSchema = {
repoFullName: z.string().optional(),
audit: z.array(z.unknown()).optional(),
};

// #5825 - maintainer-authenticated skipped-PR audit trail, mirroring GET /v1/app/skipped-pr-audit's
// filters (all optional: a bare call returns the caller's own repo-scoped feed). No owner/repo shape
// here on purpose: unlike ownerRepoShape tools this report can legitimately span every repo the caller
Expand Down Expand Up @@ -1897,6 +1912,7 @@ export const MCP_TOOL_CATEGORIES: Record<string, McpToolCategory> = {
loopover_get_repo_outcome_patterns: "maintainer",
loopover_get_outcome_calibration: "maintainer",
loopover_get_gate_precision: "maintainer",
loopover_get_selftune_override_audit: "maintainer",
loopover_get_skipped_pr_audit: "maintainer",
loopover_get_fleet_analytics: "maintainer",
loopover_get_recommendation_quality: "maintainer",
Expand Down Expand Up @@ -2152,6 +2168,17 @@ export class LoopoverMcp {
async (input) => this.toolResult(await this.getGatePrecision(input)),
);

register(
"loopover_get_selftune_override_audit",
{
description:
"Return the self-tune override audit trail for a repo — why the self-tune loop promoted, shadowed, or cleared a live gate override, newest first, optionally capped by limit. Maintainer-authenticated; read-only measurement.",
inputSchema: selftuneOverrideAuditShape,
outputSchema: selftuneOverrideAuditOutputSchema,
},
async (input) => this.toolResult(await this.getSelftuneOverrideAudit(input)),
);

register(
"loopover_get_skipped_pr_audit",
{
Expand Down Expand Up @@ -3834,6 +3861,20 @@ export class LoopoverMcp {
};
}

// (#7798) MCP surface for GET /v1/repos/:owner/:repo/selftune/overrides/audit. Same per-repo read gate as
// getGatePrecision (requireRepoAccess); listOverrideAudit is read-only, already repo-scoped, and returns []
// on any storage error, so the tool mirrors the route's { repoFullName, audit } shape exactly. The summary is
// deliberately branch-free.
private async getSelftuneOverrideAudit(input: { owner: string; repo: string; limit?: number | undefined }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
await this.requireRepoAccess(fullName);
const audit = await listOverrideAudit(this.env as unknown as StorageEnv, fullName, input.limit);
return {
summary: `LoopOver self-tune override audit for ${fullName}: ${audit.length} event(s).`,
data: { repoFullName: fullName, audit },
};
}

// #5825 - repo-scope resolution for the skipped-PR audit tool. Mirrors skippedPrAuditRepoScope in
// src/api/routes.ts (same underlying loadControlPanelRoleSummary/loadControlPanelAccessScope calls,
// same maintainer/owner/operator role gate, same "no filter -> caller's own scoped repos" fallback),
Expand Down
1 change: 1 addition & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5524,6 +5524,7 @@ describe("api routes", () => {
expect(toolNames).toContain("loopover_preview_local_pr_score");
expect(toolNames).toContain("loopover_explain_score_breakdown");
expect(toolNames).toContain("loopover_get_outcome_calibration");
expect(toolNames).toContain("loopover_get_selftune_override_audit");
expect(toolNames).toContain("loopover_get_registry_changes");
expect(toolNames).toContain("loopover_get_registry_snapshot");
expect(toolNames).toContain("loopover_get_upstream_drift");
Expand Down
2 changes: 1 addition & 1 deletion test/unit/mcp-cli-basics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ describe("loopover-mcp CLI — basics", () => {
expect(ps).toContain("[System.Management.Automation.CompletionResult]::new");
expect(ps).toContain("$commands = @('login', 'logout'");
expect(ps).toContain(
"'maintain' = @('status', 'queue', 'propose', 'approve', 'reject', 'pause', 'resume', 'set-level', 'precision', 'outcome-calibration', 'onboarding-pack', 'audit-feed', 'automation-state', 'refresh-docs', 'generate-issue-drafts', 'plan-issues')",
"'maintain' = @('status', 'queue', 'propose', 'approve', 'reject', 'pause', 'resume', 'set-level', 'precision', 'selftune-audit', 'outcome-calibration', 'onboarding-pack', 'audit-feed', 'automation-state', 'refresh-docs', 'generate-issue-drafts', 'plan-issues')",
);
});

Expand Down
Loading