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
4 changes: 3 additions & 1 deletion packages/gittensory-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

### Features

- Detect stale installs and API compatibility in doctor and status
- Detect stale installs and API compatibility in doctor and status (#28)

- Generate public-safe pr packets


## mcp-v0.2.0 - 2026-05-28
Expand Down
15 changes: 15 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,11 @@ function outputAgentPayload(payload, options, summary) {
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
return;
}
const packetMarkdown = payload?.prPacket?.markdown ?? payload?.actions?.find((action) => action?.actionType === "prepare_pr_packet")?.payload?.prPacket?.markdown;
if (typeof packetMarkdown === "string" && packetMarkdown.trim()) {
const safeMarkdown = requirePublicSafePacketMarkdown(packetMarkdown);
return process.stdout.write(safeMarkdown.endsWith("\n") ? safeMarkdown : `${safeMarkdown}\n`);
}
process.stdout.write(`${summary}\n`);
const actions = payload.actions ?? [];
for (const action of actions.slice(0, 3)) {
Expand All @@ -560,6 +565,16 @@ function outputAgentPayload(payload, options, summary) {
}
}

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.");
return markdown;
}

function isUnsafePublicPacketText(value) {
return /\b(reward\w*|score\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-\s]?trust|trust score|private[-\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:\\Users\\/i.test(value);
}

function printHelp() {
process.stdout.write(`Usage:
gittensory-mcp --stdio
Expand Down
1 change: 1 addition & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,7 @@ export const LocalBranchAnalysisSchema = z
}),
prPacket: z.object({
titleSuggestion: z.string(),
markdown: z.string(),
bodySections: z.array(z.object({ heading: z.string(), lines: z.array(z.string()) })),
reviewerNotes: z.array(z.string()),
validationSummary: z.object({
Expand Down
49 changes: 43 additions & 6 deletions src/signals/local-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export type LocalBranchAnalysis = {
};
prPacket: {
titleSuggestion: string;
markdown: string;
bodySections: Array<{ heading: string; lines: string[] }>;
reviewerNotes: string[];
validationSummary: {
Expand Down Expand Up @@ -238,6 +239,8 @@ export function buildLocalBranchAnalysis(args: {
roleContext,
laneSummary: lane.summary,
localFindings,
baseFreshness,
recommendedRerunCondition,
});
const scoreBlockers = [
...rewardRisk.scoreBlockers,
Expand Down Expand Up @@ -499,8 +502,10 @@ function buildPublicSafePrPacket(args: {
roleContext: RoleContext;
laneSummary: string;
localFindings: LocalBranchAnalysis["localFindings"];
baseFreshness: LocalBranchAnalysis["baseFreshness"];
recommendedRerunCondition: string;
}): LocalBranchAnalysis["prPacket"] {
const topPaths = args.changedFiles.slice(0, 8).map((file) => file.path);
const topPaths = args.changedFiles.slice(0, 8).map(changedFileSummary);
const publicSafeWarnings = [
...(args.roleContext.maintainerLane ? ["This is maintainer-lane context; present it as repo stewardship work."] : []),
...args.preflight.findings
Expand All @@ -510,13 +515,14 @@ function buildPublicSafePrPacket(args: {
.filter((finding) => finding.code !== "score_preview_warning" && finding.severity === "warning")
.flatMap((finding) => (finding.action ? [finding.action] : [finding.title])),
].filter(isPublicSafeText);
const nextSteps = [...publicSafeWarnings, args.baseFreshness.recommendation, args.recommendedRerunCondition, "Keep source upload disabled; this packet is based on local git metadata only."].filter(
(line): line is string => Boolean(line && isPublicSafeText(line)),
);
const validationLines =
args.validationSummary.commands.length > 0
? args.validationSummary.commands.map((entry) => `- ${entry.status}: ${entry.command}${entry.summary ? ` (${entry.summary})` : ""}`)
: ["- Not supplied yet."];
return {
titleSuggestion: args.title,
bodySections: [
const bodySections = [
{
heading: "Summary",
lines: ["Describe the user-visible problem or maintainer-facing improvement this branch addresses."],
Expand All @@ -525,6 +531,8 @@ function buildPublicSafePrPacket(args: {
heading: "Linked Context",
lines: args.preflight.linkedIssues.length > 0 ? args.preflight.linkedIssues.map((issue) => `- Closes #${issue}`) : ["- No linked issue detected; explain why this is a no-issue PR."],
},
{ heading: "Branch Freshness", lines: branchFreshnessLines(args.baseFreshness) },
{ heading: "Overlap/WIP Check", lines: overlapCautionLines(args.preflight.collisions) },
{
heading: "Changed Paths",
lines: topPaths.length > 0 ? topPaths.map((path) => `- ${path}`) : ["- No changed paths were detected from local metadata."],
Expand All @@ -533,7 +541,12 @@ function buildPublicSafePrPacket(args: {
heading: "Validation",
lines: validationLines,
},
],
{ heading: "Next Steps", lines: [...new Set(nextSteps)].slice(0, 6).map((line) => `- ${line.replace(/^- /, "")}`) },
];
return {
titleSuggestion: args.title,
markdown: renderPrPacketMarkdown(args.title, bodySections),
bodySections,
reviewerNotes: [
`Lane context: ${args.laneSummary}`,
`Review burden: ${args.preflight.reviewBurden}`,
Expand All @@ -544,6 +557,26 @@ function buildPublicSafePrPacket(args: {
};
}

function branchFreshnessLines(freshness: LocalBranchAnalysis["baseFreshness"]): string[] {
return [`- Base freshness: ${freshness.status}.`, ...freshness.warnings.filter(isPublicSafeText).map((warning) => `- ${warning}`), freshness.passedValidationCount > 0 ? `- Validation evidence supplied: ${freshness.passedValidationCount} passed command(s).` : "- No passed validation evidence was supplied."];
}

function overlapCautionLines(collisions: LocalDiffPreflightResult["collisions"]): string[] {
if (collisions.length === 0) return ["- No active overlap or WIP was detected from cached issue/PR metadata."];
return collisions
.slice(0, 3)
.map((cluster) => `- Possible overlap or WIP (${cluster.risk}): ${cluster.reason} Check ${cluster.items.slice(0, 3).map((item) => `${item.type === "pull_request" ? "PR" : item.type === "issue" ? "issue" : "merged PR"} #${item.number}`).join(", ")} before posting.`)
.filter(isPublicSafeText);
}

function changedFileSummary(file: LocalBranchChangedFile): string {
return `${file.previousPath ? `${safeRepoPath(file.previousPath)} -> ${safeRepoPath(file.path)}` : safeRepoPath(file.path)} (${file.status ?? "modified"}, ${file.binary ? "binary" : `+${nonNegative(file.additions)}/-${nonNegative(file.deletions)}`})`;
}

function renderPrPacketMarkdown(title: string, sections: Array<{ heading: string; lines: string[] }>): string {
return `${[`# ${title}`, ...sections.flatMap((section) => ["", `## ${section.heading}`, ...section.lines])].filter(isPublicSafeText).join("\n").trim()}\n`;
}

function summarizeValidation(validation: LocalBranchValidation[]): LocalBranchAnalysis["prPacket"]["validationSummary"] {
return {
passed: validation.filter((entry) => entry.status === "passed").length,
Expand All @@ -569,7 +602,11 @@ function firstCommitTitle(messages: string[] | undefined): string | undefined {
}

function isPublicSafeText(text: string): boolean {
return !/\b(reward|score|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|trust score)\b/i.test(text);
return !/\b(reward\w*|score\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-\s]?trust|trust score|private[-\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:\\Users\\/i.test(text);
}

function safeRepoPath(path: string): string {
return /^(\/Users\/|\/home\/|\/tmp\/|[A-Z]:\/Users\/)/i.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/");
}

function isTestFile(file: string): boolean {
Expand Down
50 changes: 50 additions & 0 deletions test/unit/local-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ describe("local branch analysis", () => {
expect(analysis.rewardRisk.rewardUpside.relevantLane).toBe("direct_pr");
expect(analysis.nextActions.map((action) => action.actionKind)).toContain("open_new_direct_pr");
expect(analysis.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "source_upload_disabled" })]));
expect(analysis.prPacket.markdown).toContain("## Branch Freshness");
expect(analysis.prPacket.markdown).toContain("## Overlap/WIP Check");
expect(analysis.prPacket.markdown).toContain("- Closes #7");
expect(analysis.prPacket.markdown).toContain("- passed: npm test -- cache");
expect(analysis.prPacket.markdown).toContain("metadata only");
expect(JSON.stringify(analysis.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score/i);
});

Expand Down Expand Up @@ -135,11 +140,55 @@ describe("local branch analysis", () => {
expect(analysis.baseFreshness.status).toBe("stale");
expect(analysis.baseFreshness.warnings.join(" ")).toMatch(/behind remote tracking SHA/i);
expect(analysis.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "stale_base_ref" })]));
expect(analysis.prPacket.markdown).toContain("## Branch Freshness");
expect(analysis.prPacket.markdown).toMatch(/Base freshness: stale|git fetch origin/i);
expect(analysis.preflight.findings.map((finding) => finding.code)).not.toContain("missing_test_evidence");
expect(analysis.preflight.findings.map((finding) => finding.code)).not.toContain("local_diff_missing_tests");
expect(analysis.recommendedRerunCondition).toMatch(/git fetch origin/i);
});

it("includes public-safe overlap caution and hides local absolute paths", () => {
const analysis = buildLocalBranchAnalysis({
input: {
login: "oktofeesh1",
repoFullName: repo.fullName,
body: "Fixes #7",
changedFiles: [
{ path: "/Users/example/work/src/cache.ts", previousPath: "src/cache-old.ts", additions: 12, deletions: 2, status: "renamed" },
{ path: "test/cache.test.ts", additions: 20, deletions: 0, status: "added" },
],
validation: [{ command: "npm test -- cache", status: "passed" }],
},
repo,
issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh fails", state: "open", labels: ["bug"], linkedPrs: [12] }],
pullRequests: [
{
repoFullName: repo.fullName,
number: 12,
title: "Fix cache refresh",
state: "open",
authorLogin: "someone-else",
authorAssociation: "CONTRIBUTOR",
labels: ["bug"],
linkedIssues: [7],
createdAt: "2026-05-25T00:00:00.000Z",
updatedAt: "2026-05-25T00:00:00.000Z",
},
],
profile,
outcomeHistory,
scoringSnapshot,
scoringProfile,
});

expect(analysis.preflight.status).toBe("needs_work");
expect(analysis.prPacket.markdown).toContain("Possible overlap or WIP");
expect(analysis.prPacket.markdown).toContain("PR #12");
expect(analysis.prPacket.markdown).toContain("[local path hidden]");
expect(analysis.prPacket.markdown).not.toContain("/Users/example");
expect(JSON.stringify(analysis.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score|\/Users\/example/i);
});

it("distinguishes fresh, merge-base-stale, and large unverified base states", () => {
const fresh = buildLocalBranchAnalysis({
input: {
Expand Down Expand Up @@ -314,6 +363,7 @@ describe("local branch analysis", () => {
expect.arrayContaining([
expect.objectContaining({ heading: "Changed Paths", lines: ["- No changed paths were detected from local metadata."] }),
expect.objectContaining({ heading: "Validation", lines: ["- Not supplied yet."] }),
expect.objectContaining({ heading: "Next Steps", lines: expect.arrayContaining([expect.stringContaining("metadata only")]) }),
]),
);
expect(analysis.summary).toContain("is the top private next action");
Expand Down
95 changes: 93 additions & 2 deletions test/unit/mcp-cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execFile, execFileSync } from "node:child_process";
import { createServer, type Server } from "node:http";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
Expand Down Expand Up @@ -273,6 +273,64 @@ describe("gittensory-mcp CLI", () => {
expect(explain.topAction.actionType).toBe("choose_next_work");
});

it("prints copy-paste public-safe markdown for agent packet output", async () => {
tempDir = mkdtempSync(join(tmpdir(), "gittensory-cli-"));
git(tempDir, "init");
git(tempDir, "config", "user.email", "test@example.com");
git(tempDir, "config", "user.name", "Gittensory Test");
git(tempDir, "config", "commit.gpgsign", "false");
git(tempDir, "remote", "add", "origin", "git@github.com:JSONbored/gittensory.git");
writeFileSync(join(tempDir, "README.md"), "fixture\n");
git(tempDir, "add", "README.md");
git(tempDir, "commit", "-m", "initial commit");
git(tempDir, "checkout", "-b", "codex/public-safe-pr-packets");
mkdirSync(join(tempDir, "src"));
writeFileSync(join(tempDir, "src/packet.ts"), "export const packet = true;\n");
const url = await startFixtureServer();
const output = await runAsync(
["agent", "packet", "--login", "oktofeesh1", "--cwd", tempDir, "--base", "HEAD", "--body", "Closes #39", "--validation", "passed|npm test|packet tests passed"],
{
GITTENSORY_API_URL: url,
GITTENSORY_TOKEN: "session-token",
GITTENSORY_CONFIG_DIR: tempDir,
},
);

expect(output).toContain("# Public-safe PR packet");
expect(output).toContain("## Validation");
expect(output).toContain("Closes #39");
expect(output).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|raw[-\s]?trust|private[-\s]?reviewability|reviewability|export const packet/i);
});

it("rejects unsafe server-provided packet markdown before non-json output", async () => {
tempDir = mkdtempSync(join(tmpdir(), "gittensory-cli-"));
git(tempDir, "init");
git(tempDir, "config", "user.email", "test@example.com");
git(tempDir, "config", "user.name", "Gittensory Test");
git(tempDir, "config", "commit.gpgsign", "false");
git(tempDir, "remote", "add", "origin", "git@github.com:JSONbored/gittensory.git");
writeFileSync(join(tempDir, "README.md"), "fixture\n");
git(tempDir, "add", "README.md");
git(tempDir, "commit", "-m", "initial commit");
git(tempDir, "checkout", "-b", "codex/public-safe-pr-packets");

for (const unsafePhrase of ["score: 1.15", "reward estimate", "wallet address", "hotkey id", "raw-trust: 0.7", "private-reviewability: ready"]) {
if (server) await new Promise<void>((resolve) => server?.close(() => resolve()));
server = null;
const url = await startFixtureServer({ packetMarkdown: `# Public-safe PR packet\n\n- ${unsafePhrase}\n` });
await expect(
runAsync(
["agent", "packet", "--login", "oktofeesh1", "--cwd", tempDir, "--base", "HEAD"],
{
GITTENSORY_API_URL: url,
GITTENSORY_TOKEN: "session-token",
GITTENSORY_CONFIG_DIR: tempDir,
},
),
).rejects.toThrow("Refusing to print unsafe public packet markdown from the server.");
}
});

it("rejects unsupported client snippets", () => {
expect(() => run(["init-client", "--print", "other"])).toThrow(/Unsupported client/);
});
Expand Down Expand Up @@ -316,7 +374,11 @@ function runAsync(args: string[], env: Record<string, string> = {}) {
});
}

async function startFixtureServer(options: { latestVersion?: string; minMcpVersion?: string; npmStatus?: number } = {}) {
function git(cwd: string, ...args: string[]) {
execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
}

async function startFixtureServer(options: { latestVersion?: string; minMcpVersion?: string; npmStatus?: number; packetMarkdown?: string } = {}) {
server = createServer((request, response) => {
response.setHeader("content-type", "application/json");
if (request.url && request.url.includes("gittensory-mcp/latest")) {
Expand Down Expand Up @@ -344,6 +406,10 @@ async function startFixtureServer(options: { latestVersion?: string; minMcpVersi
response.end(JSON.stringify(agentFixture()));
return;
}
if (request.url === "/v1/agent/prepare-pr-packet" && request.method === "POST") {
response.end(JSON.stringify(agentPacketFixture(options.packetMarkdown)));
return;
}
response.statusCode = 404;
response.end(JSON.stringify({ error: "not_found" }));
});
Expand All @@ -353,6 +419,31 @@ async function startFixtureServer(options: { latestVersion?: string; minMcpVersi
return `http://127.0.0.1:${address.port}`;
}

function agentPacketFixture(markdown = "# Public-safe PR packet\n\n## Linked Context\n- Closes #39\n\n## Validation\n- passed: npm test (packet tests passed)\n") {
return {
...agentFixture(),
actions: [
{
id: "action-packet",
runId: "run-1",
actionType: "prepare_pr_packet",
status: "ready",
recommendation: "Use this public-safe packet.",
why: ["Fixture"],
blockedBy: [],
publicSafeSummary: "Packet ready.",
approvalRequired: false,
safetyClass: "public_safe",
payload: {
prPacket: {
markdown,
},
},
},
],
};
}

function agentFixture() {
return {
run: {
Expand Down