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
26 changes: 26 additions & 0 deletions src/commands/feedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,32 @@ describe("feedbackAction", () => {
consoleSpy.mockRestore();
});

it("submits generic feedback when solution_id positional is omitted", async () => {
const submitFn = mock(() =>
Promise.resolve({
success: true,
message: "Feedback submitted successfully",
}),
);
const deps = createDeps({
githitsService: createMockGitHitsService({ submitFeedback: submitFn }),
});
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});

await feedbackAction(
undefined,
{ accept: true, message: "code_grep regex is fast on npm:lodash" },
deps,
);

expect(submitFn).toHaveBeenCalledWith({
solutionId: undefined,
accepted: true,
feedbackText: "code_grep regex is fast on npm:lodash",
});
consoleSpy.mockRestore();
});

it("exits with error when neither --accept nor --reject", async () => {
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
const exitSpy = spyOn(process, "exit").mockImplementation(() => {
Expand Down
50 changes: 34 additions & 16 deletions src/commands/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ export interface FeedbackDependencies {

/**
* Core feedback logic, separated for testability.
*
* `solutionId` is optional: when present, feedback is anchored to a
* specific `get_example` result; when omitted, the feedback is
* generic (covers code/package navigation, search, docs, or the
* overall experience).
*/
export async function feedbackAction(
solutionId: string,
solutionId: string | undefined,
options: FeedbackOptions,
deps: FeedbackDependencies,
): Promise<void> {
Expand Down Expand Up @@ -55,15 +60,23 @@ export async function feedbackAction(
}
}

const FEEDBACK_DESCRIPTION = `Submit feedback on a search result.
const FEEDBACK_DESCRIPTION = `Submit feedback on a tool result or the GitHits experience.

Rate whether a code example was helpful. Use --accept for positive
feedback or --reject for negative. Optionally add a message.
Two modes:
- Solution-tied: pass the [solution_id] from a prior 'githits example'
result (shown at the bottom of the markdown / under 'solution_id'
in --json) to anchor feedback to that specific result.
- Generic: omit [solution_id] to send feedback about any command
(search, pkg, docs, code) or the overall experience. A --message
is strongly recommended here.

Use --accept for positive feedback or --reject for negative.

Examples:
githits feedback abc123 --accept
githits feedback abc123 --reject -m "Example was outdated"
githits feedback abc123 --accept --message "Solved my problem" --json`;
githits feedback --accept -m "code_grep regex is fast on npm:lodash"
githits feedback --reject -m "search missing kotlin support"`;

/**
* Register the feedback command on the given program.
Expand All @@ -72,20 +85,25 @@ Examples:
export function registerFeedbackCommand(program: Command) {
program
.command("feedback")
.summary("Submit feedback on a search result")
.summary("Submit feedback on a tool result or the GitHits experience")
.description(FEEDBACK_DESCRIPTION)
.argument("<solution_id>", "Solution ID from search result")
.argument(
"[solution_id]",
"Solution ID from a prior 'githits example' result (omit for generic feedback)",
)
.addOption(new Option("--accept", "Mark as helpful").conflicts("reject"))
.addOption(new Option("--reject", "Mark as unhelpful").conflicts("accept"))
.option("-m, --message <text>", "Feedback explanation")
.option("--json", "Output as JSON for piping")
.action(async (solutionId: string, options: FeedbackOptions) => {
try {
const deps = await createContainer();
await feedbackAction(solutionId, options, deps);
} catch (error) {
if (error instanceof AuthRequiredError) process.exit(1);
throw error;
}
});
.action(
async (solutionId: string | undefined, options: FeedbackOptions) => {
try {
const deps = await createContainer();
await feedbackAction(solutionId, options, deps);
} catch (error) {
if (error instanceof AuthRequiredError) process.exit(1);
throw error;
}
},
);
}
65 changes: 65 additions & 0 deletions src/commands/mcp-instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,69 @@ describe("buildMcpInstructions", () => {
expect(mentioned.has(name)).toBe(true);
}
});

it("ships a decision tree mentioning all three workflow tools in the core block", () => {
const deps = createTestDeps();
const instructions = buildMcpInstructions(deps);
const coreEnd = instructions.indexOf("Indexed package/source tools");
const coreSection = instructions.slice(0, coreEnd);

expect(coreSection).toContain("`get_example`");
expect(coreSection).toContain("`search`");
expect(coreSection).toContain("`feedback`");
expect(coreSection).toContain("`search_language`");
});

it("orders package-section bullets by agent decision flow", () => {
const deps = createTestDeps();
const instructions = buildMcpInstructions(deps);

const positions = {
search: instructions.indexOf("- `search` —"),
searchStatus: instructions.indexOf("- `search_status`"),
codeGrep: instructions.indexOf("- `code_grep`"),
codeRead: instructions.indexOf("- `code_read`"),
codeFiles: instructions.indexOf("- `code_files`"),
docsList: instructions.indexOf("- `docs_list`"),
docsRead: instructions.indexOf("- `docs_read`"),
pkgInfo: instructions.indexOf("- `pkg_info`"),
pkgVulns: instructions.indexOf("- `pkg_vulns`"),
pkgDeps: instructions.indexOf("- `pkg_deps`"),
pkgChangelog: instructions.indexOf("- `pkg_changelog`"),
};

for (const [name, idx] of Object.entries(positions)) {
expect(idx).toBeGreaterThan(-1);
expect(`${name}=${idx}`).not.toContain("=-1");
}

// Discovery first, then code reading (grep → read → files), then
// docs, then package metadata. `code_files` follows `code_read`
// because it is a path-discovery fallback, not the step after a hit.
expect(positions.search).toBeLessThan(positions.searchStatus);
expect(positions.searchStatus).toBeLessThan(positions.codeGrep);
expect(positions.codeGrep).toBeLessThan(positions.codeRead);
expect(positions.codeRead).toBeLessThan(positions.codeFiles);
expect(positions.codeFiles).toBeLessThan(positions.docsList);
expect(positions.docsList).toBeLessThan(positions.docsRead);
expect(positions.docsRead).toBeLessThan(positions.pkgInfo);
expect(positions.pkgInfo).toBeLessThan(positions.pkgVulns);
expect(positions.pkgVulns).toBeLessThan(positions.pkgDeps);
expect(positions.pkgDeps).toBeLessThan(positions.pkgChangelog);
});

it("places the strategy tip after the bullets and the delegation tip before them", () => {
const deps = createTestDeps();
const instructions = buildMcpInstructions(deps);

const delegationIdx = instructions.indexOf(
"Delegate multi-call work to a sub-agent",
);
const firstBulletIdx = instructions.indexOf("- `search` —");
const lastBulletIdx = instructions.indexOf("- `pkg_changelog`");
const strategyIdx = instructions.indexOf("Strategy — reference-first");

expect(delegationIdx).toBeLessThan(firstBulletIdx);
expect(strategyIdx).toBeGreaterThan(lastBulletIdx);
});
});
120 changes: 67 additions & 53 deletions src/commands/mcp-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,46 @@ import type { Dependencies } from "../container.js";
* guidance for the same always-on tool surface the server registers.
*/

const CORE_BLOCK = `GitHits provides verified, canonical code examples from global open source. Use it for global solution synthesis and canonical examples when you're stuck, repeated attempts failed, you need up-to-date API usage, the question is comparative across OSS projects (e.g. "how does X vs Y handle Z"), the answer requires reading how a real codebase implements a feature, or the user mentions GitHits.
const CORE_BLOCK = `GitHits provides verified, canonical code examples from global open source for AI agents. Pick a path:

Example workflow: call \`get_example\` with one focused question; pass \`language\` only when you know the exact language name, otherwise call \`search_language\` first. Default output is markdown with a trailing \`solution_id\`; send \`feedback\` on that solution_id. Reuse prior results before searching again. For dependency-specific grounding, use package-scoped \`search\` before global \`get_example\`.`;
- Need a canonical example or cross-project pattern with no specific package pinned (you're stuck, repeated attempts failed, the user wants up-to-date API usage, or the user mentioned GitHits) — call \`get_example\` for global solution synthesis.
- Inspecting a specific known dependency or repository (stack trace points there, verifying how a particular library works, evaluating an upgrade) — call \`search\` plus the indexed code, docs, and package tools listed below.
- Question is comparative across OSS projects (e.g. "how does X vs Y handle Z") or requires reading how a real codebase implements a feature — combine the two: \`search\` for known targets, \`get_example\` for cross-project synthesis.
- Rate a result or share tool/UX feedback — call \`feedback\` with a \`solution_id\` from a prior \`get_example\` response (solution-tied) or omit it for generic feedback about any tool (\`search\`, \`code_*\`, \`docs_*\`, \`pkg_*\`) or the overall experience.

\`get_example\` workflow: pass \`language\` only when you know the exact name; otherwise call \`search_language\` first. Default output is markdown with a trailing \`solution_id\`. Reuse prior results before searching again. For dependency-specific grounding, prefer package-scoped \`search\` before global \`get_example\`.`;

const PACKAGE_TOOLS_PREAMBLE = `Indexed package/source tools inspect third-party dependency source, docs, and registry metadata. Use them when a stack trace points into a dependency, you need to verify how a library works, or you're evaluating whether to add or upgrade a package.

Package spec: \`registry:name[@version]\`. Default outputs are compact \`text-v1\` for agent context efficiency; pass \`format: "json"\` only when you need structured fields for programmatic parsing.`;

const PKG_INFO_BULLET =
'- `pkg_info` — compact package overview: latest version, license, downloads, quickstart, and active advisory count. Pass `format: "json"` for the structured envelope.';
const MULTI_TURN_TIP =
'**Delegate multi-call work to a sub-agent.** Code-navigation work (`search`, `code_grep`, `code_read`, `code_files`) often takes 3-10 calls. For cross-project comparisons, codebase mapping, pattern surveys, or "how does X actually work" investigations, spawn a sub-task/sub-agent and ask for a compact synthesis. Do it inline only when the raw snippet belongs in the main conversation.';

const SEARCH_BULLET =
'- `search` — unified search across indexed dependency code, docs, and symbols. Omit `sources` for AUTO. Use `sources:["code"]` for implementation/examples/tests text, `sources:["symbol"]` for precise API/entity lookup, and `sources:["docs"]` for guides/reference/changelogs. Default output is compact `text-v1` with ready-to-call follow-up arguments; pass `format: "json"` for structured locators, highlights, and source status. Complete by default; opt into partial hits with `allow_partial_results: true`. Incomplete responses carry a `searchRef`.';

const SEARCH_STATUS_BULLET =
"- `search_status` — follow up a prior `search` by `searchRef` to check progress, fetch partial hits, or fetch final results.";

const CODE_GREP_BULLET =
"- `code_grep` — deterministic text or regex grep over indexed source. Use it when you know the pattern; use `search` for discovery. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. Each match's `path:line` chains into `code_read`.";

const CODE_READ_BULLET =
"- `code_read` — read a dependency file by `path`. **MCP cap: 150 lines per call**. When you already have an exact path (e.g. from a stack trace), call this directly; otherwise locate it first with `search` or `code_grep` and pick a focused `start_line` / `end_line` window. Binary files set `isBinary: true` and omit `content`. On `FILE_NOT_FOUND` / `NOT_FOUND`, call `code_files` for the actual path.";

const CODE_FILES_BULLET =
'- `code_files` — list files in an indexed dependency. `path`, `path_prefix`, and `globs` are OR-ed selectors; extensions, language, file type, and file-intent filters intersect on top. Returned paths feed into `code_read` and help scope `code_grep`; pass `format: "json"` for language/type/size metadata.';

const DOCS_LIST_BULLET =
"- `docs_list` — browse hosted and repository-backed package docs. Entries include stable pageIds, source URLs, and repo-file follow-up metadata when available.";

const DOCS_READ_BULLET =
"- `docs_read` — read a documentation page by pageId. Works for both hosted docs and repo-backed docs.";

const PKG_INFO_BULLET =
'- `pkg_info` — compact package overview: latest version, license, downloads, quickstart, and active advisory count. Pass `format: "json"` for the structured envelope.';

const PKG_VULNS_BULLET =
'- `pkg_vulns` — compact known CVE / OSV advisory summary for npm, PyPI, Hex, or Crates packages, optionally pinned to `version`. Filter with `min_severity`; include retracted advisories with `include_withdrawn`. Pass `format: "json"` for per-advisory structured fields.';

Expand All @@ -39,29 +62,16 @@ const PKG_DEPS_BULLET =
const PKG_CHANGELOG_BULLET =
'- `pkg_changelog` — compact release notes for a package or GitHub repo, newest-first. Default latest mode returns recent entries with markdown body previews; `from_version` switches to range mode. Set `include_bodies: false` for a compact timeline or pass `format: "json"` for full bodies.';

const SEARCH_BULLET =
'- `search` — unified search across indexed dependency code, docs, and symbols. Omit `sources` for AUTO. Use `sources:["code"]` for implementation/examples/tests text, `sources:["symbol"]` for precise API/entity lookup, and `sources:["docs"]` for guides/reference/changelogs. Default output is compact `text-v1` with ready-to-call follow-up arguments; pass `format: "json"` for structured locators, highlights, and source status. Complete by default; opt into partial hits with `allow_partial_results: true`. Incomplete responses carry a `searchRef`.';

const SEARCH_STATUS_BULLET =
"- `search_status` — follow up a prior `search` by `searchRef` to check progress, fetch partial hits, or fetch final results.";

const CODE_FILES_BULLET =
'- `code_files` — list files in an indexed dependency. `path`, `path_prefix`, and `globs` are OR-ed selectors; extensions, language, file type, and file-intent filters intersect on top. Returned paths feed into `code_read` and help scope `code_grep`; pass `format: "json"` for language/type/size metadata.';

const CODE_READ_BULLET =
"- `code_read` — read a dependency file by `path`. **MCP cap: 150 lines per call**; choose focused `start_line` / `end_line` windows from `search` or `code_grep`. Binary files set `isBinary: true` and omit `content`. On `FILE_NOT_FOUND` / `NOT_FOUND`, call `code_files` for the actual path.";

const CODE_GREP_BULLET =
"- `code_grep` — deterministic text or regex grep over indexed source. Use it when you know the pattern; use `search` for discovery. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. Each match's `path:line` chains into `code_read`.";

const SEARCH_VS_SYMBOLS_TIP =
'Use code-first grounding for behavioral claims: source, symbols, tests, examples, and call sites beat docs prose. Prefer unified `search` for indexed dependency and repository discovery; use `sources:["symbol"]` when you want symbol-shaped results. Prefer `get_example` for global canonical example retrieval after package-scoped grounding. Use `code_grep` for deterministic text matching and `code_read` for focused-window inspection of a known file.';

const REFERENCE_FIRST_TIP =
"Strategy — reference-first, content-second. Locate symbols and lines with `search` / `code_grep` first, then read only the needed window with `code_read` using explicit `start_line` / `end_line` values, typically 80-150 lines around the match. The MCP `code_read` surface caps each call at 150 lines.";

const MULTI_TURN_TIP =
'**Delegate multi-call work to a sub-agent.** Code-navigation work (`search`, `code_grep`, `code_read`, `code_files`) often takes 3-10 calls. For cross-project comparisons, codebase mapping, pattern surveys, or "how does X actually work" investigations, spawn a sub-task/sub-agent and ask for a compact synthesis. Do it inline only when the raw snippet belongs in the main conversation.';
/**
* Combined strategy tip. Replaces the earlier
* `REFERENCE_FIRST_TIP` + `SEARCH_VS_SYMBOLS_TIP` pair, which
* overlapped: both told agents to grep-then-read and both
* contrasted `search`/`code_grep`/`get_example`. Phrase
* "reference-first" stays in the section name so prior agent
* habits and the test invariant continue to anchor here.
*/
const STRATEGY_TIP =
'Strategy — reference-first. Locate symbols and lines with `search` or `code_grep` first, then read the needed window with `code_read` using explicit `start_line` / `end_line` (typical 80-150 lines around the match; the MCP `code_read` surface caps each call at 150 lines). Source, symbols, tests, and call sites beat docs prose for behavioral claims; use `sources:["symbol"]` when you want symbol-shaped results, `code_grep` for deterministic text matching, and `get_example` for global canonical examples after package-scoped grounding.';

/**
* Build the server-level instructions string for the current session.
Expand All @@ -71,31 +81,35 @@ const MULTI_TURN_TIP =
* with the registered tool surface.
*/
export function buildMcpInstructions(_deps: Dependencies): string {
const sections = [CORE_BLOCK];

const bullets: string[] = [];
bullets.push(DOCS_LIST_BULLET);
bullets.push(DOCS_READ_BULLET);
bullets.push(PKG_INFO_BULLET);
bullets.push(PKG_VULNS_BULLET);
bullets.push(PKG_DEPS_BULLET);
bullets.push(PKG_CHANGELOG_BULLET);
bullets.push(SEARCH_BULLET);
bullets.push(SEARCH_STATUS_BULLET);
bullets.push(CODE_FILES_BULLET);
bullets.push(CODE_READ_BULLET);
bullets.push(CODE_GREP_BULLET);

const parts = [PACKAGE_TOOLS_PREAMBLE];
// Bullets ordered by agent decision flow: discovery (search) →
// code reading (grep, read, files) → docs → package metadata.
// `code_files` follows `code_read` because it is the path-
// discovery fallback when a known path doesn't resolve, not the
// step after a hit. Each bullet name↔registration is enforced by
// `mcp-instructions.test.ts`.
const bullets = [
SEARCH_BULLET,
SEARCH_STATUS_BULLET,
CODE_GREP_BULLET,
CODE_READ_BULLET,
CODE_FILES_BULLET,
DOCS_LIST_BULLET,
DOCS_READ_BULLET,
PKG_INFO_BULLET,
PKG_VULNS_BULLET,
PKG_DEPS_BULLET,
PKG_CHANGELOG_BULLET,
];

// Lead with delegation because it is the highest-leverage decision
// for code-navigation work. The reference-first / decision tips
// follow the bullets where they act as workflow guidance for the
// chosen tool.
parts.push(MULTI_TURN_TIP);
parts.push(bullets.join("\n"));
parts.push(REFERENCE_FIRST_TIP);
parts.push(SEARCH_VS_SYMBOLS_TIP);

sections.push(parts.join("\n\n"));
return sections.join("\n\n");
// for code-navigation work. The strategy tip follows the bullets
// where it acts as workflow guidance for the chosen tool.
const packageSection = [
PACKAGE_TOOLS_PREAMBLE,
MULTI_TURN_TIP,
bullets.join("\n"),
STRATEGY_TIP,
].join("\n\n");

return [CORE_BLOCK, packageSection].join("\n\n");
}
2 changes: 1 addition & 1 deletion src/commands/pkg/deps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ describe("pkgDepsAction", () => {
}

expect(errorSpy.mock.calls[0]?.[0]).toBe(
"pkg deps only supports npm, pypi, hex, crates, vcpkg, zig, rubygems, and go. Got: nuget.",
"pkg deps only supports npm, pypi, hex, crates, zig, vcpkg, rubygems, go. Got: nuget.",
);
errorSpy.mockRestore();
exitSpy.mockRestore();
Expand Down
Loading
Loading