diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index c71566daf1..31ba73fa59 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -588,7 +588,7 @@ server.registerTool( local: result.local, preflight: result.analysis.preflight, prPacket: result.analysis.prPacket, - workspaceIntelligence: result.analysis.workspaceIntelligence, + workspaceIntelligence: publicSafeWorkspaceIntelligence(result.analysis.workspaceIntelligence), }); }, ); @@ -1428,7 +1428,7 @@ async function runCli(args) { }); const payload = command === "preflight" - ? { local: result.local, preflight: result.analysis.preflight, prPacket: result.analysis.prPacket, workspaceIntelligence: result.analysis.workspaceIntelligence } + ? { local: result.local, preflight: result.analysis.preflight, prPacket: result.analysis.prPacket, workspaceIntelligence: publicSafeWorkspaceIntelligence(result.analysis.workspaceIntelligence) } : result; if (options.json) { process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); @@ -1566,7 +1566,7 @@ function outputAgentPayload(payload, options, summary) { function writeBranchAnalysisCli(result, command) { const analysis = result.analysis; - const intelligence = analysis.workspaceIntelligence; + const intelligence = command === "preflight" ? publicSafeWorkspaceIntelligence(analysis.workspaceIntelligence) : analysis.workspaceIntelligence; process.stdout.write(`${analysis.summary}\n`); process.stdout.write(`Top action: ${analysis.nextActions?.[0]?.actionKind ?? "none"}\n`); if (analysis.nextActions?.[0]?.whyThisHelps?.length) { @@ -1615,6 +1615,28 @@ function writeWorkspaceIntelligenceCli(intelligence) { process.stdout.write(`- Rerun when: ${intelligence.rerunWhen}\n`); } +function publicSafeWorkspaceIntelligence(intelligence) { + if (!intelligence) return intelligence; + return { + ...intelligence, + blockers: { + ...intelligence.blockers, + accountState: [], + }, + rerunWhen: publicSafeRerunWhen(intelligence), + }; +} + +function publicSafeRerunWhen(intelligence) { + if (intelligence.baseFreshness?.status === "stale" || intelligence.baseFreshness?.status === "possibly_stale") { + return "Run `git fetch origin` and rerun; current diff size may be inflated by stale base state."; + } + if (intelligence.blockers?.branchQuality?.length) { + return "Rerun after fixing branch-quality blockers or adding explicit validation/linked-context evidence."; + } + return "Rerun after any branch, base, or PR state changes before opening/submitting."; +} + function requirePublicSafePacketMarkdown(markdown) { const unsafeLine = markdown.split(/\r?\n/).find((line) => isUnsafePublicPacketText(line)); if (unsafeLine) throw new Error("Refusing to print unsafe public packet markdown from the server."); diff --git a/test/unit/mcp-cli-basics.test.ts b/test/unit/mcp-cli-basics.test.ts index 60331d7c31..fbc9f3411d 100644 --- a/test/unit/mcp-cli-basics.test.ts +++ b/test/unit/mcp-cli-basics.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { closeFixtureServer, run } from "./support/mcp-cli-harness"; +import { closeFixtureServer, createPacketRepo, run, runAsync, startFixtureServer } from "./support/mcp-cli-harness"; describe("gittensory-mcp CLI — basics", () => { let tempDir: string | null = null; @@ -129,6 +129,26 @@ describe("gittensory-mcp CLI — basics", () => { expect(payload.node).toBe(process.version); }); + it("redacts private account-state workspace intelligence from preflight output", async () => { + tempDir = createPacketRepo(); + const url = await startFixtureServer(); + + const env = { + GITTENSORY_API_URL: url, + GITTENSORY_TOKEN: "session-token", + GITTENSORY_SKIP_NPM_VERSION_CHECK: "true", + }; + const jsonOutput = await runAsync(["preflight", "--login", "JSONbored", "--cwd", tempDir, "--repo", "JSONbored/gittensory", "--json"], env); + const payload = JSON.parse(jsonOutput) as { workspaceIntelligence: { blockers: { accountState: string[] }; rerunWhen: string } }; + expect(payload.workspaceIntelligence.blockers.accountState).toEqual([]); + expect(payload.workspaceIntelligence.rerunWhen).toBe("Rerun after any branch, base, or PR state changes before opening/submitting."); + expect(jsonOutput).not.toMatch(/Open PR count|Credibility|account\/queue maturity|projected score/i); + + const humanOutput = await runAsync(["preflight", "--login", "JSONbored", "--cwd", tempDir, "--repo", "JSONbored/gittensory"], env); + expect(humanOutput).not.toContain("Account/queue blockers:"); + expect(humanOutput).not.toMatch(/Open PR count|Credibility|account\/queue maturity|projected score/i); + }); + it("guides unknown commands to --help", () => { expect(() => run(["bogus-command"])).toThrow(/Unknown command: bogus-command/); expect(() => run(["bogus-command"])).toThrow(/gittensory-mcp --help/); diff --git a/test/unit/support/mcp-cli-harness.ts b/test/unit/support/mcp-cli-harness.ts index 32366e3647..0c58c02b6c 100644 --- a/test/unit/support/mcp-cli-harness.ts +++ b/test/unit/support/mcp-cli-harness.ts @@ -109,6 +109,7 @@ export async function startFixtureServer( repoDecisionErrorBody?: string; repoDecisionErrorContentType?: string; packetMarkdown?: string; + localBranchAnalysis?: unknown; onPacketRequest?: (body: unknown) => void; onApiRequest?: (request: IncomingMessage) => void; } = {}, @@ -223,6 +224,11 @@ export async function startFixtureServer( response.end(JSON.stringify(agentPacketFixture(options.packetMarkdown))); return; } + if (request.url === "/v1/local/branch-analysis" && request.method === "POST") { + await readJsonRequest(request); + response.end(JSON.stringify(options.localBranchAnalysis ?? localBranchAnalysisFixture())); + return; + } // #784 maintainer controls (agent approval queue + kill-switch). if (request.url === "/v1/repos/owner/repo/agent/pending-actions" && request.method === "GET") { response.end(JSON.stringify({ repoFullName: "owner/repo", pendingActions: [{ id: "pa-1", actionClass: "merge", pullNumber: 7, reason: "clean", status: "pending" }] })); @@ -290,6 +296,32 @@ export function agentPacketFixture(markdown = "# Public-safe PR packet\n\n## Lin }; } +export function localBranchAnalysisFixture() { + return { + login: "JSONbored", + repoFullName: "JSONbored/gittensory", + generatedAt: "2026-06-01T00:00:00.000Z", + summary: "Local branch preflight fixture.", + nextActions: [{ actionKind: "prepare_pr_packet", whyThisHelps: ["Keeps public packet safe."] }], + preflight: { status: "ready", findings: [] }, + prPacket: { titleSuggestion: "Local branch preflight", markdown: "# Public-safe PR packet\n" }, + workspaceIntelligence: { + version: 2, + changedFiles: { total: 1, binary: 0, deleted: 0, renamed: 0 }, + testEvidence: { level: "validation_commands" }, + branch: { pendingCommitCount: 1 }, + baseFreshness: { status: "fresh", warnings: [] }, + blockers: { + branchQuality: [], + accountState: ["Open PR count 4 exceeds threshold 2.", "Credibility 0.2 is below floor 0.8."], + }, + ciStatusHints: [], + rerunWhen: "Rerun after account/queue maturity blockers clear.", + }, + dataQuality: { signalFidelity: { status: "complete" } }, + }; +} + export function decisionPackFixture() { return { status: "ready",