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
32 changes: 30 additions & 2 deletions plugins/flow/mcp-server/dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -30369,6 +30369,34 @@ function resolveGhRepoIdentity(execSyncImpl) {
return null;
}
}
function resolvePluginRepoIdentity(readPkgJsonImpl) {
try {
const readImpl = readPkgJsonImpl ?? (() => {
const { readFileSync: readFileSync6 } = __require("node:fs");
const { fileURLToPath: fileURLToPath4 } = __require("node:url");
const nodePath = __require("node:path");
const here = nodePath.dirname(fileURLToPath4(import.meta.url));
const pkgPath = nodePath.resolve(here, "..", "package.json");
return readFileSync6(pkgPath, "utf-8");
});
const raw = readImpl();
if (raw === null) return null;
const pkg = JSON.parse(raw);
const repoField = pkg.repository;
if (!repoField) return null;
const urlStr = typeof repoField === "string" ? repoField : repoField.url ?? "";
if (!urlStr) return null;
const match = urlStr.match(
/github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?(?:\/)?$/
);
if (!match) return null;
const [, owner, repo] = match;
if (!owner || !repo) return null;
return { owner, repo };
} catch {
return null;
}
}

// src/tools/write-native-story.ts
var WriteNativeStoryInputSchema = external_exports.object({
Expand Down Expand Up @@ -31448,7 +31476,7 @@ async function recordMaintainerFeedback(opts) {
let issueUrl;
let issueTitle;
let issueBody;
const repoIdentity = resolveGhRepoIdentity(opts.execSyncImpl);
const repoIdentity = resolvePluginRepoIdentity(opts.readPluginPkgJsonImpl);
if (repoIdentity !== null) {
issueTitle = composeFeedbackIssueTitle(validated);
issueBody = composeFeedbackIssueBody(validated);
Expand Down Expand Up @@ -45137,7 +45165,7 @@ async function reviewMaintainerInbox(opts) {
if (filenames.length === 0) {
return { ok: true, emptyInbox: true, items: [], malformedCount: 0 };
}
const repoIdentity = resolveGhRepoIdentity(opts.execSyncImpl);
const repoIdentity = resolvePluginRepoIdentity(opts.readPluginPkgJsonImpl);
const items = [];
let malformedCount = 0;
for (const filename of filenames) {
Expand Down
32 changes: 30 additions & 2 deletions plugins/flow/mcp-server/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52005,6 +52005,34 @@ function resolveGhRepoIdentity(execSyncImpl) {
return null;
}
}
function resolvePluginRepoIdentity(readPkgJsonImpl) {
try {
const readImpl = readPkgJsonImpl ?? (() => {
const { readFileSync: readFileSync6 } = __require("node:fs");
const { fileURLToPath: fileURLToPath4 } = __require("node:url");
const nodePath = __require("node:path");
const here = nodePath.dirname(fileURLToPath4(import.meta.url));
const pkgPath = nodePath.resolve(here, "..", "package.json");
return readFileSync6(pkgPath, "utf-8");
});
const raw = readImpl();
if (raw === null) return null;
const pkg = JSON.parse(raw);
const repoField = pkg.repository;
if (!repoField) return null;
const urlStr = typeof repoField === "string" ? repoField : repoField.url ?? "";
if (!urlStr) return null;
const match = urlStr.match(
/github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?(?:\/)?$/
);
if (!match) return null;
const [, owner, repo] = match;
if (!owner || !repo) return null;
return { owner, repo };
} catch {
return null;
}
}

// src/tools/write-native-story.ts
var WriteNativeStoryInputSchema = external_exports.object({
Expand Down Expand Up @@ -52487,7 +52515,7 @@ async function recordMaintainerFeedback(opts) {
let issueUrl;
let issueTitle;
let issueBody;
const repoIdentity = resolveGhRepoIdentity(opts.execSyncImpl);
const repoIdentity = resolvePluginRepoIdentity(opts.readPluginPkgJsonImpl);
if (repoIdentity !== null) {
issueTitle = composeFeedbackIssueTitle(validated);
issueBody = composeFeedbackIssueBody(validated);
Expand Down Expand Up @@ -58688,7 +58716,7 @@ async function reviewMaintainerInbox(opts) {
if (filenames.length === 0) {
return { ok: true, emptyInbox: true, items: [], malformedCount: 0 };
}
const repoIdentity = resolveGhRepoIdentity(opts.execSyncImpl);
const repoIdentity = resolvePluginRepoIdentity(opts.readPluginPkgJsonImpl);
const items = [];
let malformedCount = 0;
for (const filename of filenames) {
Expand Down
4 changes: 4 additions & 0 deletions plugins/flow/mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"private": true,
"type": "module",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/jackmcintyre/crew"
},
"scripts": {
"build": "tsc -p tsconfig.json && node scripts/normalise-dist.mjs && node scripts/bundle.mjs && node scripts/assert-bundle.mjs dist/index.js dist/cli.js && node scripts/assert-clean-install.mjs",
"build:watch": "node scripts/watch-and-normalise.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ import type { MaintainerFeedbackItem } from "../../schemas/maintainer-feedback.j
let scratch: string;
let root: string;

const STUB_EXEC_SYNC = (cmd: string, _opts: { encoding: "utf-8" }): string => {
if (cmd === "gh repo view --json owner,name") {
return JSON.stringify({ owner: { login: "test-owner" }, name: "test-repo" });
}
throw new Error(`Unexpected command: ${cmd}`);
};
const STUB_READ_PLUGIN_PKG_JSON = (): string =>
JSON.stringify({
name: "flow",
repository: { type: "git", url: "https://github.com/test-plugin-owner/test-plugin-repo" },
});

const INBOX_REL = path.join(".flow", "maintainer-inbox");

Expand Down Expand Up @@ -121,15 +120,15 @@ describe("dismissMaintainerFeedback (b) — reviewMaintainerInbox ignores dismis
// Sanity: both present before dismiss.
const before = await reviewMaintainerInbox({
targetRepoRoot: root,
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});
expect(before.items.map((i) => i.id).sort()).toEqual([ITEM_A.id, ITEM_B.id].sort());

await dismissMaintainerFeedback({ targetRepoRoot: root, id: ITEM_A.id });

const after = await reviewMaintainerInbox({
targetRepoRoot: root,
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});
expect(after.items).toHaveLength(1);
expect(after.items[0]!.id).toBe(ITEM_B.id);
Expand All @@ -142,7 +141,7 @@ describe("dismissMaintainerFeedback (b) — reviewMaintainerInbox ignores dismis

const after = await reviewMaintainerInbox({
targetRepoRoot: root,
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});
expect(after.emptyInbox).toBe(true);
expect(after.items).toHaveLength(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ import {
let scratch: string;
let root: string;

/** Stub plugin package.json — returns a recognisable plugin repo identity. */
const STUB_READ_PLUGIN_PKG_JSON = (): string =>
JSON.stringify({
name: "flow",
repository: { type: "git", url: "https://github.com/test-plugin-owner/test-plugin-repo" },
});

/** Simulates the repository field being absent / the package.json unreadable. */
const FAILING_READ_PLUGIN_PKG_JSON = (): string | null => null;

// Keep STUB_EXEC_SYNC for resolveGhRepoIdentity unit tests (non-retargeted path).
const STUB_EXEC_SYNC = (cmd: string, _opts: { encoding: "utf-8" }): string => {
if (cmd === "gh repo view --json owner,name") {
return JSON.stringify({ owner: { login: "test-owner" }, name: "test-repo" });
Expand Down Expand Up @@ -352,7 +363,7 @@ describe("renderFeedbackLinkBlock (AC1/AC3)", () => {
// ---------------------------------------------------------------------------

describe("recordMaintainerFeedback → issueUrl (AC1/AC2)", () => {
it("returns issueUrl when gh stub succeeds (AC1)", async () => {
it("returns issueUrl targeting the plugin repo when plugin pkg.json stub succeeds (AC1)", async () => {
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
Expand All @@ -361,13 +372,13 @@ describe("recordMaintainerFeedback → issueUrl (AC1/AC2)", () => {
trigger: "retro-analyst / cycle-end retro, story native:01TESTXYZ",
suggested_direction: "Add a recordMaintainerFeedback seam.",
},
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

expect(result.ok).toBe(true);
expect(result.issueUrl).toBeDefined();
expect(result.issueUrl).toMatch(
/^https:\/\/github\.com\/test-owner\/test-repo\/issues\/new\?/,
/^https:\/\/github\.com\/test-plugin-owner\/test-plugin-repo\/issues\/new\?/,
);
expect(result.issueUrl).toContain("title=");
expect(result.issueUrl).toContain("body=");
Expand All @@ -381,7 +392,7 @@ describe("recordMaintainerFeedback → issueUrl (AC1/AC2)", () => {
tool_area: "test-area",
trigger: "generalist-dev / story native:01TEST",
},
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

const url = result.issueUrl ?? "";
Expand All @@ -394,15 +405,15 @@ describe("recordMaintainerFeedback → issueUrl (AC1/AC2)", () => {
expect(url).toContain("?title=");
});

it("issueUrl is absent (not null, just missing) when gh is unavailable — inbox still written (AC2/fail-soft)", async () => {
it("issueUrl is absent (not null, just missing) when plugin pkg.json field absent — inbox still written (AC2/fail-soft)", async () => {
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
problem: "A structural limitation.",
tool_area: "test-area",
trigger: "generalist-dev / story native:01TEST",
},
execSyncImpl: FAILING_EXEC_SYNC,
readPluginPkgJsonImpl: FAILING_READ_PLUGIN_PKG_JSON,
});

// Inbox write must still succeed.
Expand All @@ -425,7 +436,7 @@ describe("recordMaintainerFeedback → issueUrl (AC1/AC2)", () => {
trigger: "sentinel-trigger-text",
suggested_direction: "sentinel-suggested-direction",
},
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

const url = result.issueUrl ?? "";
Expand All @@ -445,7 +456,7 @@ describe("recordMaintainerFeedback → issueUrl (AC1/AC2)", () => {
tool_area: "some-tool",
trigger: "some-trigger",
},
execSyncImpl: STUB_EXEC_SYNC,
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

expect(result.ok).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,127 @@ describe("maintainerInboxItemPath helper", () => {
expect(path.basename(p)).not.toContain(":");
});
});

// ---------------------------------------------------------------------------
// AC2 (story native:01KW5WMS33XC463QM60AXDGK81)
// recordMaintainerFeedback — live-session issueUrl targets the plugin's own repo
// ---------------------------------------------------------------------------

/** Stub plugin package.json returning a recognisable plugin repo identity. */
const STUB_READ_PLUGIN_PKG_JSON = (): string =>
JSON.stringify({
name: "flow",
repository: {
type: "git",
url: "https://github.com/test-plugin-owner/test-plugin-repo",
},
});

describe("recordMaintainerFeedback (AC2) — live-session issueUrl targets plugin repo", () => {
it("issueUrl is present and targets the plugin repo when readPluginPkgJsonImpl succeeds", async () => {
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
problem: "The run seam cannot route a structural finding.",
tool_area: "run-dev-terminal-action",
trigger: "generalist-dev / story native:01TESTAC2",
},
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

expect(result.ok).toBe(true);
expect(result.issueUrl).toBeDefined();
// Must target the plugin repo, not whatever cwd project is active.
expect(result.issueUrl).toContain("test-plugin-owner/test-plugin-repo");
expect(result.issueUrl).not.toContain("my-cwd-owner");
});

it("issueUrl is a valid pre-filled GitHub new-issue URL (not an API or auto-file endpoint)", async () => {
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
problem: "Issue URL shape check.",
tool_area: "record-maintainer-feedback",
trigger: "generalist-dev / story native:01TESTAC2B",
},
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

expect(result.issueUrl).toMatch(
/^https:\/\/github\.com\/test-plugin-owner\/test-plugin-repo\/issues\/new\?/,
);
expect(result.issueUrl).not.toContain("api/v3");
expect(result.issueUrl).not.toContain("/issues/create");
});

it("issueUrl body includes the problem and trigger fields", async () => {
const problem = "The reviewer cannot flag a structural engine defect.";
const trigger = "generalist-reviewer / story native:01TESTAC2C";

const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: { problem, tool_area: "run-reviewer-session", trigger },
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

const url = result.issueUrl ?? "";
const bodyParam = new URL(url).searchParams.get("body") ?? "";
expect(bodyParam).toContain(problem);
expect(bodyParam).toContain(trigger);
});

it("issueUrl is absent when readPluginPkgJsonImpl returns null (fail-soft)", async () => {
// When the plugin package.json is unreadable, the inbox write still
// succeeds — only the URL bonus is suppressed (fail-soft).
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
problem: "Fail-soft check — inbox write must still succeed.",
tool_area: "managed-fs",
trigger: "generalist-dev / story native:01TESTAC2D",
},
readPluginPkgJsonImpl: () => null,
});

expect(result.ok).toBe(true);
expect(result.id).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
expect(result.issueUrl).toBeUndefined();

// The inbox file must still have been written.
const stat = await import("node:fs").then((m) => m.promises.stat(result.absPath));
expect(stat.isFile()).toBe(true);
});

it("issueUrl is absent when the repository field is absent in the plugin package.json (fail-soft)", async () => {
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
problem: "Repository field absent check.",
tool_area: "build-feedback-issue-url",
trigger: "generalist-dev / story native:01TESTAC2E",
},
readPluginPkgJsonImpl: () => JSON.stringify({ name: "flow" }),
});

expect(result.ok).toBe(true);
expect(result.issueUrl).toBeUndefined();
});

it("also returns issueTitle and issueBody when issueUrl is present", async () => {
const result = await recordMaintainerFeedback({
targetRepoRoot: root,
item: {
problem: "Title and body fields check.",
tool_area: "review-maintainer-inbox",
trigger: "generalist-dev / story native:01TESTAC2F",
},
readPluginPkgJsonImpl: STUB_READ_PLUGIN_PKG_JSON,
});

expect(result.issueUrl).toBeDefined();
expect(result.issueTitle).toBeDefined();
expect(result.issueBody).toBeDefined();
expect(result.issueTitle).toContain("review-maintainer-inbox");
expect(result.issueBody).toContain("Title and body fields check.");
});
});
Loading
Loading