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
6 changes: 6 additions & 0 deletions changes/package-path-advisories.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"githits": minor
"@githits/mcp": minor
---

- **Package-path advisories** - Unified grep shows one compact warning per distinct package-path correction in CLI/MCP text and JSON, preserving results, failures and pending targets. Requires a backend exposing `GrepResult.pathWarnings`; injected grep providers must supply the warning array.
102 changes: 102 additions & 0 deletions docs/implementation/unified-grep.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,105 @@ different package, repository or site target; unknown reasons are not given an
invented remedy. Native public messages and error codes have their own reason
line. Terminal preparation reasons receive no preparation retry. Auth, requests
and JSON are unchanged. The shared helper owns only footer layout; grep owns these semantics.

## Package-path advisories

The backend owns path diagnosis using the selected snapshot's canonical package
repository subpath and native path observations. Clients do not infer package
roots from names, probe alternatives, rewrite selectors or retry automatically.
`GrepResult.pathWarnings` is a required array, selected and validated in both
compact and detailed modes. Each warning preserves `requestedInputIndices`,
`target`, `path`, `repositorySubpath`, `message`, and `suggestions { target path }`.
An empty list means no supported diagnosis was returned; zero matches alone are
not path evidence. Missing or malformed fields are protocol errors.

The shared projection validates and deep-copies the page, retaining hits, native
statuses/public messages, omissions, preparation estimates, traversal and cursors.
It replaces advisory objects with `pathWarnings: string[]` for CLI `--json` and
MCP `format: "json"`. Text uses the same compact sentences, one physical line
per distinct warning, after coverage/preparation and before matches:

```text
Warning: Path is prefixed with package path "packages/example". Did you mean "src"?
```

The package root and correction come directly from backend fields: the correction
is the suggestion whose target equals the warning's target. There is no local
prefix-stripping heuristic. When no same-target suggestion exists, the sentence
stops after the package path. The shared summary JSON-quotes values and escapes
DEL/C1 controls for both text and JSON. Text keeps each warning on one physical
line. Identical sentences are emitted once.
Neither output format dumps the verbose backend message or alternative pairs.
A question suggests a possible correction; it does not assert that a path exists.

The core `GrepService` contract retains complete backend metadata, including the
public `GrepPathWarning`/`GrepPathSuggestion` types exported by
`@githits/mcp/client`. Injected providers must supply the required array. Short
waits use the same parser: pending-only pages without evidence have no warning,
and mixed pages keep proven warnings beside pending targets. No warning changes
native failures, real results or search behavior.

### Deployment and release prerequisite

Backend PR [#3550](https://github.com/githits-com/pkgseer-backend/pull/3550)
merged at `b26172c5fc97287256e55d22bc64aa384297184c`. On 2026-10-24,
read-only GraphQL validation of a skipped `grep` selection accepted every
warning field at `https://oss-dev.githits.dev/api/graphql`. Production at
`https://oss.githits.dev/api/graphql` rejected `GrepResult.pathWarnings`.
A recheck at **2026-10-24 10:25 UTC** accepted every field on production. The
original package/prefix request returned one advisory with zero hits and
`COMPLETE`; the backend deployment prerequisite is now satisfied.
Introspection is disabled at both endpoints; the validation query uses
`@skip(if: true)` to check selections without executing grep.

Before releasing a client that selects this field, verify the configured
backend accepts the full selection below; merging its PR is not proof of
deployment. No schema fallback or downgraded query is provided: an older
endpoint rejects the entire grep query. Native advisory generation additionally requires the backend's released
aigrep 0.27.3/API56 path-evidence integration. Schema support alone does not
prove native advisory behavior; qualify an applicable request on that endpoint.

```graphql
query {
grep(targets: [{target: "npm:express"}], pattern: "router") @skip(if: true) {
pathWarnings {
requestedInputIndices target path repositorySubpath message
suggestions { target path }
}
}
}
```

Ship both the root `githits` CLI and public `@githits/mcp` package for their
respective consumers. Hosted MCP clients receive this behavior only after
`remote-mcp` upgrades that published package and deploys it through its own
approved workflow. This client PR authorizes no merge, release, publication or
deployment. The backend backlog's client-adoption entry remains for its owner
to close after this client work ships; it is not edited across worktrees.

### Client qualification (2026-10-24)

The compact implementation at `8e5ab06` was verified through built Node CLI and
local stdio MCP against dev. Prefix and exact selectors returned the same compact
warning string in text and JSON. Prefix returned zero hits with `COMPLETE`;
exact returned zero hits with `NON_RESUMABLE_PARTIAL` and the original
`GREP_PATH_NOT_INDEXED` failure. Hits, statuses, traversal and cursors matched.

The targeted `eval/agentic/probes/package-path-advisories.md` local MCP/dev run
used Codex (`gpt-6-luna`, high). Its trace contained one `quick_start` and two
grep calls with the same requested prefix (text, then JSON); no alternative
query was executed. The final answer recognized the correction and reported
`status: inconclusive` / `confidence: high`. No isolation violations were
recorded. This is observed behavior, not a graded usefulness/quality result.

Validation at that revision: 6,681 unit tests and 184 focused grep tests passed;
CI passed Linux/Windows tests, build checks, MCP package validation and Bun/Node
20/22/24/26 compatibility. Regression coverage includes projection, deduplication,
no same-target suggestion, one-line warnings, unusual values and DEL/C1 parity,
zero hits, native failures/cursors and mixed/pending-only short-wait results.

Source and built unauthenticated smoke modes passed. Full authenticated smoke
stops before grep at an existing recovery fixture; see the
[backlog entry](../plans/open-backlog.md#authenticated-smoke-recovery-fixture--2026-10-24).
The affected grep qualification above passed independently; no failure assertion
was weakened to bypass that broader-suite gap.
18 changes: 18 additions & 0 deletions docs/plans/open-backlog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Open backlog

## Authenticated smoke recovery fixture — 2026-10-24

During [PR #516](https://github.com/githits-com/githits-cli/pull/516), both full
authenticated smoke suites stopped before grep: the recovery probe expects
`go:github.com/docker/docker@v20.10.0` to fail, but dev now resolves it as
`v20.10.0+incompatible`. A nonexistent version returned `VERSION_NOT_FOUND`
without alternatives, so it cannot simply replace the existing probe, which
also checks backend-supplied recovery guidance. Unauthenticated smoke and the
affected built CLI/MCP grep qualification passed independently.

Ownership: the CLI/MCP smoke cohorts own the fixture and assertions; backend
recovery metadata owns the supplied alternatives. Investigate that boundary
before choosing a replacement fixture or correcting a verified producer issue.
Keep the assertions for error code, alternatives and native CLI/MCP recovery
syntax; do not turn the probe into a successful lookup or weaken coverage.
Acceptance: both authenticated dev smoke suites pass with a verified failing
version/ref and supplied alternatives, and continue through their live cohorts.

## Literal path round-trips — 2026-10-22

The path audit reproduced two accepted-input inconsistencies beyond ordinary
Expand Down
6 changes: 6 additions & 0 deletions eval/agentic/probes/package-path-advisories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Workload: Package-path advisory

Find the literal `skipLocked` in `npm:drizzle-orm` under the source path prefix
`drizzle-orm/src/pg-core`. Explain the returned result, any coverage limitation,
and any actionable path guidance. Preserve the requested scope; report possible
next actions without executing a different search.
73 changes: 73 additions & 0 deletions packages/core-internal/src/services/grep-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GrepGraphQLError,
GrepNetworkError,
type GrepParams,
type GrepPathWarning,
type GrepResult,
GrepServiceImpl,
MalformedGrepResponseError,
Expand All @@ -28,6 +29,7 @@ const params: GrepParams = {
function result(): GrepResult {
return {
indexingEstimates: [],
pathWarnings: [],
hits: [],
targets: [],
unavailableTargets: [],
Expand Down Expand Up @@ -581,3 +583,74 @@ describe("unified grep service", () => {
await expect(s.grep(params)).rejects.toThrow();
});
});

const pathWarning: GrepPathWarning = {
requestedInputIndices: [1, 0],
target: "npm:x",
path: "packages/x/src",
repositorySubpath: "packages/x",
message:
'Package paths are relative to the package root. Did you mean target "npm:x" with path "src"?',
suggestions: [
{ target: "npm:x", path: "src" },
{ target: `github:o/r@${"a".repeat(40)}`, path: "packages/x/src" },
],
};

describe("backend package-path advisories", () => {
for (const detailed of [false, true]) {
it(`selects and preserves every advisory field in ${detailed ? "detailed" : "compact"} mode without changing controls`, async () => {
const page = detailed ? detailedMixed() : mixed();
page.pathWarnings = [
pathWarning,
{ ...pathWarning, path: "packages/x/test", requestedInputIndices: [2] },
];
const requested = {
...params,
includeDetailedFields: detailed,
waitTimeoutMs: 0,
};
const fetcher = mock(
async (_url: Parameters<typeof fetch>[0], init?: RequestInit) => {
const body = JSON.parse(String(init?.body));
expect(body.variables).toEqual(requested);
expect(body.query).toMatch(
/pathWarnings\s*\{\s*requestedInputIndices target path repositorySubpath message\s*suggestions\s*\{ target path \}\s*\}/,
);
return response({ data: { grep: page } });
},
);
expect(await service(fetcher).grep(requested)).toEqual(page);
expect(fetcher).toHaveBeenCalledTimes(1);
});

it(`requires and validates the complete advisory contract in ${detailed ? "detailed" : "compact"} mode`, () => {
const page = detailed ? detailedMixed() : mixed();
expect(parseGrepResult(page, detailed).pathWarnings).toEqual([]);
for (const value of [
undefined,
null,
{},
[null],
[{ ...pathWarning, requestedInputIndices: [-1] }],
[{ ...pathWarning, requestedInputIndices: [0.5] }],
[{ ...pathWarning, suggestions: [{ target: "npm:x", path: null }] }],
]) {
expect(() =>
parseGrepResult({ ...page, pathWarnings: value }, detailed),
).toThrow(MalformedGrepResponseError);
}
for (const field of Object.keys(pathWarning)) {
const warning = { ...pathWarning };
Reflect.deleteProperty(warning, field);
expect(() =>
parseGrepResult({ ...page, pathWarnings: [warning] }, detailed),
).toThrow(MalformedGrepResponseError);
}
const warning = { ...pathWarning, suggestions: [{ path: "src" }] };
expect(() =>
parseGrepResult({ ...page, pathWarnings: [warning] }, detailed),
).toThrow(MalformedGrepResponseError);
});
}
});
31 changes: 31 additions & 0 deletions packages/core-internal/src/services/grep-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,22 @@ export interface GrepUnavailableTarget {
progressRef: string | null;
suggestedSiteTargets: string[] | null;
}
/** An unverified backend-authored alternative; never applied by the client. */
export interface GrepPathSuggestion {
target: string;
path: string;
}
/** Snapshot-backed path diagnosis, independent of matches and coverage failures. */
export interface GrepPathWarning {
requestedInputIndices: number[];
target: string;
path: string;
repositorySubpath: string;
message: string;
suggestions: GrepPathSuggestion[];
}
export interface GrepResult {
pathWarnings: GrepPathWarning[];
indexingEstimates?: DiscoveryIndexingEstimate[];
hits: GrepHit[];
targets: GrepTargetStatus[];
Expand Down Expand Up @@ -276,6 +291,18 @@ function resultSchema(detailed: boolean): z.ZodType<GrepResult> {
sourceMatchEndByte: selected(nonnegativeInt),
};
return z.object({
pathWarnings: z.array(
z.object({
requestedInputIndices: z.array(nonnegativeInt),
target: z.string(),
path: z.string(),
repositorySubpath: z.string(),
message: z.string(),
suggestions: z.array(
z.object({ target: z.string(), path: z.string() }),
),
}),
),
indexingEstimates: indexingEstimatesSchema,
hits: z.array(
z.discriminatedUnion("__typename", [
Expand Down Expand Up @@ -398,6 +425,10 @@ const GRAPHQL_QUERY = `query Grep(
contextLinesAfter: $contextLinesAfter, maxMatches: $maxMatches,
cursor: $cursor, waitTimeoutMs: $waitTimeoutMs) {
traversal nextCursor totalMatches
pathWarnings {
requestedInputIndices target path repositorySubpath message
suggestions { target path }
}
${INDEXING_ESTIMATES_SELECTION}
hits {
__typename
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export type {
GrepLineSlice,
GrepParams,
GrepPathSelector,
GrepPathSuggestion,
GrepPathWarning,
GrepReadAction,
GrepReadiness,
GrepRepositoryHit,
Expand Down
1 change: 1 addition & 0 deletions packages/mcp/src/shared/fixtures/grep-text/mixed-100.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"pathWarnings": [],
"traversal": "RESUMABLE_LIMIT",
"nextCursor": "TUdSUALkFPOMGmQmwMT9g3skeFq0I3qsSqWvFi2a7YQGlMZAKwACAABAAAEBZAAAAAAABAAAAAAAAAAAAAEgfKalBjGIr_089cv89Ti326x0GkmlpkM2twwG6FwuJwbjYzYyAAEAAAAAAgFkAAAAAAAEAIAAAAAAAAABAi2I6eMfCYe6LjvxHR-aNkEsz2Oumeyt0hFne_LMeIQTdmbjgUmp1C3fffsRaoTNAtvgGoT0aFOJQRyVtcNRvIUfNR0AAAAAADIAAAAAAAA",
"totalMatches": 100,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"pathWarnings": [],
"traversal": "RESUMABLE_LIMIT",
"nextCursor": "TUdSUAKW_xQN7o9Xb708KubQPUzUWIQwl9Drl5Y-eCSTrQjkPgABAABAAAEBZAAAAAAABAAAAAAAAAAAAAEgfKalBjGIr_089cv89Ti326x0GkmlpkM2twwG6FwuJwbjYzYAAAAQAD8egBfC0rKREwAAAE8AAAAAAA",
"totalMatches": 100,
Expand Down
Loading
Loading