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
28 changes: 25 additions & 3 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
},
);
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.");
Expand Down
22 changes: 21 additions & 1 deletion test/unit/mcp-cli-basics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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/);
Expand Down
32 changes: 32 additions & 0 deletions test/unit/support/mcp-cli-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export async function startFixtureServer(
repoDecisionErrorBody?: string;
repoDecisionErrorContentType?: string;
packetMarkdown?: string;
localBranchAnalysis?: unknown;
onPacketRequest?: (body: unknown) => void;
onApiRequest?: (request: IncomingMessage) => void;
} = {},
Expand Down Expand Up @@ -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" }] }));
Expand Down Expand Up @@ -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",
Expand Down
Loading